A CloudEvents v1.0 envelope — the ecosystem's uniform event wrapper. It carries
any domain body (a Mimir.Event wire map, a routing-decision record, ...) in
data, with the CloudEvents core context attributes as siblings.
Body-agnostic: this layer never decodes data into a typed struct. A consumer
that wants the body calls the body's own parser on data, keyed by type.
data is therefore any JSON value, not just an object — CloudEvents permits
an array, string, number, or boolean body, and the envelope carries whatever it
is verbatim rather than coercing it. A non-JSON (binary) body travels
base64-encoded in data_base64 instead, per the JSON event format; the two are
mutually exclusive and decoding data_base64 is the consumer's job.
Mimir.Event is unchanged by this module — it becomes the data of a
CloudEvent, not a CloudEvent itself (see from_event/2). The id/source/
time a CloudEvent needs but Mimir.Event lacks are supplied by the producer
that wraps it (the gateway stamps a wall-clock time, a source URI, a
natural-key id); this module invents none of them, it validates their shape.
Construction (new/1, from_event/2) is strict; from_wire/1 is tolerant —
the same posture as Mimir.Event: we only ever write well-formed events, and a
reader never rejects an otherwise-valid event over an optional-field hint.
Summary
Types
data is the opaque body — any JSON value, carried verbatim and never
interpreted here (the documented open-payload carve-out). data_base64 holds a
base64-encoded binary body instead; at most one of the two is ever set.
Functions
Wrap a lifecycle Mimir.Event as a CloudEvent. Sets type from the event's
domain/type via Mimir.CloudEvent.Types.for_event/1 and data from
Mimir.Event.to_wire/1; the producer supplies :id, :source (required) and
optionally :time, :subject, :dataschema, :extensions — this function
invents none of them. Same validation and return contract as new/1.
Parse a CloudEvents JSON event-format map. Fallible and tolerant: specversion,
id, source, type must be present non-empty strings (else
{:error, {:bad_cloudevent, {:missing, key}}}), and specversion must be
"1.0" (else {:error, {:bad_cloudevent, {:unsupported_specversion, v}}}).
time/subject/dataschema are tolerant (absent or non-string → nil); the body
is carried verbatim — data is any JSON value, data_base64 any string. Every
unrecognized top-level string key is preserved as an extension attribute rather
than discarded, so context this release does not model by name survives a
parse/render trip. Never raises.
Build a CloudEvent from attrs. :id, :source, :type are required
non-empty strings; :time (when present) must be RFC3339; :subject,
:data, and :data_base64 are optional. :data may be any JSON value and is
stored verbatim; supplying both :data and :data_base64 is an error, since
the JSON event format allows only one body. :dataschema (when present) must be
a non-empty string, and :extensions a string-keyed map whose keys do not
shadow a CloudEvents attribute this struct already models.
Render to the CloudEvents JSON event format: a string-keyed map with the
context attributes and the body as top-level keys. time/subject/dataschema
are omitted when nil; the body is rendered as data_base64 when one is set and
as data otherwise, never both. Extension attributes are merged back in at the
top level, where CloudEvents puts them. Always succeeds.
Timestamp shape check: a string DateTime.from_iso8601/1 accepts, which
requires a UTC offset and so rejects a naive local time.
Types
@type t() :: %Mimir.CloudEvent{ data: term(), data_base64: String.t() | nil, datacontenttype: String.t(), dataschema: String.t() | nil, extensions: %{optional(String.t()) => term()}, id: String.t(), source: String.t(), specversion: String.t(), subject: String.t() | nil, time: String.t() | nil, type: String.t() }
data is the opaque body — any JSON value, carried verbatim and never
interpreted here (the documented open-payload carve-out). data_base64 holds a
base64-encoded binary body instead; at most one of the two is ever set.
extensions is the open bag of CloudEvents extension attributes (the other
documented carve-out): string-keyed, carried verbatim in both directions, never
interpreted. It is how context this release does not model by name —
traceparent/tracestate for distributed tracing, partitionkey, a
broker-specific attribute — survives a parse/render trip intact.
Functions
@spec from_event(Mimir.Event.t(), map() | keyword()) :: {:ok, t()} | {:error, {:bad_cloudevent, term()}}
Wrap a lifecycle Mimir.Event as a CloudEvent. Sets type from the event's
domain/type via Mimir.CloudEvent.Types.for_event/1 and data from
Mimir.Event.to_wire/1; the producer supplies :id, :source (required) and
optionally :time, :subject, :dataschema, :extensions — this function
invents none of them. Same validation and return contract as new/1.
Parse a CloudEvents JSON event-format map. Fallible and tolerant: specversion,
id, source, type must be present non-empty strings (else
{:error, {:bad_cloudevent, {:missing, key}}}), and specversion must be
"1.0" (else {:error, {:bad_cloudevent, {:unsupported_specversion, v}}}).
time/subject/dataschema are tolerant (absent or non-string → nil); the body
is carried verbatim — data is any JSON value, data_base64 any string. Every
unrecognized top-level string key is preserved as an extension attribute rather
than discarded, so context this release does not model by name survives a
parse/render trip. Never raises.
Build a CloudEvent from attrs. :id, :source, :type are required
non-empty strings; :time (when present) must be RFC3339; :subject,
:data, and :data_base64 are optional. :data may be any JSON value and is
stored verbatim; supplying both :data and :data_base64 is an error, since
the JSON event format allows only one body. :dataschema (when present) must be
a non-empty string, and :extensions a string-keyed map whose keys do not
shadow a CloudEvents attribute this struct already models.
:datacontenttype defaults to "application/json" but is honored when given —
a producer labelling a non-JSON body gets the label it asked for. :specversion
may be supplied only as "1.0"; any other value is rejected rather than
silently replaced with the constant. Returns
{:ok, t()} | {:error, {:bad_cloudevent, reason}}.
Render to the CloudEvents JSON event format: a string-keyed map with the
context attributes and the body as top-level keys. time/subject/dataschema
are omitted when nil; the body is rendered as data_base64 when one is set and
as data otherwise, never both. Extension attributes are merged back in at the
top level, where CloudEvents puts them. Always succeeds.
Timestamp shape check: a string DateTime.from_iso8601/1 accepts, which
requires a UTC offset and so rejects a naive local time.
That is approximately RFC3339, not exactly it, and new/1 hard-rejects on
this — so the divergences are worth stating rather than implying. It rejects
three forms RFC3339 allows: the unknown-local-offset -00:00 (§4.3), a
lowercase t/z separator (§5.6), and a leap second such as
"2026-12-31T23:59:60Z" (§5.8). It accepts one form the §5.6 grammar does not:
a space in place of T. In practice every mainstream producer emits an
uppercase Z or a numeric offset and is unaffected; a producer that does not
should normalize before construction.