lib/live_guard_protocols.ex

defprotocol LiveGuard.Allowed do
  @moduledoc ~S"""
  By this protocol you can implement `allowed?/4` functions.
  """

  alias Phoenix.LiveView
  alias LiveView.Socket

  @doc ~S"""
  By this function you can protect the LiveView lifecycle stages.

  You can pattern match by the **user**, **LiveView module**, **LiveView lifecycle stage** and **LiveView lifecycle stage inputs**.
  You can put this file anywhere but `/lib/my_app_web/live/abilities.ex` is recommended.

  **It must return a boolean.**

  ## Example

  ```elixir
  # /lib/my_app_web/live/abilities.ex

  defimpl LiveGuard.Allowed, for: User do
    @before_compile {LiveGuard, :before_compile_allowed}

    def allowed?(
          %User{role: role},
          MyModuleLive,
          :handle_event,
          {"delete_item", _unsigned_params, _socket}
        )
        when role in [:viewer, :customer],
        do: false

    # other `allowed?/4` functions...
  end
  ```
  > Note: As you can see, you don't have to define catch-all `allowed?/4` function because we used `@before_compile {LiveGuard, :before_compile_allowed}` hook. It returns `true`. This is optional.

  If the user is not authenticated you can add the following implementation as below:
  ```elixir
  defimpl LiveGuard.Allowed, for: Atom do
    @before_compile {LiveGuard, :before_compile_allowed}

    def allowed?(nil, MyModuleLive, :handle_event, {"delete_item", _unsigned_params, _socket}),
      do: false

    # other `allowed?/4` functions...
  end
  ```
  """
  @typedoc "A user struct or nil when the user is not authenticated."
  @type t() :: struct() | nil
  @spec allowed?(
          user :: struct() | nil,
          live_view_module :: module(),
          stage :: :mount,
          stage_inputs ::
            {params :: LiveView.unsigned_params() | :not_mounted_at_router, session :: map(),
             socket :: Socket.t()}
        ) :: boolean()
  @spec allowed?(
          user :: struct() | nil,
          live_view_module :: module(),
          stage :: :handle_params,
          stage_inputs ::
            {unsigned_params :: LiveView.unsigned_params(), uri :: String.t(),
             socket :: Socket.t()}
        ) :: boolean()
  @spec allowed?(
          user :: struct() | nil,
          live_view_module :: module(),
          stage :: :handle_event,
          stage_inputs ::
            {event :: binary(), unsigned_params :: LiveView.unsigned_params(),
             socket :: Socket.t()}
        ) :: boolean()
  @spec allowed?(
          user :: struct() | nil,
          live_view_module :: module(),
          stage :: :handle_info,
          stage_inputs :: {msg :: term(), socket :: Socket.t()}
        ) :: boolean()
  @spec allowed?(
          user :: struct() | nil,
          live_view_module :: module(),
          stage :: :handle_async,
          stage_inputs ::
            {name :: atom(), async_fun_result :: {:ok | :exit, term()}, socket :: Socket.t()}
        ) :: boolean()
  def allowed?(user, live_view_module, stage, stage_inputs)
end

defprotocol LiveGuard.GuardedStages do
  @moduledoc ~S"""
  #### _Optional_

  By this protocol you can implement `guarded_stages/1` functions.
  """

  @doc ~S"""
  #### _Optional_

  This function is for optimization.

  By default if you use the `on_mount/4` callback of LiveGuard, it will attach hooks to attachable LiveView lifecycle stages (`:handle_params`, `:handle_event`, `:handle_info` and `:handle_async`).

  If you need to protect for example only the `:handle_event` LiveView lifecycle stage for an individual LiveView module you can use this function.
  You can put this file anywhere but `/lib/my_app_web/live/guarded_stages.ex` is recommended.

  **It must return a list of [valid attachable LiveView lifecycle stages](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#attach_hook/4) (unless `:after_render`).**

  ## Example

  ```elixir
  # /lib/my_app_web/live/guarded_stages.ex

  defimpl LiveGuard.GuardedStages, for: Atom do
    @before_compile {LiveGuard, :before_compile_guarded_stages}

    def guarded_stages(MyModuleLive), do: [:handle_event]

    # other `guarded_stages?/1` functions...
  end
  ```
  In this case it will only attach hook to `:handle_event` LiveView lifecycle stage.
  > Note: As you can see, you don't have to define catch-all `guarded_stages/1` function because we used `@before_compile {LiveGuard, :before_compile_guarded_stages}` hook. It returns the attachable LiveView lifecycle stages (`:handle_params`, `:handle_event`, `:handle_info` and `:handle_async`). This is optional.
  """
  @typedoc "A LiveView module."
  @type t() :: module()
  @spec guarded_stages(live_view_module :: module()) :: [LiveGuard.attachable_lifecycle_stages()]
  def guarded_stages(live_view_module)
end