lib/runbox/scenario/output_action.ex

defmodule Runbox.Scenario.OutputAction do
  @moduledoc """
  Output action represents a side effect produced by a run.

  ## Creating output actions

  Creating output action typically happens from scenario. It is done by
  creating one of the appropriate struct:

    * `t:Runbox.Scenario.OutputAction.UpsertAssetAttributes.t/0`
    * `t:Runbox.Scenario.OutputAction.DeleteAssetAttributes.t/0`
    * `t:Runbox.Scenario.OutputAction.DeleteAllAssetAttributes.t/0`
    * `t:Runbox.Scenario.OutputAction.UpsertEdge.t/0`
    * `t:Runbox.Scenario.OutputAction.DeleteEdge.t/0`
    * `t:Runbox.Scenario.OutputAction.Notification.t/0`
    * `t:Runbox.Scenario.OutputAction.Event.t/0`
    * `t:Runbox.Scenario.OutputAction.ExecuteSQL.t/0`

  For example:

      iex> %OutputAction.UpsertAssetAttributes{
      ...>   type: "/asset/camera",
      ...>   id: "one",
      ...>   attributes: %{"foo" => "bar"}
      ...> }

  ## Asset and edge output actions

  Changes done by output actions to assets and edges are scoped for each
  scenario run. The global result is computed from the changes done by
  individual runs.

  A single run is forbidden to do multiple changes to the same object (asset
  attribute or edge) at the same time. However distinct runs can perform such
  changes, because they are scoped to each run.

  The rules to compute the global state apply in the following order:

    1. Change with the higher timestamp has precedence.

    1. Updating an asset attribute or an edge has precedence to deleting it.

    1. Change with the higher value has precedence (applies only for asset attributes).
  """

  alias __MODULE__, as: OA

  @type asset_type :: String.t()
  @type asset_id :: String.t()
  @type edge_type :: String.t()

  defmodule UpsertAssetAttributes do
    @moduledoc """
    Parameters for output action Upsert Asset Attributes.

    The resulting output action will create or update attributes of the given
    asset.
    """

    @enforce_keys [:type, :id, :attributes]
    defstruct [:type, :id, :attributes]

    @typedoc """
    Upsert Asset Attributes

      * `:type` - asset type
      * `:id` - asset ID
      * `:attributes` - a map of attributes in format `%{"name" => value}` to be
        inserted or updated
    """
    @type t :: %__MODULE__{
            type: OA.asset_type(),
            id: OA.asset_id(),
            attributes: map()
          }
  end

  defmodule DeleteAssetAttributes do
    @moduledoc """
    Parameters for output action Delete Asset Attributes.

    The resulting output action will delete attributes of the given asset. This
    action succeeds even if the attributes do not exist.
    """

    @enforce_keys [:type, :id, :attributes]
    defstruct [:type, :id, :attributes]

    @typedoc """
    Delete Asset Attributes

      * `:type` - asset type
      * `:id` - asset ID
      * `:attributes` - a map of attributes to be deleted

    The leaf attributes of the `:attributes` map are deleted.
    E.g., if asset has the following attributes
    ```elixir
    %{
      "a": 1,
      "b": %{"c" => 2}
    }
    ```
    and the following `:attributes` are provided in the output action
    ```elixir
    %{
      "b": %{"c" => true}
    }
    ```
    then `b.c` attribute will be deleted, resulting in
    ```elixir
    %{
      "a": 1
      "b": %{}
    }
    ```

    """
    @type t :: %__MODULE__{
            type: OA.asset_type(),
            id: OA.asset_id(),
            attributes: map()
          }
  end

  defmodule DeleteAllAssetAttributes do
    @moduledoc """
    Parameters for output action Delete All Asset Attributes.

    The resulting output action will delete all existing attributes of the
    given asset. This is scoped to the attributes created by the same run.
    """

    @enforce_keys [:type, :id]
    defstruct [:type, :id]

    @typedoc """
    Delete All Asset Attributes

      * `:type` - asset type
      * `:id` - asset ID
    """
    @type t :: %__MODULE__{
            type: OA.asset_type(),
            id: OA.asset_id()
          }
  end

  defmodule UpsertEdge do
    @moduledoc """
    Parameters for output action Upsert Edge.

    The resulting output action will create an edge between two assets. The
    assets do not have to exist at the time of the edge creation.
    """

    @enforce_keys [:from_type, :from_id, :to_type, :to_id, :type]
    defstruct [:from_type, :from_id, :to_type, :to_id, :type]

    @typedoc """
    Upsert Edge

      * `:from_type` - type of the asset which this edge goes out from
      * `:from_id` - id of the asset which this edge goes out from
      * `:to_type` - type of the asset which this edge goes to
      * `:to_id` - id of the asset which this edge goes to
      * `:type` - type of the edge
    """
    @type t :: %__MODULE__{
            from_type: OA.asset_type(),
            from_id: OA.asset_id(),
            to_type: OA.asset_type(),
            to_id: OA.asset_id(),
            type: OA.edge_type()
          }
  end

  defmodule DeleteEdge do
    @moduledoc """
    Parameters for output action Delete Edge.

    The resulting output action will delete an edge between two assets. This
    action succeeds even if the edge does not exist.
    """

    @enforce_keys [:from_type, :from_id, :to_type, :to_id, :type]
    defstruct [:from_type, :from_id, :to_type, :to_id, :type]

    @typedoc """
    Delete Edge

      * `:from_type` - type of the asset which this edge goes out from
      * `:from_id` - id of the asset which this edge goes out from
      * `:to_type` - type of the asset which this edge goes to
      * `:to_id` - id of the asset which this edge goes to
      * `:type` - type of the edge
    """
    @type t :: %__MODULE__{
            from_type: OA.asset_type(),
            from_id: OA.asset_id(),
            to_type: OA.asset_type(),
            to_id: OA.asset_id(),
            type: OA.edge_type()
          }
  end

  defmodule Notification do
    @moduledoc """
    Parameters for output action Notification.

    The resulting output action sends a notification.
    """

    @enforce_keys [:type, :data]
    defstruct type: nil,
              data: nil,
              primary_asset: nil,
              ids: [],
              aqls: %{},
              priority: :medium,
              metadata: %{},
              direct_subscriptions: nil,
              attachments: nil

    @typedoc """
    Notification

      * `:type` - string, type of the notification
      * `:data` - map, data for the template evaluation
      * `:primary_asset` - string, optional, id of an asset that corresponds to this notification
      * `:ids` - list of asset ids, optional, to filter recipients, each recipient must have
        sufficient privileges to see all listed IDs
      * `:aqls` - map, AQLs which are evaluated, provide further context to the templates, and also can
        serve the same purpose as `ids` - filter recipients to only those who can see the whole
        result of the query. Key is the name of the AQL which can be used in the template. Value
        is a map with `aql` (the AQL query) and `primary` (whether the AQL should also be used
        to filter recipients) keys.
      * `:priority` - optional, defaults to `:medium`, can be `:low`, `:medium`, `:high`, only
        applicable to some channels
      * `:metadata` - map, optional, additional metadata which can be used in notification group
        subscriptions. Should adhere to the format specified in `notification/spec.exs`.
      * `:direct_subscriptions` - list of maps, subscriptions to be used additionally to the usual
        routing. Each map should have the following.
          * `:user` - map, user object to be used instead of Altworx object, should have
            `notification.email` and optionally even `notification.language`.
          * `:templates` - list of ids, which templates should be used
          * `:channels` - list of ids, which channels should be used
      * `:attachments` - list of maps, optional, attachments that should be sent with the notification.
        Each map has the following keys.
          * `:data` - binary, content of the attachment
          * `:filename` - string, name of the file
          * `:content_type` - string, optional, MIME type of the data
          * `:type` - whether the attachment is to inlined in the template (`:inline`) or not
            (`:attachment`, this is the default)
    """
    @type t :: %__MODULE__{
            type: String.t() | atom(),
            data: map(),
            primary_asset: String.t() | nil,
            ids: [String.t()],
            aqls: map(),
            priority: :low | :medium | :high | String.t(),
            metadata: map(),
            direct_subscriptions: [map()] | nil,
            attachments: [map()] | nil
          }
  end

  defmodule Event do
    @moduledoc """
    Parameters for output action Event.

    The resulting output action creates an event.
    """

    @enforce_keys [:type, :template, :actors]
    defstruct type: nil,
              template: nil,
              actors: nil,
              params: %{},
              origin_messages: []

    @typedoc """
    Event

      * `:type` - type of the event. It's declared in Scenario's Manifest.
      * `:template` - event's template. See `AssetMap.Events.Template`.
      * `:actors` - map of actors which are interpolated in the template.
      * `:params` - map of template params that are interpolated in the template.
      * `:origin_messages` - list of raw messages linked with the event itself.
    """
    @type t :: %__MODULE__{
            type: String.t() | atom(),
            template: String.t(),
            actors: map(),
            params: map(),
            origin_messages: list()
          }
  end

  defmodule ExecuteSQL do
    @moduledoc """
    Parameters for output action Execute SQL.

    The resulting output action executes an SQL statement.
    """

    @enforce_keys [:db_connection, :sql_query, :data, :type]
    defstruct db_connection: nil,
              sql_query: nil,
              data: nil,
              type: nil

    @typedoc """
    Execute SQL

      * `:db_connection` - map with database connection info (`hostname`, `username`, `password`, `database`)
      * `:sql_query` - SQL query.
      * `:data` - list of parameterized data.
      * `:type` - database type, right now only PostgreSQL is supported. It's checked in `OutputProcessor` and
        the value must be `:postgresql`.

    If `:run_id` is present in `data`, it's replaced with actual run_id in `AssetMap.Api.SqlExecutor`.
    """
    @type t :: %__MODULE__{
            db_connection: map(),
            sql_query: String.t(),
            data: list(),
            type: :postgresql | String.t()
          }
  end

  defmodule IncidentFuture do
    @moduledoc "Incident Future"
    @enforce_keys [
      :status,
      :severity,
      :timestamp,
      :description
    ]
    defstruct [
      :status,
      :severity,
      :timestamp,
      :description
    ]

    @type t :: %IncidentFuture{
            status: String.t(),
            severity: OA.Incident.severity(),
            timestamp: integer(),
            description: String.t()
          }
  end

  defmodule IncidentHistory do
    @moduledoc "Incident History"
    @enforce_keys [
      :status,
      :severity,
      :timestamp,
      :description
    ]
    defstruct [
      :status,
      :severity,
      :timestamp,
      :description,
      :attributes
    ]

    @type t :: %IncidentHistory{
            status: String.t(),
            severity: OA.Incident.severity(),
            timestamp: integer(),
            description: String.t(),
            attributes: map() | nil
          }
  end

  defmodule IncidentActor do
    @moduledoc "Incident Actor"
    @enforce_keys [
      :type,
      :id
    ]
    defstruct [
      :type,
      :id
    ]

    @type t :: %IncidentActor{
            type: String.t(),
            id: String.t()
          }
  end

  defmodule Incident do
    @moduledoc """
    Parameters for output action Incident.

    The resulting output action creates an incident.
    """
    @enforce_keys [
      :type,
      :id,
      :subject,
      :status,
      :resolved,
      :severity,
      :future,
      :history
    ]
    defstruct [
      :type,
      :id,
      :subject,
      :status,
      :resolved,
      :severity,
      :future,
      :history,
      :actors,
      :user_actions,
      :additional_attributes
    ]

    @type severity :: 1..4

    @typedoc """
    Incident
    """
    @type t :: %Incident{
            type: String.t(),
            id: String.t(),
            subject: String.t(),
            status: String.t(),
            resolved: boolean(),
            severity: severity(),
            future: [IncidentFuture.t()],
            history: [IncidentHistory.t()],
            actors: [IncidentActor.t()] | nil,
            user_actions: map() | nil,
            additional_attributes: map() | nil
          }
  end

  defmodule IncidentPatch do
    @moduledoc """
    Parameters for output action Incident Patch.

    The resulting output action updates an incident.
    """
    @enforce_keys [
      :type,
      :id
    ]
    defstruct [
      :type,
      :id,
      :subject,
      :status,
      :resolved,
      :severity,
      :future,
      :history,
      :actors,
      :user_actions,
      :additional_attributes
    ]

    @typedoc """
    Incident Patch
    """
    @type t :: %IncidentPatch{
            type: String.t(),
            id: String.t(),
            subject: String.t() | nil,
            status: String.t() | nil,
            resolved: boolean() | nil,
            severity: OA.Incident.severity() | nil,
            future: [IncidentFuture.t()] | nil,
            history: [IncidentHistory.t()] | nil,
            actors:
              %{
                optional(:add) => [IncidentActor.t()],
                optional(:remove) => [IncidentActor.t()]
              }
              | nil,
            user_actions:
              %{
                optional(:upsert) => map(),
                optional(:remove) => list()
              }
              | nil,
            additional_attributes:
              %{
                optional(:upsert) => map(),
                optional(:remove) => map()
              }
              | nil
          }
  end

  defmodule BadOutputAction do
    @moduledoc """
    Body of invalid output action.
    """

    @enforce_keys [:params]
    defstruct [:params]

    @typedoc """
    Bad Output Action

      * `:params` - holds the original parameters
    """
    @type t :: %__MODULE__{
            params: any()
          }
  end

  @type oa_params ::
          UpsertAssetAttributes.t()
          | DeleteAssetAttributes.t()
          | DeleteAllAssetAttributes.t()
          | UpsertEdge.t()
          | DeleteEdge.t()
          | Notification.t()
          | Event.t()
          | ExecuteSQL.t()
          | Incident.t()
          | IncidentPatch.t()

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

  @typedoc """
  Output Action

  This struct is produced by `Runbox.Runtime.Stage.Sandbox.execute_run/3`.
  """
  @type t :: %__MODULE__{
          body: oa_params() | BadOutputAction.t(),
          timestamp: integer()
        }
end