lib/permit.ex

defmodule Permit do
  @moduledoc """
  Authorization facilities for the application.
  """
  defstruct roles: [], permissions: Permit.Permissions.new(), subject: nil

  alias Permit.HasRoles
  alias Permit.Permissions
  alias Permit.Types

  @type t :: %Permit{
          roles: [Types.role()],
          permissions: Permissions.t(),
          subject: Types.subject() | nil
        }

  defmacro __using__(opts) do
    alias Permit.Types

    permissions_module = Keyword.fetch!(opts, :permissions_module)

    predicates =
      permissions_module
      |> Macro.expand(__CALLER__)
      |> apply(:actions_module, [])
      |> apply(:list_groups, [])
      |> Enum.map(&add_predicate_name/1)
      |> Enum.map(fn {predicate, name} ->
        quote do
          @spec unquote(predicate)(Permit.t(), Types.resource()) :: boolean()
          def unquote(predicate)(authorization, resource) do
            Permit.verify_record(authorization, resource, unquote(name))
          end
        end
      end)

    quote do
      @doc """
      Initializes a structure holding permissions for a given user role.

      Returns a Permit struct.
      """

      require unquote(permissions_module)

      def actions_module,
        do: unquote(permissions_module).actions_module()

      @spec can(HasRoles.t()) :: Permit.t()
      def can(nil),
        do: raise("Unable to create permit authorization for nil role/user")

      def can(who) do
        who
        |> HasRoles.roles()
        |> Stream.map(fn role ->
          unquote(permissions_module).can(role)
        end)
        |> Enum.reduce(fn auth1, auth2 ->
          %Permit{auth1 | permissions: Permissions.join(auth1.permissions, auth2.permissions)}
        end)
        |> Map.put(:roles, HasRoles.roles(who))
        |> Map.put(:subject, (is_struct(who) && who) || nil)
      end

      def resolver_module, do: Permit.Resolver

      defoverridable resolver_module: 0

      unquote(predicates)

      # defdelegate authorize_and_preload_one!
    end
  end

  @spec has_subject(Permit.t()) :: boolean()
  def has_subject(%Permit{subject: nil}), do: false
  def has_subject(%Permit{subject: _}), do: true

  @spec do?(Permit.t(), Types.action_group(), Types.resource()) :: boolean()
  def do?(authorization, action, resource) do
    Permit.verify_record(authorization, action, resource)
  end

  # @spec add_permission(
  #         Permit.t(),
  #         Types.action_group(),
  #         Types.resource_module(),
  #         list(),
  #         Types.condition()
  #       ) ::
  #         Permit.t()
  # def add_permission(authorization, action, resource, bindings, conditions)
  #     when is_list(conditions) do
  #   parsed_conditions = parse_conditions(bindings, conditions)

  #   authorization.permissions
  #   |> Permissions.add(action, resource, parsed_conditions)
  #   |> then(&%Permit{authorization | permissions: &1})
  # end

  @spec verify_record(Permit.t(), Types.resource(), Types.action_group()) :: boolean()
  def verify_record(authorization, record, action) do
    authorization.permissions
    |> Permissions.granted?(action, record, authorization.subject)
  end

  defp add_predicate_name(atom),
    do: {(Atom.to_string(atom) <> "?") |> String.to_atom(), atom}
end