defmodule Post do defstruct name: "john" end defmodule Terminator.UUID.Performer do @moduledoc """ Performer is a main actor for determining abilities """ use Terminator.UUID.Schema import Ecto.Changeset import Ecto.Query alias __MODULE__ alias Terminator.UUID.Ability alias Terminator.UUID.Repo alias Terminator.UUID.Role alias Terminator.UUID.PerformersEntities alias Terminator.UUID.PerformersRoles @typedoc "A performer struct" @type t :: %Performer{} schema "terminator_uuid_performers" do field(:abilities, {:array, :string}, default: []) many_to_many(:roles, Role, join_through: PerformersRoles) has_many(:entities, PerformersEntities) timestamps() end def changeset(%Performer{} = struct, params \\ %{}) do struct |> cast(params, []) end @doc """ Grant given grant type to a performer. ## Examples Function accepts either `Terminator.UUID.Ability` or `Terminator.UUID.Role` grants. Function is merging existing grants with the new ones, so calling grant with same grants will not duplicate entries in table. To grant particular ability to a given performer iex> Terminator.UUID.Performer.grant(%Terminator.UUID.Performer{id: 1}, %Terminator.UUID.Ability{id: 1}) To grant particular role to a given performer iex> Terminator.UUID.Performer.grant(%Terminator.UUID.Performer{id: 1}, %Terminator.UUID.Role{id: 1}) """ @spec grant(Performer.t(), Ability.t() | Role.t()) :: Performer.t() def grant(%Performer{id: id} = _performer, %Role{id: _id} = role) do # Preload performer roles performer = Performer |> Repo.get!(id) |> Repo.preload([:roles]) roles = merge_uniq_grants(performer.roles ++ [role]) changeset = changeset(performer) |> put_assoc(:roles, roles) changeset |> Repo.update!() end def grant(%{performer: %Performer{id: _pid} = performer}, %Role{id: _id} = role) do grant(performer, role) end def grant(%{performer_id: id}, %Role{id: _id} = role) do performer = Performer |> Repo.get!(id) grant(performer, role) end def grant(%Performer{id: id} = _performer, %Ability{id: _id} = ability) do performer = Performer |> Repo.get!(id) abilities = Enum.uniq(performer.abilities ++ [ability.identifier]) changeset = changeset(performer) |> put_change(:abilities, abilities) changeset |> Repo.update!() end def grant(%{performer: %Performer{id: id}}, %Ability{id: _id} = ability) do performer = Performer |> Repo.get!(id) grant(performer, ability) end def grant(%{performer_id: id}, %Ability{id: _id} = ability) do performer = Performer |> Repo.get!(id) grant(performer, ability) end def grant(_, _), do: raise(ArgumentError, message: "Bad arguments for giving grant") def grant( %Performer{id: _pid} = performer, %Ability{id: _aid} = ability, %{__struct__: _entity_name, id: _entity_id} = entity ) do entity_abilities = load_performer_entities(performer, entity) case entity_abilities do nil -> PerformersEntities.create(performer, entity, [ability.identifier]) entity -> abilities = Enum.uniq(entity.abilities ++ [ability.identifier]) PerformersEntities.changeset(entity) |> put_change(:abilities, abilities) |> Repo.update!() end performer end def grant( %{performer_id: id}, %Ability{id: _id} = ability, %{__struct__: _entity_name, id: _entity_id} = entity ) do grant(%Performer{id: id}, ability, entity) end def grant( %{performer: %Performer{id: _pid} = performer}, %Ability{id: _id} = ability, %{__struct__: _entity_name, id: _entity_id} = entity ) do grant(performer, ability, entity) end def grant(_, _, _), do: raise(ArgumentError, message: "Bad arguments for giving grant") @doc """ Revoke given grant type from a performer. ## Examples Function accepts either `Terminator.UUID.Ability` or `Terminator.UUID.Role` grants. Function is directly opposite of `Terminator.UUID.Performer.grant/2` To revoke particular ability from a given performer iex> Terminator.UUID.Performer.revoke(%Terminator.UUID.Performer{id: 1}, %Terminator.UUID.Ability{id: 1}) To revoke particular role from a given performer iex> Terminator.UUID.Performer.revoke(%Terminator.UUID.Performer{id: 1}, %Terminator.UUID.Role{id: 1}) """ @spec revoke(Performer.t(), Ability.t() | Role.t()) :: Performer.t() def revoke(%Performer{id: id} = _performer, %Role{id: _id} = role) do from(pa in PerformersRoles) |> where([pr], pr.performer_id == ^id and pr.role_id == ^role.id) |> Repo.delete_all() end def revoke(%{performer: %Performer{id: _pid} = performer}, %Role{id: _id} = role) do revoke(performer, role) end def revoke(%{performer_id: id}, %Role{id: _id} = role) do revoke(%Performer{id: id}, role) end def revoke(%Performer{id: id} = _performer, %Ability{id: _id} = ability) do performer = Performer |> Repo.get!(id) abilities = Enum.filter(performer.abilities, fn grant -> grant != ability.identifier end) changeset = changeset(performer) |> put_change(:abilities, abilities) changeset |> Repo.update!() end def revoke( %{performer: %Performer{id: _pid} = performer}, %Ability{id: _id} = ability ) do revoke(performer, ability) end def revoke(%{performer_id: id}, %Ability{id: _id} = ability) do revoke(%Performer{id: id}, ability) end def revoke(_, _), do: raise(ArgumentError, message: "Bad arguments for revoking grant") def revoke( %Performer{id: _pid} = performer, %Ability{id: _id} = ability, %{__struct__: _entity_name, id: _entity_id} = entity ) do entity_abilities = load_performer_entities(performer, entity) case entity_abilities do nil -> performer entity -> abilities = Enum.filter(entity.abilities, fn grant -> grant != ability.identifier end) if length(abilities) == 0 do entity |> Repo.delete!() else PerformersEntities.changeset(entity) |> put_change(:abilities, abilities) |> Repo.update!() end performer end end def revoke( %{performer_id: id}, %Ability{id: _id} = ability, %{__struct__: _entity_name, id: _entity_id} = entity ) do revoke(%Performer{id: id}, ability, entity) end def revoke( %{performer: %Performer{id: _pid} = performer}, %Ability{id: _id} = ability, %{__struct__: _entity_name, id: _entity_id} = entity ) do revoke(performer, ability, entity) end def revoke(_, _, _), do: raise(ArgumentError, message: "Bad arguments for revoking grant") def load_performer_entities(performer, %{__struct__: entity_name, id: entity_id}) do PerformersEntities |> where( [e], e.performer_id == ^performer.id and e.assoc_id == ^entity_id and e.assoc_type == ^PerformersEntities.normalize_struct_name(entity_name) ) |> Repo.one() end def table, do: :terminator_uuid_performers defp merge_uniq_grants(grants) do Enum.uniq_by(grants, fn grant -> grant.identifier end) end end