StatifierUI.Trace.Json (StatifierUI v0.3.0)

Copy Markdown View Source

The canonical JSON encoder that makes ADR-0005's byte-comparable golden traces real.

JSON.encode!/1 alone is not enough: Erlang map iteration order tracks insertion order only below the small-map threshold, so two runs of the same producer can build the same keys in different orders and get different bytes back. encode/1 walks the term itself instead - objects emit their keys in Enum.sort/1 (lexicographic) order at every nesting level, arrays emit in the order given (the producer has already put them in the order docs/wire-format.md requires), and every scalar and string is delegated to JSON.encode_to_iodata!/1 so escaping is the stdlib's problem, not this module's.

This encoder rejects nothing: a term outside StatifierUI.Trace.Message.json() is a producer bug that JSON.encode_to_iodata!/1 raises on, which is the right failure for an invariant StatifierUI.Trace.Normalizer is supposed to have already established.

Summary

Functions

Reads one JSON object - a single JSON Lines line - back into a %Message{}.

Reads a JSON Lines document back into a message list - the inverse of encode_lines/1.

Encodes a JSON-ready term to canonical iodata - object keys lexicographic at every level, arrays in the order given.

Renders a list of messages as JSON Lines: one encode_message/1 result per line, each terminated by "\n" - the shape the golden fixtures use.

Renders one %Message{} to a canonical JSON string - Message.to_map/1 followed by encode_to_string/1.

encode/1, flattened to a binary.

Functions

decode(line)

@spec decode(String.t()) :: {:ok, StatifierUI.Trace.Message.t()} | {:error, term()}

Reads one JSON object - a single JSON Lines line - back into a %Message{}.

The inverse of encode_message/1, and the function docs/ops-embedding.md tells a host to call over a persisted stream. Decoding is structural only: StatifierUI.Trace.Message.from_map/1 splits the envelope from the payload and leaves payload values in wire shape, so encode_message/1 over the result reproduces the input bytes exactly (encode/1 is canonical, so key order cannot drift).

A line that is not JSON is {:error, {:json, reason}}; valid JSON that is not a well-formed envelope is the tagged error StatifierUI.Trace.Message.from_map/1 returned.

decode_lines(document)

@spec decode_lines(String.t()) ::
  {:ok, [StatifierUI.Trace.Message.t()]}
  | {:error, {:line, pos_integer(), term()}}

Reads a JSON Lines document back into a message list - the inverse of encode_lines/1.

Blank lines are skipped, so the trailing newline encode_lines/1 writes round-trips cleanly and a hand-edited file with a stray blank line still loads. The first line that fails stops the read and is reported as {:error, {:line, number, reason}}, one-based, because "which line" is the first thing anyone debugging a truncated capture needs.

encode(map)

@spec encode(StatifierUI.Trace.Message.json()) :: iodata()

Encodes a JSON-ready term to canonical iodata - object keys lexicographic at every level, arrays in the order given.

encode_lines(messages)

@spec encode_lines([StatifierUI.Trace.Message.t()]) :: String.t()

Renders a list of messages as JSON Lines: one encode_message/1 result per line, each terminated by "\n" - the shape the golden fixtures use.

encode_message(message)

@spec encode_message(StatifierUI.Trace.Message.t()) :: String.t()

Renders one %Message{} to a canonical JSON string - Message.to_map/1 followed by encode_to_string/1.

encode_to_string(term)

@spec encode_to_string(StatifierUI.Trace.Message.json()) :: String.t()

encode/1, flattened to a binary.