lib/runbox/runtime/output_action.ex

defmodule Runbox.Runtime.OutputAction do
  @moduledoc """
  Output Action for internal use by Runtime
  """
  alias Runbox.Runtime.OABody
  alias Runbox.Scenario.OutputAction, as: ScenarioOA
  alias __MODULE__, as: OA

  defstruct [:body, :timestamp, :scenario_id, :run_id]

  @typedoc "Runtime Output Action"
  @type t :: %__MODULE__{
          body: ScenarioOA.oa_params() | ScenarioOA.BadOutputAction.t(),
          timestamp: integer(),
          scenario_id: String.t(),
          run_id: String.t()
        }

  @doc """
  Creates a new output action.

  To produce a valid output action, the `params` have to be one of the accepted
  structs. See the documentation of the particular structs for details.

  If the `params` are not one of the accepted structs or some of the fields do
  not pass validation, it returns an output action with body set to
  `t:Runbox.Scenario.OutputAction.BadOutputAction.t/0` with original params
  inside.
  """
  @spec new(params :: ScenarioOA.oa_params(), integer(), String.t(), String.t()) :: t()
  def new(params, timestamp, scenario_id, run_id) do
    if is_oa_body?(params) and valid_body?(params) do
      %OA{body: params, timestamp: timestamp, scenario_id: scenario_id, run_id: run_id}
    else
      body = %ScenarioOA.BadOutputAction{params: params}
      %OA{body: body, timestamp: timestamp, scenario_id: scenario_id, run_id: run_id}
    end
  end

  @doc """
  Returns `true` if valid output action.
  """
  @spec valid?(OA.t() | any()) :: boolean()
  def valid?(%OA{body: body, timestamp: ts, scenario_id: sid, run_id: rid}) do
    is_oa_body?(body) and valid_body?(body) and is_integer(ts) and is_binary(sid) and is_binary(rid)
  end

  def valid?(_) do
    false
  end

  @doc """
  Returns `true` if valid output action body.
  """
  @spec is_oa_body?(ScenarioOA.oa_params() | any()) :: boolean()
  def is_oa_body?(params) do
    not is_nil(OABody.impl_for(params))
  end

  defp valid_body?(params) do
    OABody.valid?(params)
  end
end

defprotocol Runbox.Runtime.OABody do
  @moduledoc false
  @spec valid?(t) :: boolean()
  def valid?(body)
end

defimpl Runbox.Runtime.OABody, for: Runbox.Scenario.OutputAction.UpsertAssetAttributes do
  def valid?(%Runbox.Scenario.OutputAction.UpsertAssetAttributes{
        type: type,
        id: id,
        attributes: attributes
      }) do
    is_binary(type) and
      id_valid?(id) and
      is_map(attributes)
  end

  defp id_valid?(id), do: is_binary(id) and not String.contains?(id, "/")
end

defimpl Runbox.Runtime.OABody, for: Runbox.Scenario.OutputAction.DeleteAssetAttributes do
  def valid?(%Runbox.Scenario.OutputAction.DeleteAssetAttributes{
        type: type,
        id: id,
        attributes: attributes
      }) do
    is_binary(type) and
      is_binary(id) and
      is_map(attributes)
  end
end

defimpl Runbox.Runtime.OABody, for: Runbox.Scenario.OutputAction.DeleteAllAssetAttributes do
  def valid?(%Runbox.Scenario.OutputAction.DeleteAllAssetAttributes{
        type: type,
        id: id
      }) do
    is_binary(type) and is_binary(id)
  end
end

defimpl Runbox.Runtime.OABody, for: Runbox.Scenario.OutputAction.UpsertEdge do
  def valid?(%Runbox.Scenario.OutputAction.UpsertEdge{
        from_type: from_type,
        from_id: from_id,
        to_type: to_type,
        to_id: to_id,
        type: type
      }) do
    is_binary(from_type) and is_binary(from_id) and
      is_binary(to_type) and is_binary(to_id) and
      is_binary(type)
  end
end

defimpl Runbox.Runtime.OABody, for: Runbox.Scenario.OutputAction.DeleteEdge do
  def valid?(%Runbox.Scenario.OutputAction.DeleteEdge{
        from_type: from_type,
        from_id: from_id,
        to_type: to_type,
        to_id: to_id,
        type: type
      }) do
    is_binary(from_type) and is_binary(from_id) and
      is_binary(to_type) and is_binary(to_id) and
      is_binary(type)
  end
end

defimpl Runbox.Runtime.OABody, for: Runbox.Scenario.OutputAction.Notification do
  alias Runbox.Scenario.OutputAction.Notification

  def valid?(%Notification{
        type: type,
        primary_asset: primary_asset,
        ids: ids,
        aqls: aqls,
        priority: priority,
        metadata: metadata,
        direct_subscriptions: direct_subscriptions,
        attachments: attachments
      }) do
    type_valid?(type) and
      ids_valid?(ids) and
      is_map(aqls) and
      is_map(metadata) and
      priority_valid?(priority) and
      direct_subscriptions_valid?(direct_subscriptions) and
      primary_asset_valid?(primary_asset) and
      attachments_valid?(attachments)
  end

  defp type_valid?(type), do: is_binary(type) or (is_atom(type) and not is_nil(type))
  defp priority_valid?(priority), do: priority in [:low, :medium, :high, "low", "medium", "high"]
  defp ids_valid?(ids), do: is_list(ids) and Enum.all?(ids, &is_binary/1)
  defp direct_subscriptions_valid?(ds), do: is_nil(ds) or is_list(ds)
  defp primary_asset_valid?(pa), do: is_nil(pa) or is_binary(pa)
  defp attachments_valid?(at), do: is_nil(at) or is_list(at)
end

defimpl Runbox.Runtime.OABody, for: Runbox.Scenario.OutputAction.Event do
  alias Runbox.Scenario.OutputAction.Event

  def valid?(%Event{
        type: type,
        template: template,
        actors: actors,
        params: params,
        origin_messages: origin_messages
      }) do
    valid_type?(type) and
      is_binary(template) and
      is_map(actors) and
      is_map(params) and
      is_list(origin_messages)
  end

  defp valid_type?(type), do: is_binary(type) or (is_atom(type) and not is_nil(type))
end

defimpl Runbox.Runtime.OABody, for: Runbox.Scenario.OutputAction.ExecuteSQL do
  alias Runbox.Scenario.OutputAction.ExecuteSQL

  def valid?(%ExecuteSQL{
        db_connection: db_connection,
        sql_query: sql_query,
        data: data,
        type: type
      }) do
    is_map(db_connection) and
      is_binary(sql_query) and
      is_list(data) and
      type in [:postgresql, "postgresql"]
  end
end

defimpl Runbox.Runtime.OABody, for: Runbox.Scenario.OutputAction.Incident do
  alias Runbox.Scenario.OutputAction.Incident
  alias Runbox.Scenario.OutputAction.IncidentActor
  alias Runbox.Scenario.OutputAction.IncidentFuture
  alias Runbox.Scenario.OutputAction.IncidentHistory

  def valid?(%Incident{
        id: id,
        future: future,
        history: history,
        actors: actors
      }) do
    id_valid?(id) and
      future_valid?(future) and
      history_valid?(history) and
      actors_valid?(actors)
  end

  defp id_valid?(id), do: is_binary(id) and not String.contains?(id, "/")

  defp future_valid?(future),
    do: is_list(future) and Enum.all?(future, &match?(%IncidentFuture{}, &1))

  defp history_valid?(history),
    do: is_list(history) and Enum.all?(history, &match?(%IncidentHistory{}, &1))

  defp actors_valid?(nil), do: true

  defp actors_valid?(actors),
    do: is_list(actors) and Enum.all?(actors, &match?(%IncidentActor{}, &1))
end

defimpl Runbox.Runtime.OABody, for: Runbox.Scenario.OutputAction.IncidentPatch do
  alias Runbox.Scenario.OutputAction.IncidentPatch
  alias Runbox.Scenario.OutputAction.IncidentActor
  alias Runbox.Scenario.OutputAction.IncidentFuture
  alias Runbox.Scenario.OutputAction.IncidentHistory

  def valid?(%IncidentPatch{
        future: future,
        history: history,
        actors: actors
      }) do
    future_valid?(future) and
      history_valid?(history) and
      actors_valid?(actors[:add]) and
      actors_valid?(actors[:remove])
  end

  defp future_valid?(nil), do: true

  defp future_valid?(future),
    do: is_list(future) and Enum.all?(future, &match?(%IncidentFuture{}, &1))

  defp history_valid?(nil), do: true

  defp history_valid?(history),
    do: is_list(history) and Enum.all?(history, &match?(%IncidentHistory{}, &1))

  defp actors_valid?(nil), do: true

  defp actors_valid?(actors),
    do: is_list(actors) and Enum.all?(actors, &match?(%IncidentActor{}, &1))
end

defimpl Runbox.Runtime.OABody, for: Runbox.Scenario.OutputAction.BadOutputAction do
  def valid?(_), do: true
end