Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Comredis

 Comredis

How to use elixir metaprogramming to generate syntatically correct Redis commands.

Iuri Fernandes

April 28, 2016
Tweet

More Decks by Iuri Fernandes

Other Decks in Programming

Transcript

  1. “Requests are sent from the client to the Redis server

    as arrays of strings representing the arguments of the command to execute.” –Redis documentation RESP (REdis Serialization Protocol)
  2. Ruby # Get the value of a key. # #

    @param [String] key # @return [String] def get(key) synchronize do |client| client.call([:get, key]) end end https:/ /github.com/redis/redis-rb/blob/master/lib/ redis.rb
  3. Elixir {:ok, conn} = Redix.start_link # => {:ok, #PID<0.310.0>} Redix.command(conn,

    ~w(SET hello world)) # => {:ok, "OK"} Redix.command(conn, ~w(GET hello)) # => {:ok, "world"}
  4. Elixir {:ok, conn} = Redix.start_link # => {:ok, #PID<0.310.0>} Redix.command(conn,

    ~w(SET hello world)) # => {:ok, "OK"} Redix.command(conn, ~w(GET hello)) # => {:ok, "world"}
  5. Elixir {:ok, conn} = Redix.start_link # => {:ok, #PID<0.310.0>} Redix.command(conn,

    set("hello", "world"))) # => {:ok, "OK"} Redix.command(conn, get("hello")) # => {:ok, "world"}
  6. Elixir {:ok, conn} = Redix.start_link # => {:ok, #PID<0.310.0>} conn

    |> Redix.command(set("hello", "world"))) # => {:ok, "OK"} conn |> Redix.command(get("hello")) # => {:ok, "world"}
  7. Elixir defmodule Commands do def get(key) do ~w(GET #{key}) end

    def set(key, value) do ~w(SET #{key} #{value}) end # ... end
  8. lib/redis.rb => 2730 LOC* * Includes a lot of documentation

    * Handles responses * It does not support all the commands Not for the redis-rb
  9. Imagine if I could generate all these functions automatically! You

    can! Metaprogramming FTW! Better, at compile time!
  10. { ... "GET": { "summary": "Get the value of a

    key", "complexity": "O(1)", "arguments": [ { "name": "key", "type": "key" } ], "since": "1.0.0", "group": "string" }, ... } https:/ /github.com/antirez/redis-doc/blob/master/commands.json
  11. Elixir defmodule Comredis.Command.Generator do defmacro __using__(_options) do for command <-

    FileReader.load do generate(command) end end defp generate(command = %Command{}) do quote do @doc unquote doc(command) unquote bodies(command, Argument.split_options(command.arguments)) end end ... end
  12. Elixir defp bodies(command, {required_args, []}) do args = argument_names(required_args) quote

    do def unquote(command.canonical_name)(unquote_splicing(args)) do List.flatten [unquote(command.name), unquote_splicing(args)] end end end
  13. Elixir defp bodies(%Command{canonical_name: :get, name: "GET"}, {[%Argument{canonical_name: :key}], []}) do

    args = [{:key, [], Elixir}] quote do def unquote(command.canonical_name)(unquote_splicing(args)) do List.flatten [unquote(command.name), unquote_splicing(args)] end end end defp bodies(%Command{canonical_name: :get, name: "GET"}, {%Argument{canonical_name: :key}, []}) do quote do def get(key) do List.flatten ["GET", key] end end end
  14. How to test it? Don’t test code generation, but the

    generated code Write tests for each function? No, automatically test all of them
  15. Property-based testing Generator: random arguments compliant to command arguments Property:

    commands have a valid syntax Use a Redis server as a test oracle github.com/parroty/excheck (triq)