# {{{ WhereClause module defmodule Sorcery.Query.WhereClause do @moduledoc false defstruct [:lvar, :tk, :attr, :left, :right, :op, :other_lvar, :other_lvar_attr, :arg_name, :right_type] @type t :: %__MODULE__{ lvar: binary(), tk: atom(), attr: atom(), left: any(), right: any(), op: atom(), other_lvar: nil | binary(), other_lvar_attr: nil | atom(), arg_name: nil | atom(), right_type: :literal | :lvar | :arg } def new([lvar, tk, clauses]) when is_list(clauses) do Enum.map(clauses, fn {attr, value} -> new([lvar, tk, attr, value]) end) end def new([lvar, tk, attr, value]) do {op, right} = split_values(value) right_type = get_right_type(right) lvar? = references_lvar?(right) #op = if lvar?, do: :in, else: op {other_lvar, other_lvar_attr} = split_lvar(lvar?, right) arg_name = arg_name(right) struct(__MODULE__, %{ lvar: String.to_atom(lvar), tk: tk, attr: attr, right: right, op: op, other_lvar: other_lvar, other_lvar_attr: other_lvar_attr, arg_name: arg_name, right_type: right_type }) end # {{{ private defp get_right_type("?" <> _), do: :lvar defp get_right_type(right) when is_atom(right) do case "#{right}" do "args_" <> _ -> :arg _ -> :literal end end defp get_right_type(_), do: :literal defp arg_name(right) when is_atom(right) do case "#{right}" do "args_" <> argname -> String.to_atom(argname) _ -> nil end end defp arg_name(_), do: nil defp references_lvar?("?" <> _), do: true defp references_lvar?(_), do: false defp split_values({op, right}), do: {op, right} defp split_values(right), do: {:==, right} defp split_lvar(false, _), do: {nil, nil} defp split_lvar(_, str) do case String.split(str, ".") do [root, attr] -> {String.to_atom(root), String.to_atom(attr)} [root] -> {String.to_atom(root), nil} end end # }}} end # }}} defmodule Sorcery.Query do @moduledoc ~s""" A query module defines, in plain elixir data structures, the kind of data we want to watch for. The syntax takes some inspiration from Datalog, but with many differences as well. """ alias Sorcery.Query.WhereClause defstruct [ :refstr, args: %{}, where: [], find: %{}, lvar_tks: [] ] @type t :: %__MODULE__{refstr: String.t(), where: list(WhereClause), find: map()} def new(opts) do ref = "#{inspect(make_ref())}" lvar_tks = Enum.map(opts[:where], fn [lvar, tk | _] -> {lvar, tk} end) |> Enum.uniq() opts = opts |> Map.put_new(:refstr, ref) |> Map.put_new(:lvar_tks, lvar_tks) struct(__MODULE__, opts) end defmacro __using__(opts) do quote bind_quoted: [opts: opts] do @raw_struct Sorcery.Query.new(opts) def raw_struct(), do: @raw_struct def tks_affected() do @raw_struct.where |> Enum.map(&(Enum.at(&1, 1))) |> Enum.uniq() end def finds() do find = @raw_struct.find Enum.reduce(find, %{}, fn {str, :*}, acc -> Map.put(acc, String.to_atom(str), :*) {str, li}, acc -> lvarkey = String.to_existing_atom(str) clause_attrs = Enum.filter(__MODULE__.clauses(), fn %{lvar: lvar} -> lvar == lvarkey end) |> Enum.map(&(&1.attr)) li = clause_attrs ++ [:id | li] |> Enum.uniq() Map.put(acc, lvarkey, li) end) end def clauses() do for clause <- @raw_struct.where do Sorcery.Query.WhereClause.new(clause) end |> List.flatten() end def clauses(args) do Enum.map(clauses(), fn %{right_type: :arg, arg_name: k} = clause -> clause |> Map.put(:right, args[k]) |> Map.put(:arg_name, nil) |> Map.put(:right_type, :literal) clause -> clause end) end end end end