kanonak-codec (Rust)
The generic, ontology-independent Kanonak codec runtime — the Rust port of
@kanonak-protocol/codec. Given a CodecSchema (the per-package metadata a generated
typed SDK embeds) and a set of typed nodes, it builds the canonical input model
and content-addresses it via kanonak-canonical,
producing the exact content hash the kanonak hash CLI emits. It also
(de)serializes the normalized-JSON wire form.
A node is a plain JSON object (serde_json::Map<String, serde_json::Value>):
the $-envelope ($type, $id, optional $extra, …) plus alias-collapsed
local-name fields. A generated typed model serializes to one.
API
use ;
// nodes: &[Node], schema/pkg: &serde_json::Value
let hash = content_hash?; // "sha256:..."
let form = canonical_form?; // {"subjects":[...]}
let wire = serialize?; // normalized-JSON Node, numbers in their JSON forms
let node = deserialize?; // typed Node (unmodeled -> $extra)
let is_app = type_matches?; // bool
let member = enum_member; // Option<EnumMemberMatch>
-
build_package— the canonical input model (subject per node + synthesized package-wrapper subject). The seam every other entrypoint shares. -
content_hash/canonical_form— delegate tokanonak-canonical. -
serialize/deserialize— the normalized-JSON wire form.serializewrites modeled fields (nulls dropped) then$extraspread as siblings (modeled wins a collision; no$extrakey on the wire);deserializekeeps$-keys and$type-modeled fields top-level and collects everything else into$extrafor lossless round-trip. From 3.0.0, each integer and decimal value travels in its JSON form (kanonak-protocol 4.11.0'snumbers-in-json, runtime#54), through lists and embedded values. In the node,integer, its four sign-restricted forms anddecimalare a string of their canonical digits, since aserde_json::Valueholds no integer past 64 bits;long,unsignedLongandintand narrower are numbers. An embedded value whose class the schema doesn't hold is carried as given.deserializejudges theValues it's given: without serde_json'sarbitrary_precision, a number such as1.0000000000000000001is already thef641.0, so a caller that must refuse it parses with that feature. A JSON number past ±(2^53 − 1) for a wide integer, a number for a decimal, a string for a narrow integer, or a value outside its datatype's range is refused, ending[json-number-form], ascodec-vectors-json-numbers.jsonpins in every port. -
The serde binding (3.0.0), for a generated struct's fields, so serde_json reads and writes them as
serializeanddeserializedo:Integer<D>, anintegeror one of its sign-restricted forms held as its canonical digits, whereDisXsdInteger,NonNegativeInteger,PositiveInteger,NonPositiveIntegerorNegativeInteger, whose range it checks;LongandUnsignedLong, overi64andu64, written as strings;Decimal, held as its canonical digits and read from a string only.
intand narrower takei32and the rest, which serde_json already refuses a string, a fraction or overflow for. Each refusal of these types ends[json-number-form]. -
type_matches— whether a node (any member of$types, else its$type) is an instance of a class: the class itself, or a subclass whose schema entry lists it in the optionalancestorsfield (every superclass, transitively, by versioned URI; canonicalization-inert). Classes compare by versionless identity; a type the schema cannot read never matches. A typed instance asks the same throughKanonakNode::type_matcheson its envelope. -
enum_member— the enumeration member a{"$ref": ...}names, as anEnumMemberMatch { enum_type, uri, member }, orNone("not this schema's").
Reading earlier compatible versions (runtime#28). A node typed at an
earlier compatible version of the schema's own package still reads: the exact
versioned class first, else the same class at the highest version for which
kanonak_canonical::is_readable_by(written, schema_version) holds.
serialize, deserialize, type_matches and enum_member share that one lookup, and the
decoded node keeps the $type it was written with. Rejections end in a
bracketed kind — [unknown-type], [newer-version], [other-major],
[other-minor-line]. Hashing stays EXACT-VERSION only: a content hash is
computed over the producer's versioned URIs, so a node (or embedded value)
whose class the schema has only at a compatible later version fails with
[hash-needs-exact-version] rather than producing a different hash.
Hashing also refuses a statement the schema doesn't declare, an $extra entry or a key the class doesn't model, with [hash-undeclared-statement]: a codec holds one package's schema and can't type another package's predicate, so such a node is hashed with the SDK, over the graph. Serializing and deserializing still carry it (codec 2.0.0, runtime#37).
Reference values (runtime#6). A {"$ref": uri} must address a named
resource (publisher/package[@version]/name); a $ref containing a URI
fragment (#) is rejected at any depth by canonicalization, serialize and
deserialize alike, with an error ending in [fragment-reference].
An object property carries a reference ({"$ref": ...}) or an embedded node,
gated by codec-vectors-embedded.json. Malformed input fails loudly
(CodecError); no fallbacks.
Test
Runs the shared golden vectors in ../vectors/ (codec-vectors*.json,
including the compatibility file codec-vectors-compat.json).