lib/permit/permissions/condition.ex

defmodule Permit.Permissions.Condition do
  @moduledoc """
     Condition
  """
  @enforce_keys [:condition, :condition_type]
  defstruct [:condition, :condition_type, :semantics]

  alias __MODULE__
  alias Permit.Types
  alias Permit.Permissions.Condition.Operators
  require Permit.Permissions.Condition.Operators
  import Ecto.Query

  @type condition_type :: :const | :function_1 | :function_2 | {:operator, module()}
  @type t :: %Condition{
          condition: Types.condition(),
          condition_type: condition_type(),
          semantics: (any() -> boolean())
        }

  @eq_operators Operators.eq_operators()
  @eq Operators.eq()
  @operators Operators.all()

  @spec new(Types.condition()) :: Condition.t()
  def new({key, {:not, nil}})
      when is_atom(key) do
    %Condition{
      condition: {key, {@eq, nil, [not: true]}},
      condition_type: {:operator, Operators.IsNil},
      semantics: Operators.IsNil.semantics(not: true).(nil)
    }
  end

  def new({key, nil})
      when is_atom(key) do
    %Condition{
      condition: {key, {@eq, nil, []}},
      condition_type: {:operator, Operators.IsNil},
      semantics: Operators.IsNil.semantics().(nil)
    }
  end

  def new({key, {operator, value}})
      when operator in @operators and is_atom(key) and not is_nil(value),
      do: new({key, {operator, value, []}})

  def new({key, {{:not, operator}, value}})
      when operator in @operators and is_atom(key) and not is_nil(value),
      do: new({key, {operator, value, not: true}})

  def new({key, {{:not, operator}, value, ops}})
      when operator in @operators and is_atom(key) and not is_nil(value),
      do: new({key, {operator, value, [{:not, true}, ops]}})

  def new({key, {operator, nil, ops}})
      when operator in @eq_operators and is_atom(key) do
    if Keyword.get(ops, :not, false) do
      new({key, {:not, nil}})
    else
      new({key, nil})
    end
  end

  def new({key, {operator, value, ops}})
      when is_atom(operator) and is_atom(key) and not is_nil(value) do
    case Operators.get(operator) do
      {:ok, module} ->
        %Condition{
          condition: {key, {operator, value, ops}},
          condition_type: {:operator, module},
          semantics: module.semantics(ops).(value)
        }

      :error ->
        raise "Unsupported operator #{inspect(operator)}"
    end
  end

  def new({key, value})
      when is_atom(key) and not is_nil(value) do
    {:ok, operator} = Operators.get(@eq)

    %Condition{
      condition: {key, {@eq, value, []}},
      condition_type: {:operator, operator},
      semantics: operator.semantics().(value)
    }
  end

  def new(true),
    do: %Condition{condition: true, condition_type: :const}

  def new(false),
    do: %Condition{condition: false, condition_type: :const}

  def new(function) when is_function(function, 1),
    do: %Condition{condition: function, condition_type: :function_1}

  def new(function) when is_function(function, 2),
    do: %Condition{condition: function, condition_type: :function_2}

  @spec satisfied?(Condition.t(), Types.resource(), Types.subject()) :: boolean()
  def satisfied?(%Condition{condition: condition, condition_type: :const}, _record, _subject),
    do: condition

  def satisfied?(
        %Condition{condition: {key, _}, condition_type: {:operator, _}, semantics: function},
        record,
        _subject
      )
      when is_struct(record) do
    record
    |> Map.get(key)
    |> then(function)
  end

  def satisfied?(%Condition{condition: condition}, module, _subject)
      when is_atom(module),
      do: !!condition

  def satisfied?(%Condition{condition: function, condition_type: :function_1}, record, _subject),
    do: !!function.(record)

  def satisfied?(%Condition{condition: function, condition_type: :function_2}, record, subject),
    do: !!function.(subject, record)

  @spec to_dynamic_query(Condition.t()) :: {:ok, term} | {:error, term()}
  def to_dynamic_query(%Condition{condition: condition, condition_type: :const}),
    do: {:ok, dynamic(^condition)}

  def to_dynamic_query(%Condition{
        condition: {key, {_op, val, ops}} = condition,
        condition_type: {:operator, operator}
      }) do
    case operator.dynamic_query(key, ops) do
      nil ->
        {:error, {:condition_unconvertible, condition}}

      query ->
        {:ok, query.(val)}
    end
  end

  def to_dynamic_query(%Condition{condition_type: other}),
    do: {:error, {:condition_unconvertible, other}}
end