Mimir.CloudEvent (Mimir v0.5.0)

Copy Markdown View Source

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

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.

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

t()

@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

from_event(event, opts)

@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.

from_wire(wire)

@spec from_wire(term()) :: {:ok, t()} | {:error, {:bad_cloudevent, term()}}

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.

new(attrs)

@spec new(map() | keyword()) :: {:ok, t()} | {:error, {:bad_cloudevent, term()}}

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}}.

to_wire(ce)

@spec to_wire(t()) :: map()

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.

valid_time?(t)

@spec valid_time?(term()) :: boolean()

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.