Slide 1

Slide 1 text

Comredis Iuri Fernandes Redis, Elixir, Ruby, Metaprogramming, Testing and other Buzzwords

Slide 2

Slide 2 text

Ruby require "redis" redis = Redis.new redis.set("hello ", "world") # => "OK" redis.get("hello") # => "world"

Slide 3

Slide 3 text

Connection management Authentication Communication (Request-response) Client RESP Server

Slide 4

Slide 4 text

“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)

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

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"}

Slide 8

Slide 8 text

Elixir Redix.pipeline(conn, [ ~w(INCR foo), ~w(INCR foo), ~w(INCR foo 2) ]) #=> {:ok, [1, 2, 4]}

Slide 9

Slide 9 text

Elixir Redix.pipeline(conn, [ ~w(INCR foo), ~w(INCR foo), ~w(INCRBY foo 2) ]) #=> {:ok, [1, 2, 4]}

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

Ruby redis = Redis.new redis.set("hello ", "world") # => "OK" redis.get("hello") # => "world"

Slide 12

Slide 12 text

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"}

Slide 13

Slide 13 text

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"}

Slide 14

Slide 14 text

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"}

Slide 15

Slide 15 text

Elixir defmodule Commands do def get(key) do ~w(GET #{key}) end def set(key, value) do ~w(SET #{key} #{value}) end # ... end

Slide 16

Slide 16 text

Bad idea!

Slide 17

Slide 17 text

lib/redis.rb => 2730 LOC* * Includes a lot of documentation * Handles responses * It does not support all the commands Not for the redis-rb

Slide 18

Slide 18 text

Imagine if I could generate all these functions automatically! You can! Metaprogramming FTW! Better, at compile time!

Slide 19

Slide 19 text

{ ... "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

Slide 20

Slide 20 text

Parser (Poison) Commands JSON specification Command structured data Elixir macros Functions

Slide 21

Slide 21 text

Macro rules Rule 1: Don’t Write Macros Rule 2: Use Macros Gratuitously

Slide 22

Slide 22 text

Elixir defmodule Comredis do use Comredis.Command.Generator ... end

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

Abstract Syntax Tree Expression quote unquote 1 + 1

Slide 25

Slide 25 text

quote unquote 1 + 1 {:+, [context: Elixir, import: Kernel], [1, 1]}

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

How to test it? Don’t test code generation, but the generated code Write tests for each function? No, automatically test all of them

Slide 31

Slide 31 text

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)

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

What is missing? Type checking to use Dialyzer More documentation Something else?

Slide 34

Slide 34 text

Who uses similar ideas? Elixir - unicode representation ex2ms - ETS match expressions

Slide 35

Slide 35 text

Questions? @fqiuri github.com/iurifq/comredis