kanonak-codec 3.0.1

Kanonak codec runtime (Rust port). Builds the canonical input model from typed nodes + an embedded CodecSchema and content-addresses it via kanonak-canonical.
Documentation

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 kanonak_codec::{
    build_package, canonical_form, content_hash, deserialize, enum_member, serialize, type_matches,
    Node,
};

// nodes: &[Node], schema/pkg: &serde_json::Value
let hash = content_hash(&nodes, &schema, &pkg)?;   // "sha256:..."
let form = canonical_form(&nodes, &schema, &pkg)?; // {"subjects":[...]}
let wire = serialize(&node, &schema)?;              // normalized-JSON Node, numbers in their JSON forms
let node = deserialize(&wire, &schema)?;            // typed Node (unmodeled -> $extra)
let is_app = type_matches(&node, "example.com/vocab@5.1.0/Application", &schema)?; // bool
let member = enum_member(&schema, "example.com/vocab@5.0.0/north");                // 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 to kanonak-canonical.

  • serialize / deserialize — the normalized-JSON wire form. serialize writes modeled fields (nulls dropped) then $extra spread as siblings (modeled wins a collision; no $extra key on the wire); deserialize keeps $-keys and $type-modeled fields top-level and collects everything else into $extra for lossless round-trip. From 3.0.0, each integer and decimal value travels in its JSON form (kanonak-protocol 4.11.0's numbers-in-json, runtime#54), through lists and embedded values. In the node, integer, its four sign-restricted forms and decimal are a string of their canonical digits, since a serde_json::Value holds no integer past 64 bits; long, unsignedLong and int and narrower are numbers. An embedded value whose class the schema doesn't hold is carried as given. deserialize judges the Values it's given: without serde_json's arbitrary_precision, a number such as 1.0000000000000000001 is already the f64 1.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], as codec-vectors-json-numbers.json pins in every port.

  • The serde binding (3.0.0), for a generated struct's fields, so serde_json reads and writes them as serialize and deserialize do:

    • Integer<D>, an integer or one of its sign-restricted forms held as its canonical digits, where D is XsdInteger, NonNegativeInteger, PositiveInteger, NonPositiveInteger or NegativeInteger, whose range it checks;
    • Long and UnsignedLong, over i64 and u64, written as strings;
    • Decimal, held as its canonical digits and read from a string only.

    int and narrower take i32 and 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 optional ancestors field (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 through KanonakNode::type_matches on its envelope.

  • enum_member — the enumeration member a {"$ref": ...} names, as an EnumMemberMatch { enum_type, uri, member }, or None ("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

cargo test

Runs the shared golden vectors in ../vectors/ (codec-vectors*.json, including the compatibility file codec-vectors-compat.json).