277 @spec/@type annotations added to 58 files covering all public APIs: contexts (propagation, radio, weather, terrain, beacons, commercial), GRIB2 decoders, terrain analysis, duct detection, rain scatter, CSV/ADIF import, weather clients, and all Ecto schemas. Dialyzer passes with 0 errors.
66 lines
2.4 KiB
Elixir
66 lines
2.4 KiB
Elixir
defmodule Microwaveprop.Radio.EnrichmentStatus do
|
|
@moduledoc """
|
|
State machine for contact enrichment lifecycle.
|
|
|
|
States:
|
|
:pending → initial, no work started
|
|
:queued → Oban job inserted
|
|
:processing → worker actively running
|
|
:complete → data exists in DB
|
|
:failed → worker exhausted retries
|
|
:unavailable → data cannot be fetched (missing pos, archive gone, etc.)
|
|
|
|
Valid transitions:
|
|
pending → queued, unavailable
|
|
queued → processing, failed
|
|
processing → complete, failed
|
|
failed → queued (retry)
|
|
unavailable → queued (retry with new data source)
|
|
complete → pending (reset for re-enrichment)
|
|
"""
|
|
|
|
@type status :: :pending | :queued | :processing | :complete | :failed | :unavailable
|
|
@type field_name :: :hrrr_status | :weather_status | :terrain_status | :iemre_status
|
|
|
|
@states [:pending, :queued, :processing, :complete, :failed, :unavailable]
|
|
@fields [:hrrr_status, :weather_status, :terrain_status, :iemre_status]
|
|
|
|
@transitions %{
|
|
pending: [:queued, :unavailable],
|
|
queued: [:processing, :failed],
|
|
processing: [:complete, :failed],
|
|
failed: [:queued],
|
|
unavailable: [:queued],
|
|
complete: [:pending]
|
|
}
|
|
|
|
@spec states() :: [status()]
|
|
def states, do: @states
|
|
|
|
@spec fields() :: [field_name()]
|
|
def fields, do: @fields
|
|
|
|
@doc "Returns true if transitioning from `from` to `to` is valid."
|
|
@spec valid_transition?(status(), status()) :: boolean()
|
|
def valid_transition?(from, to) do
|
|
to in Map.get(@transitions, from, [])
|
|
end
|
|
|
|
@doc "Validates and applies a status transition on a changeset."
|
|
@spec transition(Ecto.Changeset.t(), field_name(), status()) :: Ecto.Changeset.t()
|
|
def transition(changeset, field, new_status) when field in @fields and new_status in @states do
|
|
current = Ecto.Changeset.get_field(changeset, field)
|
|
|
|
if valid_transition?(current, new_status) do
|
|
Ecto.Changeset.put_change(changeset, field, new_status)
|
|
else
|
|
Ecto.Changeset.add_error(changeset, field, "invalid transition from #{current} to #{new_status}")
|
|
end
|
|
end
|
|
|
|
@doc "Force-sets status without transition validation (for migrations, resets)."
|
|
@spec force_status(Ecto.Changeset.t(), field_name(), status()) :: Ecto.Changeset.t()
|
|
def force_status(changeset, field, status) when field in @fields and status in @states do
|
|
Ecto.Changeset.put_change(changeset, field, status)
|
|
end
|
|
end
|