As I continue to work on Exvalibur
, the generator for blazingly
fast validators of maps based on sets of predefined rules, I have implemented custom guards and pattern matching
of values. Now one might specify rules as
rules = [
%{matches: %{currency_pair: ~Q[<<"EUR", _::binary>>], valid: ~Q[valid]},
conditions: %{rate: %{min: 1.0, max: 2.0}},
guards: ["is_boolean(valid)"]
}]
What’s going on here? The above will match the map, having keys currency_pair
and valid
(specified explicitly)
and key rate
specified implicitly through the condition
. The resulting validator module will contain the
following positive validation clause:
def valid?(%{currency_pair: <<"EUR", _::binary>>, valid: valid} = mâp)
when rate >= 1.0 and rate <= 2.0 and is_boolean(valid) do
{:ok,
%{currency_pair: mâp[:currency_pair],
valid: mâp[:valid],
rate: mâp[:rate]}}
end
As one can see, the quoted expression are to be used for both pattern match declaration and introducing
the variable to be used in custom guard. The reason is we cannot just put an arbitrary expression as a map value.
That said, rules = [%{currency_pair: <<"EUR", _::binary>>}]
won’t pass the compilation stage.
To quote the expression we use the custom sigils.
We ultimately want to support interpolation inside this sigil to allow dynamic expressions
def rule_for_currency(<<currency::binary-size(3), _::binary>>) do
[%{currency_pair: ~q[<<"#{currency}", _::binary>>]}]
end
Luckily enough, there is the Elixir core where we might borrow the implementation from, slightly modified. I would post here the most interesting clause, the rest might be easily found in the source code repository.
defmacro sigil_q({:<<>>, meta, pieces}, []) do
tokens =
case :elixir_interpolation.unescape_tokens(pieces) do
{:ok, unescaped_tokens} -> unescaped_tokens
{:error, reason} -> raise ArgumentError, to_string(reason)
end
quote do
Code.string_to_quoted!(
unquote({:<<>>, meta, tokens}), unquote(meta)
)
end
end
We delegate the interpolation to Elixir core, and then construct an AST out of this string. Easy-peasy.
For plain variables, as in the first example, ~Q[var]
works exactly as Macro.var(var, nil)
, constructing an AST tuple
like {:var, [line: 1], nil}
.
Conclusion
Custom sigils are nifty and I always wanted to find the application for one. Here I go.
Happy custom validating!