Last active
October 7, 2020 17:24
-
-
Save princejwesley/bc4b164f277b331916da106840b80990 to your computer and use it in GitHub Desktop.
Minimal LINQ like Query for elixir
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Enum.Query.CompileError do | |
@moduledoc ~S""" | |
Raise compile time error while building query | |
""" | |
defexception [:message] | |
end | |
defmodule Enum.Query do | |
@moduledoc ~S""" | |
Query builder of below form: | |
from x in xs, | |
:where x > 100 | |
from x in xs, | |
:where x > 100, | |
:where div(x, 4), | |
:select x + 2 | |
from {x,y} in xys, | |
:where x in ["yes", "no"], | |
:select y | |
""" | |
# Query clauses | |
@clauses [:where, :select] | |
@doc ~S""" | |
build from query | |
""" | |
defmacro from(expr, kws \\ []) do | |
# second argument must be keyword lists | |
unless Keyword.keyword?(kws), do: raise ArgumentError, "second argument must be a keyword list" | |
from!(expr, kws, nil) | |
end | |
defp from!({:in, _, [vars, rvalue]} = expr, [{clause, action} | t], output) when clause in @clauses do | |
query = if output, do: output, else: rvalue | |
from!(expr, t, build_clause!(vars, query, clause, action)) | |
end | |
defp from!({:in, _, _}, [], output) do | |
output | |
end | |
defp from!(_, _, _) do | |
#support only `from var in enumerables` form | |
raise ArgumentError, "first argument must be of form 'from var in enumerables'" | |
end | |
defp extract_vars(vars) do | |
case vars do | |
{v, _, _} -> Atom.to_string(v) | |
v when is_tuple(v) -> Tuple.to_list(v) |> Enum.map(&extract_vars/1) | |
end | |
end | |
defp vars!(vars) do | |
v = List.wrap(vars) |> Enum.map(&extract_vars/1) | |
dups = v -- Enum.uniq(v) | |
unless dups == [], do: query_error("`#{hd dups}` is bound more than one time") | |
nil | |
end | |
defp build_clause!(vars, rvalue, :where, action) do | |
quote do | |
Enum.filter(unquote(rvalue), fn (unquote(vars)) -> unquote(action) end) | |
end | |
end | |
defp build_clause!(vars, rvalue, :select, action) do | |
quote do | |
Enum.map(unquote(rvalue), fn (unquote(vars)) -> unquote(action) end) | |
end | |
end | |
# drop frames of ours | |
defp query_error(msg) do | |
# old = :erlang.system_flag(:backtrace_depth, 20) | |
#ignore Process.info stack trace | |
{:current_stacktrace, [_|st]} = Process.info(self, :current_stacktrace) | |
st = Enum.drop_while st, fn {name, _, _, _} -> String.starts_with?(Atom.to_string(name), ["Elixir.Enum.Query", "elixir_"]) end | |
# old = :erlang.system_flag(:backtrace_depth, old) | |
reraise %Enum.Query.CompileError{message: msg}, st | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Simple enumerable LINQ like macro with
where
andselect
clauses.