LemonCore.Events (lemon_core v0.1.0)

View Source

The registry of typed LemonCore.Bus payloads — the platform's wire format.

Each entry maps a LemonCore.Event type atom to the module describing its payload. Publishing a registered type with a payload that is neither the right struct nor a map is a bug; LemonCore.Bus.broadcast_event/3 catches it in :dev and :test.

Full catalog, migration order and the versioning rules live in docs/platform/bus-events.md in the Lemon repository.

Scope

Only platform contract topics are registered — those whose payload crosses an app boundary or reaches the control plane and its clients: run:<id>, session:<key>, exec_approvals, cron, system, goals, routing_feedback.

Topics whose publisher and every subscriber live in one app (nodes, presence, run_graph:*, parent_question:*, and the sim/arena family) are deliberately absent. Their shapes are documented in their owning app; giving them core structs would push domain types back into lemon_core.

Compatibility

Every payload module accepts a legacy free-form map through from_map/1, and implements Access so consumers written against maps keep working for one deprecation cycle.

Summary

Functions

Coerce a payload to its registered struct, reporting why if it cannot be.

Coerce a payload to its registered struct, leaving unregistered types untouched.

Every payload module, published or nested.

Payload modules that are only ever nested inside another payload, never published alone.

The payload module for an event type, or nil if the type is not part of the contract.

Whether an event type is part of the typed platform contract.

Event type atom to payload module.

Functions

cast(type, payload)

@spec cast(atom(), term()) :: {:ok, term()} | {:error, String.t()}

Coerce a payload to its registered struct, reporting why if it cannot be.

The strict counterpart to coerce/2. Use it at trust boundaries — anywhere a payload arrives from outside the process that will publish it — so a malformed payload is refused with a reason rather than broadcast as-is. Unregistered types pass through unchanged: they have no declared shape to validate against.

iex> LemonCore.Events.cast(:secret_changed, %{"owner" => "u", "name" => "k", "action" => "put"})
{:ok, %LemonCore.Events.SecretChanged{owner: "u", name: "k", action: "put"}}

iex> {:error, reason} = LemonCore.Events.cast(:secret_changed, %{"owner" => "u"})
iex> reason =~ "SecretChanged"
true

coerce(type, payload)

@spec coerce(atom(), term()) :: term()

Coerce a payload to its registered struct, leaving unregistered types untouched.

Consumers use this to accept both the struct and the legacy map during the migration:

payload = LemonCore.Events.coerce(:run_completed, event.payload)

modules()

@spec modules() :: [module()]

Every payload module, published or nested.

nested_modules()

@spec nested_modules() :: [module()]

Payload modules that are only ever nested inside another payload, never published alone.

payload_module(type)

@spec payload_module(atom()) :: module() | nil

The payload module for an event type, or nil if the type is not part of the contract.

registered?(type)

@spec registered?(atom()) :: boolean()

Whether an event type is part of the typed platform contract.

registry()

@spec registry() :: %{required(atom()) => module()}

Event type atom to payload module.