defmodule Janus.Policy.Rule do
@moduledoc """
Defines a rule for an individual schema and action.
"""
@valid_clauses [:where, :where_not, :or_where]
defstruct [
:schema,
:action,
allow: [],
deny: []
]
@type t :: %__MODULE__{
schema: Janus.schema_module(),
action: Janus.action(),
allow: [keyword() | boolean()],
deny: [keyword() | boolean()]
}
@doc false
def new(schema, action) do
%__MODULE__{schema: schema, action: action}
end
@doc false
def apply_rule(rule, :deny, []), do: Map.merge(rule, %{allow: [], deny: [[]]})
def apply_rule(rule, field, opts) do
opts = parse_opts!(opts)
if [] in rule.deny do
rule
else
Map.update(rule, field, [opts], &[opts | &1])
end
end
@doc false
def merge(
%{schema: s, action: a} = rule,
%{schema: s, action: a, allow: allow, deny: deny}
) do
rule = Enum.reduce(allow, rule, &apply_rule(&2, :allow, &1))
rule = Enum.reduce(deny, rule, &apply_rule(&2, :deny, &1))
rule
end
defp parse_opts!(opts) do
with true <- Keyword.keyword?(opts),
[] <- opts |> Keyword.keys() |> Enum.uniq() |> Kernel.--(@valid_clauses) do
combine(opts)
else
opts when is_list(opts) -> invalid_opts!(opts)
_ -> invalid_opts!(opts)
end
end
defp invalid_opts!(value) do
raise ArgumentError, "invalid options passed to `allow` or `deny`: `#{inspect(value)}`"
end
defp combine(opts, acc \\ [])
defp combine([{:or_where, or_clause} | opts], acc) do
combine(opts, [{:or, {:where, or_clause}, acc}])
end
defp combine([clause | opts], acc) do
combine(opts, [clause | acc])
end
defp combine([], acc), do: Enum.reverse(acc)
end