defmodule Mobus.Stepwise.Components.StepwiseProjection do
@moduledoc """
Projection component for stepwise workflows.
Produces canonical `Mobus.Stepwise.Projection` from the current runtime state.
"""
alias Mobus.Stepwise.Projection
alias Mobus.Stepwise.ProjectionHelpers
alias Mobus.Stepwise.SpecHelpers
@doc """
Builds the canonical `Mobus.Stepwise.Projection` from the current runtime.
This is an ALF pipeline stage. Computes the projection struct containing
all information needed for UI rendering: current state, available events,
UI descriptor, artifacts, blocked reasons, subscriptions, errors, and trace.
Available events are derived from the step's position in the ordered step list:
- First step: `[:next]`
- Last step: `[:back]`
- Middle steps: `[:back, :next]`
## Parameters
* `event` — pipeline event map with `:spec` and `:runtime`
* `opts` — ALF stage options (unused)
## Returns
* Updated event map with `%Mobus.Stepwise.Projection{}` in both
`event.runtime.projection` and `event.projection`.
"""
@spec call(map(), map()) :: map()
def call(%{spec: spec, runtime: runtime} = event, _opts) do
current = Map.get(runtime, :current_state)
ordered = SpecHelpers.ordered_steps(spec)
projection = %Projection{
execution_id: Map.fetch!(runtime, :execution_id),
profile: :stepwise,
current_state: current,
available_events: available_events(ordered, current),
blocked_reasons: Map.get(runtime, :blocked_reasons, %{}),
breakpoint_hits: Map.get(runtime, :breakpoint_hits, []),
subscriptions: ProjectionHelpers.subscriptions_for(spec, runtime),
artifacts: Map.get(runtime, :artifacts, %{}),
ui: ProjectionHelpers.ui_for(spec, current, runtime),
errors: Map.get(runtime, :errors, []),
trace: Map.get(runtime, :trace, []),
extensions: ProjectionHelpers.build_extensions(spec, runtime)
}
runtime = Map.put(runtime, :projection, projection)
event |> Map.put(:runtime, runtime) |> Map.put(:projection, projection)
end
defp available_events(ordered, current) when is_list(ordered) do
idx = Enum.find_index(ordered, &SpecHelpers.equivalent_state?(&1, current))
cond do
is_nil(idx) ->
[]
idx == 0 and idx + 1 < length(ordered) ->
[:next]
idx + 1 >= length(ordered) and idx - 1 >= 0 ->
[:back]
idx + 1 < length(ordered) and idx - 1 >= 0 ->
[:back, :next]
true ->
[]
end
end
defp available_events(_ordered, _current), do: []
end