lib/membership/role.ex

defmodule Membership.Role do
  @moduledoc """
  Role is main representation of feature flags assigned to a role
  """

  use Membership.Schema
  import Ecto.Query

  alias Membership.Role
  alias Membership.Role.Server
  alias Membership.Feature
  alias Membership.RoleFeatures

  @typedoc "A Role struct"
  @type t :: %Role{}

  schema "membership_roles" do
    field(:identifier, :string)
    field(:name, :string)

    many_to_many(:features, Feature,
      join_through: RoleFeatures,
      on_replace: :delete
    )
  end

  def changeset(%Role{} = struct, params \\ %{}) do
    struct
    |> cast(params, [:identifier, :name])
    |> cast_assoc(:features, required: false)
    |> validate_required([:identifier, :name])
    |> unique_constraint(:identifier, message: "Role already exists")
  end

  def build(identifier, name) do
    changeset(%Role{}, %{
      identifier: identifier,
      name: name
    })
    |> Ecto.Changeset.apply_changes()
  end

  def create(identifier, name, features \\ []) do
    role =
      changeset(%Role{}, %{
        identifier: identifier,
        name: name
      })
      |> Repo.insert_or_update()

    Enum.map(features, fn f ->
      Feature.create(f.identifier, f.name)
      |> Feature.grant(role)
    end)
  end

  def table, do: :membership_roles

  @doc """
  Grant given grant type to a feature.

  ## Examples

  Function accepts either `Membership.Role` or `Membership.Feature` 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 feature to a given role

      iex> Membership.Role.grant(%Membership.Feature{id: 1}, %Membership.Role{id: 1})

  To grant particular feature to a given role

      iex> Membership.Role.grant(%Membership.Role{id: 1}, %Membership.Feature{id: 1})

  """

  @spec grant(Role.t(), Role.t() | Feature.t()) :: Member.t()
  def grant(%Role{id: id} = _role, %Feature{id: feature_id} = _feature) do
    # Preload Role features
    role = Role |> Repo.get!(id)
    feature = Feature |> Repo.get!(feature_id)

    revoke(feature, role)

    %RoleFeatures{role_id: role.id, feature_id: feature.id}
    |> Repo.insert()

    Server.reload()
  end

  def grant(%{role: %Role{id: _pid} = role}, %Feature{id: _id} = feature) do
    grant(role, feature)
  end

  def grant(%{role_id: id}, %Feature{id: _id} = feature) do
    Role
    |> Repo.get!(id)
    |> grant(feature)
  end

  def grant(%Feature{id: feature_id} = _feature, %Role{id: id} = _role) do
    role = Role |> Repo.get!(id)
    feature = Feature |> Repo.get!(feature_id)

    revoke(feature, role)

    %RoleFeatures{role_id: role.id, feature_id: feature.id}
    |> Repo.insert()

    Server.reload()
  end

  def grant(%{feature: feature}, %Role{id: _id} = role) do
    grant(role, feature)
  end

  def grant(%{feature_id: id}, %Role{id: _id} = role) do
    grant(role, %Feature{id: id})
  end

  def grant(_, _), do: raise(ArgumentError, message: "Bad arguments for giving grant")

  def grant(_, _, _), do: raise(ArgumentError, message: "Bad arguments for giving grant")

  @doc """
  Revoke given grant type from a member.

  ## Examples

  Function accepts either `Membership.Role` or `Membership.Feature` grants.
  Function is directly opposite of `Membership.Member.grant/2`

  To revoke particular feature from a given plan

      iex> Membership.Role.revoke(%Membership.Feature{id: 1}, %Membership.Role{id: 1})

  To revoke particular plan from a given feature

      iex> Membership.Role.revoke(%Membership.Role{id: 1}, %Membership.Feature{id: 1})

  """
  @spec revoke(Role.t(), Role.t() | Feature.t()) :: Member.t()
  def revoke(%Role{id: id} = _, %Feature{id: _id} = feature) do
    from(pa in RoleFeatures)
    |> where([pr], pr.role_id == ^id and pr.feature_id == ^feature.id)
    |> Repo.delete_all()
  end

  def revoke(%{role: %Role{id: _pid} = role}, %Feature{id: _id} = feature) do
    revoke(role, feature)
  end

  def revoke(%{feature_id: id}, %Feature{id: _id} = feature) do
    revoke(%Role{id: id}, feature)
  end

  def revoke(%Feature{id: id} = _, %Role{id: _id} = role) do
    from(pa in RoleFeatures)
    |> where([pr], pr.feature_id == ^id and pr.role_id == ^role.id)
    |> Repo.delete_all()
  end

  def revoke(
        %{feature: %Feature{id: _pid} = feature},
        %Role{id: _id} = role
      ) do
    revoke(role, feature)
  end

  def revoke(%{feature_id: id}, %Role{id: _id} = role) do
    revoke(%Feature{id: id}, role)
  end

  def revoke(_, _), do: raise(ArgumentError, message: "Bad arguments for revoking grant")

  def revoke(_, _, _), do: raise(ArgumentError, message: "Bad arguments for revoking grant")

  def load_role_feature(role, %{
        __struct__: _feature_name,
        id: feature_id,
        identifier: _identifier
      }) do
    FeatureRoles
    |> where(
      [e],
      e.role_id == ^role.id and e.feature_id == ^feature_id
    )
    |> Repo.one()
  end

  def all() do
    Repo.all(Membership.Role)
    |> Repo.preload(:features)
    |> Enum.map(fn x ->
      features = Enum.map(x.features, fn f -> f.identifier end)
      {x.identifier, features}
    end)
  end
end