lib/glific/flows/exit.ex

defmodule Glific.Flows.Exit do
  @moduledoc """
  The Exit object which encapsulates one exit in a given node.
  """
  alias __MODULE__

  use Ecto.Schema

  alias Glific.{
    Flows,
    Flows.FlowContext,
    Flows.Node,
    Messages.Message,
    Metrics,
    Repo
  }

  @required_fields [:uuid]

  @type t() :: %__MODULE__{
          uuid: Ecto.UUID.t() | nil,
          node_uuid: Ecto.UUID.t() | nil,
          node: Node.t() | nil,
          destination_node_uuid: Ecto.UUID.t() | nil,
          destination_node: Node.t() | nil
        }

  embedded_schema do
    field :uuid, Ecto.UUID

    field :node_uuid, Ecto.UUID
    embeds_one :node, Node

    field :destination_node_uuid, Ecto.UUID
    embeds_one :destination_node, Node
  end

  @spec add_reverse(map(), Exit.t()) :: map()
  defp add_reverse(uuid_map, %{destination_node_uuid: nil}), do: uuid_map

  defp add_reverse(uuid_map, %{node_uuid: src, destination_node_uuid: dst}) do
    # we are only interested in one node leading to the destination, if there are multiple
    # we'll take the last update
    Map.put(
      uuid_map,
      {:reverse, dst},
      {:source, src}
    )
  end

  @doc """
  Process a json structure from floweditor to the Glific data types
  """
  @spec process(map(), map(), Node.t()) :: {Exit.t(), map()}
  def process(json, uuid_map, node) do
    Flows.check_required_fields(json, @required_fields)

    exit = %Exit{
      uuid: json["uuid"],
      node_uuid: node.uuid,
      destination_node_uuid: json["destination_uuid"]
    }

    uuid_map =
      uuid_map
      |> Map.put(exit.uuid, {:exit, exit})
      |> add_reverse(exit)

    {exit, uuid_map}
  end

  @doc """
  Validate a exit
  """
  @spec validate(Exit.t(), Keyword.t(), map()) :: Keyword.t()
  def validate(_exit, errors, _flow) do
    errors
  end

  @doc """
  Execute a exit, given a message stream.
  """
  @spec execute(atom() | Exit.t(), atom() | FlowContext.t(), [Message.t()]) ::
          {:ok, FlowContext.t() | nil, [Message.t()]} | {:error, String.t()}
  def execute(exit, context, messages) do
    context = Repo.preload(context, :flow)
    # update the flow count
    Metrics.bump(%{
      uuid: exit.uuid,
      destination_uuid: exit.destination_node_uuid,
      flow_uuid: context.flow_uuid,
      flow_id: context.flow.id,
      organization_id: context.organization_id,
      type: "exit",
      recent_message: get_recent_messages(context.recent_inbound)
    })

    if is_nil(exit.destination_node_uuid) do
      FlowContext.reset_context(context)
      {:ok, nil, []}
    else
      {:ok, {:node, node}} = Map.fetch(context.uuid_map, exit.destination_node_uuid)

      Node.execute(
        node,
        FlowContext.set_node(context, node),
        messages
      )
    end
  end

  # get most recent message
  @spec get_recent_messages(nil | list()) :: map()
  defp get_recent_messages(recent_inbound) when recent_inbound in [nil, []], do: %{}
  defp get_recent_messages(recent_inbound), do: hd(recent_inbound)
end