1defmodule Mariaex.Coder do
2  @moduledoc """
3  Declarative generator for MySQL protocol messages, which can generate based on declarative description
4  decoder and encoder.
5
6  Example:
7
8        defcoder :text_cmd do
9          command 1
10          statement :string_eof
11        end
12
13  Will generate 2 functions:
14
15      __encode__({:text_cmd, 0x0e, "test"}) # => <<14, 116, 101, 115, 116>>
16
17      __decode__(:text_cmd, <<14, 116, 101, 115, 116>>) # => {:text_cmd, 14, "test"}
18
19  Additionally it generates record, like `Record.record(:text_cmd, [:command, :statement])`,
20  so that you can use it to create commands or access information in it.
21
22  Example would be: `text_cmd(command: 14, statement: "test")`
23
24  Check `Mariaex.Messages` for more examples.
25
26  For now, there is possible to insert custom functions for decoding of data. Example is in handshake
27  command:
28
29  See definition and implementation:
30
31      `auth_plugin_data2: {__MODULE__, auth_plugin_data2}`
32
33  It is used only for decoding, but it may change in the future for encoding.
34  """
35  defmacro __using__(_opts) do
36    quote do
37      import Mariaex.Coder, only: [defcoder: 2]
38      import Record, only: [defrecord: 2]
39      import Mariaex.Coder.Utils
40
41      @before_compile unquote(__MODULE__)
42      Module.register_attribute(__MODULE__, :decoders, accumulate: true)
43      Module.register_attribute(__MODULE__, :encoders, accumulate: true)
44    end
45  end
46
47  defmacro __before_compile__(env) do
48    decoders = Enum.reverse Module.get_attribute(env.module, :decoders)
49    encoders = Enum.reverse Module.get_attribute(env.module, :encoders)
50    [for {type, function} <- decoders do
51      quote do
52        def __decode__(unquote(type), body), do: unquote(function)(body)
53      end
54    end,
55    for {type, function} <- encoders do
56      quote do
57        def __encode__(unquote(type)() = rec), do: unquote(function)(rec)
58      end
59    end]
60  end
61
62  defmacro defcoder(name, [do: spec]) do
63    spec = case spec do
64             {:__block__, _meta, spec} -> spec
65             spec -> [spec]
66           end
67    keys = for {key, _, _} <- spec, key != :_, do: key
68    decoder = split_to_stages(spec) |> gen_stages(name, keys)
69    encoder = gen_encoder(name, spec, keys)
70    quote do
71      defrecord unquote(name), unquote(keys)
72      unquote(decoder)
73      unquote(encoder)
74    end
75  end
76
77  def gen_encoder(name, spec, keys) do
78    function = ("encode_" <> Atom.to_string(name)) |> String.to_atom
79    quote do
80      Module.put_attribute __MODULE__, :encoders, {unquote(name), unquote(function)}
81      def unquote(function)(unquote(name)(unquote(for key <- keys, do: {key, Macro.var(key, nil)}))) do
82        unquote({:<<>>, [], Enum.flat_map(spec, &match(&1, :encode))})
83      end
84    end
85  end
86
87  @empty_stage %{head: [], body: nil}
88
89  defp split_to_stages(spec) do
90    {last, other} = Enum.reduce(spec, {@empty_stage, []}, fn(kv = {_key, _, [value | _]}, {actual = %{head: head, body: _body}, all}) ->
91      cond do
92        is_integer(value) ->
93          {%{actual | head: [kv | head]}, all}
94        true ->
95          {@empty_stage, [%{actual | head: Enum.reverse([:next | head]), body: kv} | all]}
96      end
97    end)
98    case last do
99      %{head: [], body: nil} -> other
100      _ -> [%{last | head: Enum.reverse(last.head) } | other]
101    end |> Enum.reverse
102  end
103
104  defp gen_stages(allspec, name, keys) do
105    matches = gen_matches(allspec, keys)
106    function = ("decode_" <> Atom.to_string(name)) |> String.to_atom
107    quote do
108      Module.put_attribute __MODULE__, :decoders, {unquote(name), unquote(function)}
109      def unquote(function)(next) do
110        unquote_splicing(matches)
111        unquote(name)(unquote(for key <- keys, do: {key, Macro.var(key, nil)}))
112      end
113    end
114  end
115
116  defp gen_matches(allspec, keys) do
117    for spec <- allspec do
118      body = gen_body(spec[:body], keys)
119      quoted_head = case spec[:head] do
120        [:next] ->
121          []
122        head ->
123          binary_match = {:<<>>, [], Enum.flat_map(head, &match(&1, :decode))}
124          [(quote do: unquote(binary_match) = next)]
125      end
126      quoted_head ++ [body]
127    end |> List.flatten
128  end
129
130  defp gen_body({key, _, [:length_string]}, _) do
131    quote do
132      <<length :: size(8)-little, unquote(Macro.var(key, nil)) :: size(length)-binary, next :: binary>> = next
133    end
134  end
135
136  defp gen_body({key, _, [{module, function}]}, _) do
137    quote do: {unquote(Macro.var(key, nil)), next} = apply(unquote(module), unquote(function), [next])
138  end
139
140  defp gen_body({key, _, [:length_encoded_integer]}, _) do
141    quote do: {unquote(Macro.var(key, nil)), next} = length_encoded_integer(next)
142  end
143
144  defp gen_body({key, _, [:length_encoded_string]}, _) do
145    quote do: {unquote(Macro.var(key, nil)), next} = length_encoded_string(next)
146  end
147
148  defp gen_body({key, _, [:length_encoded_string, :until_eof]}, _) do
149    quote do: unquote(Macro.var(key, nil)) = length_encoded_string_eof(next)
150  end
151
152  defp gen_body({key, _, [:string]}, _) do
153    quote do: [unquote(Macro.var(key, nil)), next] = :binary.split(next, <<0>>)
154  end
155
156  defp gen_body({key, _, [:string_eof]}, _) do
157    quote do: unquote(Macro.var(key, nil)) = next
158  end
159
160  defp gen_body({key, _, [function]}, _keys) do
161    quote do
162      size = unquote(function) * 8
163      <<unquote(Macro.var(key, nil)) :: size(size), next :: binary>> = next
164    end
165  end
166
167  defp gen_body({key, _, [function, :string]}, _keys) do
168    quote do
169      size = unquote(function)
170      <<unquote(Macro.var(key, nil)) :: size(size)-binary, next :: binary>> = next
171    end
172  end
173  defp gen_body(nil, _keys) do
174    []
175  end
176
177  defp match({:_, _, [length]}, _) when is_integer(length) do
178    [quote do: 0 :: unquote(length)*8]
179  end
180  defp match({key, _, [length]}, _) when is_integer(length) do
181    [quote do: unquote(Macro.var(key, nil)) :: size(unquote(length*8))-little]
182  end
183  defp match({key, _, [length, :string]}, _) do
184    [quote do: unquote(Macro.var(key, nil)) :: size(unquote(length))-binary]
185  end
186  defp match(:next, _) do
187    [quote do: next :: binary]
188  end
189  defp match({key, _, [:string]}, _) do
190    [(quote do: unquote(Macro.var(key, nil)) :: binary),
191     (quote do: 0 :: 8)]
192  end
193  defp match({key, _, [:length_string]}, :encode) do
194    [(quote do: byte_size(unquote(Macro.var(key, nil))) :: 8 ),
195     (quote do: unquote(Macro.var(key, nil)) :: binary)]
196  end
197  defp match({key, _, [:string_eof]}, :encode) do
198    [(quote do: unquote(Macro.var(key, nil)) :: binary)]
199  end
200  # this clauses are wrong, because it is imposible to generate this kind of integer in a binary match
201  defp match({key, _, [:length_encoded_integer]}, :encode) do
202    [(quote do: unquote(Macro.var(key, nil)) :: integer)]
203  end
204  defp match({key, _, [:length_encoded_string | _]}, :encode) do
205    [(quote do: unquote(Macro.var(key, nil)) :: binary)]
206  end
207  # All custom implementations are ignored yet
208  defp match({key, _, [{_module, _function}]}, :encode) do
209    [(quote do: unquote(Macro.var(key, nil)) :: binary)]
210  end
211
212  defmodule Utils do
213    def length_encoded_string(bin) do
214      {length, next} = length_encoded_integer(bin)
215      << string :: size(length)-binary, next :: binary >> = next
216      {string, next}
217    end
218
219    def length_encoded_string_eof(bin, acc \\ []) do
220      case length_encoded_string(bin) do
221        {value, ""} ->
222          Enum.reverse([value | acc])
223        {value, rest} ->
224          length_encoded_string_eof(rest, [value | acc])
225      end
226    end
227
228    def length_encoded_integer(bin) do
229      case bin do
230        << value :: 8, rest :: binary >> when value <= 250 -> {value, rest}
231        << 252 :: 8, value :: 16-little, rest :: bits >> -> {value, rest}
232        << 253 :: 8, value :: 24-little, rest :: bits >> -> {value, rest}
233        << 254 :: 8, value :: 64-little, rest :: bits >> -> {value, rest}
234      end
235    end
236
237    def to_length_encoded_integer(int) do
238      case int do
239        int when int <= 250 -> << int :: 8 >>
240        int when int <= 65535 -> << 252 :: 8, int :: 16-little >>
241        int when int <= 16777215 -> << 253 :: 8, int :: 24-little >>
242        int -> << 254 :: 8, int :: 64-little >>
243      end
244    end
245  end
246end
247