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
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— modeled fields (nulls dropped) then$extraspread as siblings (modeled wins a collision; no$extrakey on the wire).deserialize—$-keys and$type-modeled fields stay top-level; everything else collects into$extrafor lossless round-trip.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.
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).