// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A JSON tree under construction, for one OTLP request body.
///
/// The encoders write members in the order the protocol lists them and the tree
/// keeps that order, so a body reads like the specification's own field table and
/// two runs of the same program produce the same bytes. The text itself is written
/// by `moonjson`, which is what keeps this package from owning a second JSON
/// writer: escapes, number formatting and the strict grammar are the family
/// library's job.
///
/// The OpenTelemetry JSON mapping has rules a general-purpose encoder does not
/// follow — 64-bit integers are decimal strings, enum fields are integers, names
/// are lowerCamelCase — so those are decided here, member by member.
priv enum Container {
/// The values of an array, in the order they were written.
Listing(Array[Json])
/// The members of an object, in order, and the name whose value is due.
Members(Array[(String, Json)], String?)
}
///|
priv struct JsonWriter {
stack : Array[Container]
mut root : Json?
}
///|
fn JsonWriter::new() -> JsonWriter {
{ stack: [], root: None, }
}
///|
fn JsonWriter::text(self : JsonWriter) -> String {
let tree = match self.root {
Some(tree) => tree
None => Json::null()
}
@moonjson.dumps(tree)
}
///|
/// Put a finished value where the open container wants it.
fn JsonWriter::emit(self : JsonWriter, value : Json) -> Unit {
let depth = self.stack.length()
if depth == 0 {
self.root = Some(value)
return
}
let top = self.stack[depth - 1]
match top {
Listing(items) => items.push(value)
Members(members, waiting) =>
match waiting {
Some(name) => {
members.push((name, value))
self.stack[depth - 1] = Members(members, None)
}
// Every value inside an object is written after its name; a value with
// none would be a body no reader could parse, so it is a mistake here
// rather than on the wire.
None => abort("a JSON member was written without a name")
}
}
}
///|
fn JsonWriter::open_object(self : JsonWriter) -> Unit {
self.stack.push(Members([], None))
}
///|
fn JsonWriter::close_object(self : JsonWriter) -> Unit {
let top = self.pop()
match top {
Members(members, _) => self.emit(object_of(members))
Listing(_) => abort("an array was closed as an object")
}
}
///|
fn JsonWriter::open_array(self : JsonWriter) -> Unit {
self.stack.push(Listing([]))
}
///|
fn JsonWriter::close_array(self : JsonWriter) -> Unit {
let top = self.pop()
match top {
Listing(items) => self.emit(Json::array(items))
Members(_, _) => abort("an object was closed as an array")
}
}
///|
fn JsonWriter::pop(self : JsonWriter) -> Container {
match self.stack.pop() {
Some(top) => top
None => abort("a container was closed that was never opened")
}
}
///|
fn JsonWriter::name(self : JsonWriter, name : String) -> Unit {
let depth = self.stack.length()
if depth == 0 {
abort("a member name was written outside an object")
}
match self.stack[depth - 1] {
Members(members, _) => self.stack[depth - 1] = Members(members, Some(name))
Listing(_) => abort("a member name was written inside an array")
}
}
///|
fn JsonWriter::string(self : JsonWriter, value : String) -> Unit {
self.emit(Json::string(value))
}
///|
/// A number that is already text, such as an enum's integer.
fn JsonWriter::raw(self : JsonWriter, value : String) -> Unit {
self.emit(Json::number(0.0, repr=value))
}
///|
/// A 64-bit integer. Protobuf's JSON mapping writes one as a decimal string, and
/// every timestamp in OTLP is such a field: a nanosecond value does not fit the
/// 53 bits a JSON number survives.
fn JsonWriter::int64(self : JsonWriter, value : Int64) -> Unit {
self.emit(Json::string(value.to_string()))
}
///|
/// A double. A value JSON cannot carry — an infinity or a NaN — is written as
/// zero: a batch is worth more than the one point a broken producer contributed,
/// and a request with `Infinity` in it is refused by every receiver.
fn JsonWriter::double(self : JsonWriter, value : Double) -> Unit {
if value >= -1.7976931348623157e308 && value <= 1.7976931348623157e308 {
self.emit(Json::number(value))
} else {
self.emit(Json::number(0.0))
}
}
///|
fn JsonWriter::boolean(self : JsonWriter, value : Bool) -> Unit {
self.emit(Json::boolean(value))
}
///|
/// An object from its members, in the order they were written.
fn object_of(members : Array[(String, Json)]) -> Json {
let fields : Map[String, Json] = Map([])
for entry in members {
fields.set(entry.0, entry.1)
}
Json::object(fields)
}