lib/runbox/runtime/output_action.ex

defmodule Runbox.Runtime.OutputAction do
  @moduledoc group: :internal
  @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 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
    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 oa_body?(ScenarioOA.oa_params() | any()) :: boolean()
  def oa_body?(params) do
    not is_nil(OABody.impl_for(params))
  end

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

  @doc """
  Returns `true` if provided attributes access tags are valid.
  """
  @spec attributes_access_tags_valid?(ScenarioOA.access_tags()) :: boolean()
  def attributes_access_tags_valid?(access_tags) do
    attributes_valid? =
      fn access_tags ->
        access_tags
        |> Map.keys()
        |> Enum.all?(fn attribute ->
          (is_list(attribute) and Enum.all?(attribute, &is_binary/1)) or
            attribute == :all or
            is_binary(attribute)
        end)
      end

    tag_definitions_valid? =
      fn access_tags ->
        access_tags
        |> Map.values()
        |> Enum.all?(&attribute_tag_definition_valid?/1)
      end

    is_map(access_tags) and attributes_valid?.(access_tags) and
      tag_definitions_valid?.(access_tags)
  end

  defp attribute_tag_definition_valid?(tag_def) do
    (is_list(tag_def) and Enum.all?(tag_def, &(is_binary(&1) and String.trim(&1) == &1))) or
      (is_binary(tag_def) and String.trim(tag_def) == tag_def)
  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,
        access_tags: access_tags
      }) do
    is_binary(type) and
      id_valid?(id) and
      is_map(attributes) and
      Runbox.Runtime.OutputAction.attributes_access_tags_valid?(access_tags)
  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,
        access_tags: access_tags
      }) do
    is_binary(type) and
      is_binary(id) and
      is_map(attributes) and
      Runbox.Runtime.OutputAction.attributes_access_tags_valid?(access_tags)
  end
end

defimpl Runbox.Runtime.OABody, for: Runbox.Scenario.OutputAction.DeleteAllAssetAttributes do
  def valid?(%Runbox.Scenario.OutputAction.DeleteAllAssetAttributes{
        type: type,
        id: id,
        access_tags: access_tags
      }) do
    is_binary(type) and is_binary(id) and
      Runbox.Runtime.OutputAction.attributes_access_tags_valid?(access_tags)
  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,
        access_tags: access_tags
      }) do
    is_binary(from_type) and is_binary(from_id) and
      is_binary(to_type) and is_binary(to_id) and
      is_binary(type) and is_list(access_tags) and
      Enum.all?(access_tags, &(is_binary(&1) and String.trim(&1) == &1))
  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,
        access_tags: access_tags
      }) do
    is_binary(from_type) and is_binary(from_id) and
      is_binary(to_type) and is_binary(to_id) and
      is_binary(type) and is_list(access_tags) and
      Enum.all?(access_tags, &(is_binary(&1) and String.trim(&1) == &1))
  end
end

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

  def valid?(%Notification{
        type: type,
        primary_actor: primary_actor,
        actors: actors,
        aqls: aqls,
        priority: priority,
        metadata: metadata,
        direct_subscriptions: direct_subscriptions,
        attachments: attachments,
        email_reply_to: reply_to
      }) do
    type_valid?(type) and
      actors_valid?(actors) and
      is_map(aqls) and
      is_map(metadata) and
      priority_valid?(priority) and
      direct_subscriptions_valid?(direct_subscriptions) and
      primary_actor_valid?(primary_actor) and
      attachments_valid?(attachments) and
      email_reply_to_valid?(reply_to)
  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 actors_valid?(actors), do: is_list(actors) and Enum.all?(actors, &ref_valid?/1)
  defp direct_subscriptions_valid?(ds), do: is_nil(ds) or is_list(ds)
  defp primary_actor_valid?(pa), do: is_nil(pa) or ref_valid?(pa)
  defp attachments_valid?(at), do: is_nil(at) or is_list(at)
  defp email_reply_to_valid?(rt), do: is_nil(rt) or is_map(rt)
  defp ref_valid?(%AssetActor{type: type, id: id}) when is_binary(type) and is_binary(id), do: true

  defp ref_valid?(%IncidentActor{type: type, id: id}) when is_binary(type) and is_binary(id),
    do: true

  defp ref_valid?(_), do: false
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,
        incident_actors: incident_actors,
        params: params,
        origin_messages: origin_messages
      }) do
    valid_type?(type) and
      is_binary(template) and
      is_map(actors) and
      (is_nil(incident_actors) or is_map(incident_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, &actor_valid?/1)

  defp actor_valid?(%IncidentActor{type: type, id: id}), do: is_binary(type) and is_binary(id)
  defp actor_valid?(_actor), do: false
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