Skip to main content

kanonak_codec/
lib.rs

1//! kanonak-codec — the generic, ontology-independent codec runtime (Rust port).
2//!
3//! Given a `CodecSchema` (the per-package metadata a generated SDK embeds) and a
4//! set of typed nodes, it builds the canonical input model and content-addresses
5//! it via `kanonak-canonical` (the same content-form the Python/TypeScript
6//! references and the `kanonak hash` CLI produce). It also (de)serializes the
7//! normalized-JSON wire form. Self-contained: carriers come from the schema's
8//! datatype URIs, and the resolved foundation URIs are embedded by the generator,
9//! so hashing needs no runtime ontology resolution.
10//!
11//! A node is a plain JSON object (`serde_json::Map<String, serde_json::Value>`) —
12//! the `$`-envelope plus alias-collapsed local-name fields. A generated typed
13//! model serializes to one. Note: `serde_json::Value` (the node field model) is
14//! distinct from `kanonak_canonical::Value` (the canonical-input value enum).
15
16use kanonak_canonical::{
17    canonical_decimal, canonical_form as canonical_form_pkg, canonical_hash as canonical_hash_pkg,
18    canonical_integer, carrier_of, is_readable_by, lenient_versionless_key, parse_coordinate,
19    versionless_key, CanonError, CoordinateVersion, Package, Statement, Subject, Value,
20};
21use serde_json::{Map, Value as Json};
22
23mod numbers;
24mod typed;
25pub use numbers::{
26    Decimal, Integer, IntegerDatatype, Long, NegativeInteger, NonNegativeInteger, NonPositiveInteger,
27    PositiveInteger, UnsignedLong, XsdInteger,
28};
29pub use typed::{to_node, KanonakNode, KanonakResource, Ref};
30
31/// The reserved `$`-envelope keys, which never become statements/predicates.
32/// `$name` (0.2.0) carries an embedded value's authored dict-key — hash-relevant.
33/// `$types` (0.4.0, runtime#10) carries a multi-typed node's FULL type set.
34const ENVELOPE_KEYS: [&str; 7] = [
35    "$type",
36    "$types",
37    "$id",
38    "$name",
39    "$contentHash",
40    "$version",
41    "$extra",
42];
43
44/// A node is a JSON object.
45pub type Node = Map<String, Json>;
46
47/// Errors raised by the codec runtime. Fails loudly — no fallbacks.
48///
49/// `#[non_exhaustive]`: a later 1.x release may add a variant, so a `match`
50/// outside this crate needs a wildcard arm. Branch on the message's bracketed
51/// kind (`[unknown-type]`, `[fragment-reference]`, ...) rather than the variant.
52#[derive(Debug)]
53#[non_exhaustive]
54pub enum CodecError {
55    /// A node, schema, or package context was malformed.
56    Malformed(String),
57    /// The underlying canonical library rejected a lexical/value.
58    Canon(CanonError),
59}
60
61impl std::fmt::Display for CodecError {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            CodecError::Malformed(m) => write!(f, "{}", m),
65            CodecError::Canon(e) => write!(f, "{}", e.0),
66        }
67    }
68}
69
70impl std::error::Error for CodecError {}
71
72impl From<CanonError> for CodecError {
73    fn from(e: CanonError) -> Self {
74        CodecError::Canon(e)
75    }
76}
77
78fn err<T>(msg: impl Into<String>) -> Result<T, CodecError> {
79    Err(CodecError::Malformed(msg.into()))
80}
81
82/// The raw lexical token of a scalar — the input the canonical form normalizes.
83/// bool -> "true"/"false"; string -> the string; number -> its plain decimal
84/// string (serde_json's `Number::to_string()` gives "5" / "1.5", never
85/// scientific notation). The canonical crate re-normalizes from there.
86fn lexical(value: &Json) -> String {
87    match value {
88        Json::Bool(b) => {
89            if *b {
90                "true".to_string()
91            } else {
92                "false".to_string()
93            }
94        }
95        Json::String(s) => s.clone(),
96        Json::Number(n) => n.to_string(),
97        other => other.to_string(),
98    }
99}
100
101/// The schema's `classes` map. A class entry may carry an optional
102/// `ancestors` list (0.6.1, runtime#28): every superclass, transitively, by
103/// durable VERSIONED URI — what lets [`type_matches`] accept a subclass node
104/// for a superclass check. Additive and canonicalization-INERT: a schema
105/// without it is still valid, and absent, a type match is by the class itself
106/// only — stricter, never looser.
107fn classes_of(schema: &Json) -> Result<&Map<String, Json>, CodecError> {
108    schema
109        .get("classes")
110        .and_then(|c| c.as_object())
111        .ok_or_else(|| CodecError::Malformed("schema is missing 'classes'".into()))
112}
113
114/// A coordinate's versionless key and version, or `None` when the string is
115/// not a STRICTLY well-formed coordinate or carries no version (never errors).
116fn versioned_coordinate(uri: &str) -> Option<(String, CoordinateVersion)> {
117    let c = parse_coordinate(uri).ok()?;
118    let version = c.version?;
119    Some((format!("{}/{}/{}", c.publisher, c.package, c.name), version))
120}
121
122/// The versionless identity of a coordinate (versioned or not), or `None`
123/// when the string is not one.
124fn identity_of(uri: &str) -> Option<String> {
125    versionless_key(uri).ok()
126}
127
128fn version_order(v: CoordinateVersion) -> (u64, u64, u64) {
129    (v.major, v.minor, v.patch)
130}
131
132fn format_version(v: CoordinateVersion) -> String {
133    format!("{}.{}.{}", v.major, v.minor, v.patch)
134}
135
136/// THE class lookup (runtime#28). A node is typed with the version of the
137/// class its producer's import resolved to; a codec generated from a later
138/// COMPATIBLE version of the same package must still read it. So: the exact
139/// versioned key first, then the class with the same publisher, package and
140/// name whose version can read the written one — `is_readable_by` from
141/// `kanonak-canonical`, the protocol's single compatibility rule, pinned in
142/// every port. Returns the class's schema key and entry. When nothing
143/// matches, the error says why, ending in a bracketed kind the conformance
144/// vectors pin: `[unknown-type]`, `[newer-version]` (the data may use terms
145/// this schema lacks), `[other-major]`, or `[other-minor-line]` (below 1.0.0
146/// the minor is the incompatible line).
147fn class_for<'a>(
148    classes: &'a Map<String, Json>,
149    type_uri: &str,
150) -> Result<(&'a str, &'a Json), String> {
151    if let Some((key, cls)) = classes.get_key_value(type_uri) {
152        return Ok((key.as_str(), cls));
153    }
154    let unknown = || format!("no schema for type {} [unknown-type]", type_uri);
155    let (key, written) = match versioned_coordinate(type_uri) {
156        Some(found) => found,
157        None => return Err(unknown()),
158    };
159
160    let mut readable: Option<(&'a str, &'a Json, CoordinateVersion)> = None;
161    let mut nearest: Option<CoordinateVersion> = None;
162    for (uri, cls) in classes.iter() {
163        let version = match versioned_coordinate(uri) {
164            Some((k, v)) if k == key => v,
165            _ => continue,
166        };
167        if is_readable_by(written, version) {
168            if readable.map_or(true, |(_, _, best)| {
169                version_order(version) > version_order(best)
170            }) {
171                readable = Some((uri.as_str(), cls, version));
172            }
173        } else if nearest.map_or(true, |n| version_order(version) > version_order(n)) {
174            nearest = Some(version);
175        }
176    }
177    if let Some((uri, cls, _)) = readable {
178        return Ok((uri, cls));
179    }
180    match nearest {
181        Some(r) => Err(incompatible_version(type_uri, written, r)),
182        None => Err(unknown()),
183    }
184}
185
186fn incompatible_version(type_uri: &str, w: CoordinateVersion, r: CoordinateVersion) -> String {
187    let (ws, rs) = (format_version(w), format_version(r));
188    if w.major != r.major {
189        return format!(
190            "{} is written at {}, a different major version than this codec's schema ({}) \
191             [other-major]",
192            type_uri, ws, rs
193        );
194    }
195    if w.major == 0 && w.minor != r.minor {
196        return format!(
197            "{} is written at {}; below 1.0.0 a different minor is a different version line \
198             than this codec's schema ({}) [other-minor-line]",
199            type_uri, ws, rs
200        );
201    }
202    format!(
203        "{} is written at {}, newer than this codec's schema ({}); upgrade the codec to read it \
204         [newer-version]",
205        type_uri, ws, rs
206    )
207}
208
209/// The class to HASH a node or embedded value with: the exact versioned class
210/// only. A content hash is computed over the producer's predicate and type
211/// URIs, versions included, so a node written at an earlier compatible
212/// version cannot be re-hashed with a later schema — its predicates would
213/// carry the later version and the hash would differ. When only a compatible
214/// class exists, say so (`[hash-needs-exact-version]`) rather than produce a
215/// different hash or the bare "no schema" error.
216fn hash_class_for<'a>(
217    schema: &'a Json,
218    type_uri: &str,
219    what: &str,
220) -> Result<&'a Json, CodecError> {
221    let classes = classes_of(schema)?;
222    if let Some(cls) = classes.get(type_uri) {
223        return Ok(cls);
224    }
225    match class_for(classes, type_uri) {
226        Ok((found, _)) => err(format!(
227            "Cannot hash {} {}: this codec's schema has it at {}, and a content hash is \
228             computed with the producer's schema version — hashing needs the exact class \
229             [hash-needs-exact-version]",
230            what, type_uri, found
231        )),
232        Err(_) => err(format!(
233            "no schema for {} {} [unknown-type]",
234            what, type_uri
235        )),
236    }
237}
238
239/// Whether a node is an instance of the class `class_uri` — what a generated
240/// type guard asks (runtime#28). Each of the node's types (`$types`, else its
241/// `$type`) is resolved through the same compatible lookup [`deserialize`]
242/// uses, so a node written at an earlier compatible version satisfies the
243/// later version's guard; it matches when that class IS `class_uri` or has it
244/// among its `ancestors`. Classes compare by versionless identity. A type this
245/// schema cannot read (unknown, newer, another major) never matches. Errors
246/// when `class_uri` is not a class coordinate or the schema is malformed.
247/// For a typed instance, [`KanonakNode::type_matches`] asks the same question
248/// of its envelope.
249pub fn type_matches(node: &Node, class_uri: &str, schema: &Json) -> Result<bool, CodecError> {
250    let members: Vec<&str> = match node.get("$types") {
251        None | Some(Json::Null) => node
252            .get("$type")
253            .and_then(|t| t.as_str())
254            .filter(|t| !t.is_empty())
255            .into_iter()
256            .collect(),
257        Some(Json::Array(items)) => items
258            .iter()
259            .map(|t| t.as_str())
260            .collect::<Option<Vec<&str>>>()
261            .ok_or_else(|| {
262                CodecError::Malformed("type_matches: $types must be a list of type URIs".into())
263            })?,
264        Some(_) => return err("type_matches: $types must be a list of type URIs"),
265    };
266    type_matches_members(&members, class_uri, schema)
267}
268
269/// The one type-match walk, over a node's type members (`$types`, else
270/// `$type`), shared by the dictionary and typed surfaces.
271pub(crate) fn type_matches_members(
272    members: &[&str],
273    class_uri: &str,
274    schema: &Json,
275) -> Result<bool, CodecError> {
276    let target = identity_of(class_uri).ok_or_else(|| {
277        CodecError::Malformed(format!(
278            "type_matches: '{}' is not a class coordinate",
279            class_uri
280        ))
281    })?;
282    let classes = classes_of(schema)?;
283
284    for &member in members {
285        let (key, cls) = match class_for(classes, member) {
286            Ok(found) => found,
287            Err(_) => continue,
288        };
289        let type_uri = cls
290            .get("typeUri")
291            .and_then(|t| t.as_str())
292            .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'typeUri'", key)))?;
293        if identity_of(type_uri).as_deref() == Some(target.as_str()) {
294            return Ok(true);
295        }
296        match cls.get("ancestors") {
297            None | Some(Json::Null) => {}
298            Some(Json::Array(ancestors)) => {
299                for ancestor in ancestors {
300                    let ancestor = ancestor.as_str().ok_or_else(|| {
301                        CodecError::Malformed(format!(
302                            "class {}: 'ancestors' must be a list of class URIs",
303                            key
304                        ))
305                    })?;
306                    if identity_of(ancestor).as_deref() == Some(target.as_str()) {
307                        return Ok(true);
308                    }
309                }
310            }
311            Some(_) => {
312                return err(format!(
313                    "class {}: 'ancestors' must be a list of class URIs",
314                    key
315                ))
316            }
317        }
318    }
319    Ok(false)
320}
321
322/// An enumeration member found by [`enum_member`]. `#[non_exhaustive]`: read
323/// its fields; a later 1.x release may add one.
324#[derive(Debug, Clone, PartialEq)]
325#[non_exhaustive]
326pub struct EnumMemberMatch {
327    /// The enumeration class's durable URI (its key in the schema's `enums`).
328    pub enum_type: String,
329    /// The member's durable URI as this schema keys it.
330    pub uri: String,
331    /// The member's schema entry (`{"label": ...}`).
332    pub member: Json,
333}
334
335/// The enumeration member a `{"$ref": …}` names (runtime#28): the exact
336/// versioned key first, then the same member at a version that can read the
337/// written one — so a member referenced at an earlier compatible version of
338/// the package resolves against a later schema. `None` when this schema has
339/// no such member, which, as for `enums` itself, means "not mine", never
340/// "invalid".
341pub fn enum_member(schema: &Json, reference: &str) -> Option<EnumMemberMatch> {
342    let enums = schema.get("enums").and_then(|e| e.as_object())?;
343    for (enum_type, e) in enums.iter() {
344        if let Some(member) = e.get("members").and_then(|m| m.get(reference)) {
345            return Some(EnumMemberMatch {
346                enum_type: enum_type.clone(),
347                uri: reference.to_string(),
348                member: member.clone(),
349            });
350        }
351    }
352    let (key, written) = versioned_coordinate(reference)?;
353    let mut best: Option<(&str, &str, &Json, CoordinateVersion)> = None;
354    for (enum_type, e) in enums.iter() {
355        let members = match e.get("members").and_then(|m| m.as_object()) {
356            Some(members) => members,
357            None => continue,
358        };
359        for (uri, member) in members.iter() {
360            let version = match versioned_coordinate(uri) {
361                Some((k, v)) if k == key => v,
362                _ => continue,
363            };
364            if !is_readable_by(written, version) {
365                continue;
366            }
367            if best.map_or(true, |(_, _, _, b)| {
368                version_order(version) > version_order(b)
369            }) {
370                best = Some((enum_type.as_str(), uri.as_str(), member, version));
371            }
372        }
373    }
374    best.map(|(enum_type, uri, member, _)| EnumMemberMatch {
375        enum_type: enum_type.to_string(),
376        uri: uri.to_string(),
377        member: member.clone(),
378    })
379}
380
381/// Validate a node-or-embedded's `$types` envelope (0.4.0, runtime#10) and
382/// return the validated set, or `None` when the node is single-typed.
383/// Invariants: sorted by UTF-8 bytes, at least two members, no duplicates, and
384/// `$type` (the dispatch key, chosen by the schema layer's primary rule) is a
385/// member. Enforced wherever the envelope is touched — serialize, deserialize,
386/// and canonicalization — so a producer fails at emit time and a reader never
387/// masks a nondeterministic emitter by silently repairing the set.
388fn validated_types(
389    map: &Map<String, Json>,
390    where_: &str,
391) -> Result<Option<Vec<String>>, CodecError> {
392    let raw = match map.get("$types") {
393        None | Some(Json::Null) => return Ok(None),
394        Some(v) => v,
395    };
396    let items = match raw.as_array() {
397        Some(items) => items,
398        None => {
399            return err(format!(
400                "{}: $types must be a list of non-empty type URIs",
401                where_
402            ))
403        }
404    };
405    let mut types: Vec<String> = Vec::with_capacity(items.len());
406    for item in items {
407        match item.as_str() {
408            Some(s) if !s.is_empty() => types.push(s.to_string()),
409            _ => {
410                return err(format!(
411                    "{}: $types must be a list of non-empty type URIs",
412                    where_
413                ))
414            }
415        }
416    }
417    if types.len() < 2 {
418        return err(format!(
419            "{}: $types with {} member(s) is forbidden — a single-typed node carries \
420             only $type (a second encoding of the same content would be hash-ambiguous)",
421            where_,
422            types.len()
423        ));
424    }
425    for pair in types.windows(2) {
426        match pair[0].as_bytes().cmp(pair[1].as_bytes()) {
427            std::cmp::Ordering::Equal => {
428                return err(format!(
429                    "{}: $types carries duplicate member {}",
430                    where_, pair[1]
431                ))
432            }
433            std::cmp::Ordering::Greater => {
434                return err(format!(
435                    "{}: $types is not sorted by UTF-8 bytes ({} sorts after {}) — \
436                     ordering is the producer's job, never the reader's",
437                    where_, pair[0], pair[1]
438                ))
439            }
440            std::cmp::Ordering::Less => {}
441        }
442    }
443    match map.get("$type").and_then(|t| t.as_str()) {
444        Some(primary) if types.iter().any(|t| t == primary) => Ok(Some(types)),
445        other => err(format!(
446            "{}: $type ({:?}) must be present and a member of $types",
447            where_, other
448        )),
449    }
450}
451
452/// Recursively validate every `$types` envelope in a wire value (the node
453/// itself and any embedded node at any depth). Shared by [`serialize`] (the
454/// producer fails at emit time) and [`deserialize`] (the strict reader rejects
455/// rather than repairs).
456fn assert_types_envelopes(value: &Json, where_: &str) -> Result<(), CodecError> {
457    match value {
458        Json::Array(items) => {
459            for (i, item) in items.iter().enumerate() {
460                assert_types_envelopes(item, &format!("{}[{}]", where_, i))?;
461            }
462            Ok(())
463        }
464        Json::Object(map) => assert_types_envelopes_map(map, where_),
465        _ => Ok(()),
466    }
467}
468
469fn assert_types_envelopes_map(map: &Map<String, Json>, where_: &str) -> Result<(), CodecError> {
470    if map.contains_key("$types") {
471        validated_types(map, where_)?;
472    }
473    for (key, value) in map.iter() {
474        if key != "$types" {
475            assert_types_envelopes(value, &format!("{}.{}", where_, key))?;
476        }
477    }
478    Ok(())
479}
480
481/// Every reference value (`{"$ref": uri}`) in a wire value, at any depth, must
482/// address a NAMED resource: `publisher/package[@version]/name`, no URI fragment
483/// (runtime#6). Fragments address embedded resources for navigation and
484/// deep-linking; they are not part of the reference graph, and accepting one
485/// here would establish that capability by accident. Shared by [`serialize`],
486/// [`deserialize`] and canonicalization, so all three reject the same input.
487fn assert_reference_values(value: &Json, where_: &str) -> Result<(), CodecError> {
488    match value {
489        Json::Array(items) => {
490            for (i, item) in items.iter().enumerate() {
491                assert_reference_values(item, &format!("{}[{}]", where_, i))?;
492            }
493            Ok(())
494        }
495        Json::Object(map) => assert_reference_values_map(map, where_),
496        _ => Ok(()),
497    }
498}
499
500fn assert_reference_values_map(map: &Map<String, Json>, where_: &str) -> Result<(), CodecError> {
501    if let Some(Json::String(uri)) = map.get("$ref") {
502        assert_named_reference(uri, where_)?;
503    }
504    for (key, value) in map.iter() {
505        assert_reference_values(value, &format!("{}.{}", where_, key))?;
506    }
507    Ok(())
508}
509
510fn assert_named_reference(uri: &str, where_: &str) -> Result<(), CodecError> {
511    if uri.contains('#') {
512        return err(format!(
513            "{}: reference value '{}' contains a URI fragment. A reference addresses a named \
514             resource (publisher/package[@version]/name); fragments are a navigation convention, \
515             not part of the reference graph [fragment-reference]",
516            where_, uri
517        ));
518    }
519    Ok(())
520}
521
522/// Build a single canonical `Value` for one (non-list) field datum, per its
523/// schema prop.
524fn build_value(prop: &Json, raw: &Json, schema: &Json) -> Result<Value, CodecError> {
525    let kind = prop
526        .get("kind")
527        .and_then(|k| k.as_str())
528        .ok_or_else(|| CodecError::Malformed("schema prop is missing 'kind'".into()))?;
529    if kind == "object" {
530        // A node: a reference (`{"$ref"}`) or an embedded resource.
531        if let Some(reference) = raw.get("$ref").and_then(|r| r.as_str()) {
532            return Ok(Value::Reference(reference.to_string()));
533        }
534        if let Some(map) = raw.as_object() {
535            return embedded_value(prop, map, schema);
536        }
537        return err(format!(
538            "Object property expects a reference ({{\"$ref\": ...}}) or an embedded \
539             node (a map), got {}",
540            raw
541        ));
542    }
543    let datatype = prop
544        .get("datatype")
545        .and_then(|d| d.as_str())
546        .ok_or_else(|| CodecError::Malformed("datatype prop is missing 'datatype'".into()))?;
547    match carrier_of(datatype) {
548        None => Ok(Value::Raw(lexical(raw))),
549        Some(carrier) => Ok(Value::Typed {
550            carrier,
551            lexical: lexical(raw),
552        }),
553    }
554}
555
556/// Canonicalize an embedded value (0.2.0): a map with no `$id`, an optional
557/// `$name` (the authored dict-key — hash-relevant), an optional `$type`, and
558/// schema-mapped fields. An explicit `$type` emits a type statement inside the
559/// embedded (hash-relevant even when it equals the range-derived type); without
560/// it, fields map via the containing property's `range` and NO type statement is
561/// emitted — range-derived typing is inference only.
562fn embedded_value(
563    prop: &Json,
564    map: &Map<String, Json>,
565    schema: &Json,
566) -> Result<Value, CodecError> {
567    if map.contains_key("$id") {
568        return err(
569            "An embedded value must not carry $id — to point at a named resource, \
570             pass a reference ({\"$ref\": ...}).",
571        );
572    }
573    let types = validated_types(map, "Embedded value")?;
574    let explicit_type = map.get("$type").and_then(|t| t.as_str());
575    let cls_uri = match explicit_type.or_else(|| prop.get("range").and_then(|r| r.as_str())) {
576        Some(uri) => uri,
577        None => {
578            return err(
579                "Cannot map embedded value: it carries no $type and the property \
580                 declares no range.",
581            )
582        }
583    };
584    let cls = hash_class_for(schema, cls_uri, "embedded type")?;
585    let props = cls
586        .get("props")
587        .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", cls_uri)))?;
588
589    let mut statements = field_statements(map, cls_uri, props, schema)?;
590    if let Some(members) = types {
591        // A multi-typed embedded ($types implies an explicit $type): one type
592        // statement per member, in $types (UTF-8 sorted) order — all hash-relevant.
593        let type_predicate = schema
594            .get("typePredicate")
595            .and_then(|p| p.as_str())
596            .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
597        for member in members {
598            statements.push(Statement {
599                predicate: type_predicate.to_string(),
600                value: Value::Reference(member),
601            });
602        }
603    } else if let Some(type_uri) = explicit_type {
604        let type_predicate = schema
605            .get("typePredicate")
606            .and_then(|p| p.as_str())
607            .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
608        statements.push(Statement {
609            predicate: type_predicate.to_string(),
610            value: Value::Reference(type_uri.to_string()),
611        });
612    }
613    let name = map
614        .get("$name")
615        .and_then(|n| n.as_str())
616        .filter(|s| !s.is_empty())
617        .map(|s| s.to_string());
618    Ok(Value::Embedded { name, statements })
619}
620
621/// The statements for one node-or-embedded's modeled fields + its `$extra` —
622/// everything except the type triple (subjects always carry one; embeddeds only
623/// when explicitly typed).
624fn field_statements(
625    source: &Map<String, Json>,
626    class_uri: &str,
627    props: &Json,
628    schema: &Json,
629) -> Result<Vec<Statement>, CodecError> {
630    let mut out: Vec<Statement> = Vec::new();
631
632    for (key, raw) in source.iter() {
633        if ENVELOPE_KEYS.contains(&key.as_str()) || raw.is_null() {
634            continue;
635        }
636        match props.get(key) {
637            None => return undeclared_statement(class_uri, &format!("\"{}\"", key)),
638            Some(prop) => {
639                let predicate =
640                    prop.get("predicate")
641                        .and_then(|p| p.as_str())
642                        .ok_or_else(|| {
643                            CodecError::Malformed(format!("prop {} is missing 'predicate'", key))
644                        })?;
645                let value = match raw.as_array() {
646                    Some(items) => {
647                        // An empty list contributes NO statement — absent and empty
648                        // are identical at the canonical layer (the wire serialize
649                        // still preserves the empty list).
650                        if items.is_empty() {
651                            continue;
652                        }
653                        let mut list = Vec::with_capacity(items.len());
654                        for item in items {
655                            list.push(build_value(prop, item, schema)?);
656                        }
657                        Value::List(list)
658                    }
659                    None => build_value(prop, raw, schema)?,
660                };
661                out.push(Statement {
662                    predicate: predicate.to_string(),
663                    value,
664                });
665            }
666        }
667    }
668
669    // Open-world extras ride through serialize and deserialize, but this codec
670    // can't type them, so it refuses to hash them rather than hash them as text.
671    if let Some(extra) = source.get("$extra") {
672        let extra = extra
673            .as_object()
674            .ok_or_else(|| CodecError::Malformed("$extra must be an object".into()))?;
675        for (predicate, raw) in extra.iter() {
676            if raw.is_null() {
677                continue;
678            }
679            return undeclared_statement(class_uri, &format!("$extra statement {}", predicate));
680        }
681    }
682    Ok(out)
683}
684
685/// A codec holds one package's schema, so it can't know the range of a
686/// predicate another package declares on its classes (open-world augmentation).
687/// Hashing such a statement as untyped text would give a hash that differs from
688/// `kanonak hash`'s without a word, so hashing refuses it, naming it
689/// (`[hash-undeclared-statement]`). Serializing and deserializing carry it.
690fn undeclared_statement<T>(class_uri: &str, what: &str) -> Result<T, CodecError> {
691    err(format!(
692        "Cannot hash {}'s {}: this codec's schema doesn't declare it, so it can't type its value. \
693         Hash it with the SDK, over the graph [hash-undeclared-statement]",
694        class_uri, what
695    ))
696}
697
698/// The statements for one subject node: the rdf:type triple(s), then its fields.
699fn statements(node: &Node, schema: &Json) -> Result<Vec<Statement>, CodecError> {
700    let types = {
701        let id = node
702            .get("$id")
703            .and_then(|i| i.as_str())
704            .unwrap_or("(no $id)");
705        let where_ = format!("Node {}", id);
706        let types = validated_types(node, &where_)?;
707        assert_reference_values_map(node, &where_)?;
708        types
709    };
710    let type_uri = node
711        .get("$type")
712        .and_then(|t| t.as_str())
713        .filter(|s| !s.is_empty())
714        .ok_or_else(|| CodecError::Malformed("node is missing $type".into()))?;
715
716    let cls = hash_class_for(schema, type_uri, "type")?;
717    let props = cls
718        .get("props")
719        .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", type_uri)))?;
720
721    let type_predicate = schema
722        .get("typePredicate")
723        .and_then(|p| p.as_str())
724        .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
725
726    // The rdf:type triple(s) every subject carries: one per $types member for a
727    // multi-typed node (in $types' UTF-8 sorted order), else the single $type.
728    let members = types.unwrap_or_else(|| vec![type_uri.to_string()]);
729    let mut out: Vec<Statement> = members
730        .into_iter()
731        .map(|member| Statement {
732            predicate: type_predicate.to_string(),
733            value: Value::Reference(member),
734        })
735        .collect();
736    out.extend(field_statements(node, type_uri, props, schema)?);
737    Ok(out)
738}
739
740/// Build the canonical input model: a subject per node + the synthesized
741/// package-wrapper subject (raw label + `Package` type), exactly the subject set
742/// `kanonak hash` produces for the equivalent authored package.
743pub fn build_package(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<Package, CodecError> {
744    let mut subjects: Vec<Subject> = Vec::with_capacity(nodes.len() + 1);
745    for node in nodes {
746        let id = node
747            .get("$id")
748            .and_then(|i| i.as_str())
749            .filter(|s| !s.is_empty())
750            .ok_or_else(|| CodecError::Malformed("node is missing $id".into()))?;
751        subjects.push(Subject {
752            uri: id.to_string(),
753            statements: statements(node, schema)?,
754        });
755    }
756
757    let publisher = pkg
758        .get("publisher")
759        .and_then(|p| p.as_str())
760        .ok_or_else(|| CodecError::Malformed("pkg is missing 'publisher'".into()))?;
761    let package_name = pkg
762        .get("packageName")
763        .and_then(|p| p.as_str())
764        .ok_or_else(|| CodecError::Malformed("pkg is missing 'packageName'".into()))?;
765    let version = pkg
766        .get("version")
767        .and_then(|p| p.as_str())
768        .ok_or_else(|| CodecError::Malformed("pkg is missing 'version'".into()))?;
769
770    let pkg_uri = format!(
771        "{}/{}@{}/{}",
772        publisher, package_name, version, package_name
773    );
774
775    let type_predicate = schema
776        .get("typePredicate")
777        .and_then(|p| p.as_str())
778        .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
779    let label_predicate = schema
780        .get("labelPredicate")
781        .and_then(|p| p.as_str())
782        .ok_or_else(|| CodecError::Malformed("schema is missing 'labelPredicate'".into()))?;
783    let package_type_uri = schema
784        .get("packageTypeUri")
785        .and_then(|p| p.as_str())
786        .ok_or_else(|| CodecError::Malformed("schema is missing 'packageTypeUri'".into()))?;
787
788    let mut pkg_statements: Vec<Statement> = Vec::new();
789    if let Some(label) = pkg.get("label") {
790        if !label.is_null() {
791            let label = label
792                .as_str()
793                .ok_or_else(|| CodecError::Malformed("pkg label must be a string".into()))?;
794            pkg_statements.push(Statement {
795                predicate: label_predicate.to_string(),
796                value: Value::Raw(label.to_string()),
797            });
798        }
799    }
800    pkg_statements.push(Statement {
801        predicate: type_predicate.to_string(),
802        value: Value::Reference(package_type_uri.to_string()),
803    });
804    subjects.push(Subject {
805        uri: pkg_uri,
806        statements: pkg_statements,
807    });
808
809    Ok(Package { subjects })
810}
811
812/// The canonical form (the `{subjects:[...]}` JSON) of a package from nodes.
813pub fn canonical_form(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<String, CodecError> {
814    Ok(canonical_form_pkg(&build_package(nodes, schema, pkg)?)?)
815}
816
817/// The `sha256:` content hash of a package from nodes — matches `kanonak hash`.
818pub fn content_hash(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<String, CodecError> {
819    Ok(canonical_hash_pkg(&build_package(nodes, schema, pkg)?)?)
820}
821
822/// A node's normalized-JSON wire projection: `$extra` spread back to top
823/// level, nulls dropped.
824fn project_node(node: &Node) -> Node {
825    let mut out = Map::new();
826    for (key, value) in node.iter() {
827        if key == "$extra" || value.is_null() {
828            continue;
829        }
830        out.insert(key.clone(), value.clone());
831    }
832    if let Some(extra) = node.get("$extra").and_then(|e| e.as_object()) {
833        for (key, value) in extra.iter() {
834            if !value.is_null() && !out.contains_key(key) {
835                out.insert(key.clone(), value.clone());
836            }
837        }
838    }
839    out
840}
841
842/// A node parsed from normalized JSON as [`deserialize`] parses it, its values
843/// as JSON gave them.
844pub(crate) fn parse_node(json_obj: &Node, schema: &Json) -> Result<Node, CodecError> {
845    let type_uri = json_obj
846        .get("$type")
847        .and_then(|t| t.as_str())
848        .ok_or_else(|| CodecError::Malformed("Cannot deserialize: missing string $type".into()))?;
849
850    // Reader-side $types validation, at every depth: an unsorted / singleton /
851    // duplicate / non-member set is REJECTED, never silently repaired —
852    // determinism belongs to the producer, and a lenient reader would mask a
853    // nondeterministic emitter.
854    {
855        let where_ = json_obj
856            .get("$id")
857            .and_then(|i| i.as_str())
858            .unwrap_or(type_uri);
859        assert_types_envelopes_map(json_obj, &format!("deserialize {}", where_))?;
860        assert_reference_values_map(json_obj, &format!("deserialize {}", where_))?;
861    }
862
863    let (cls_uri, cls) = class_for(classes_of(schema)?, type_uri)
864        .map_err(|e| CodecError::Malformed(format!("Cannot deserialize: {}", e)))?;
865    let props = cls
866        .get("props")
867        .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", cls_uri)))?;
868
869    let mut node = Map::new();
870    node.insert("$type".to_string(), Json::String(type_uri.to_string()));
871    let mut extra = Map::new();
872    for (key, value) in json_obj.iter() {
873        if key == "$type" {
874            continue;
875        }
876        if key.starts_with('$') || props.get(key).is_some() {
877            node.insert(key.clone(), value.clone());
878        } else {
879            extra.insert(key.clone(), value.clone());
880        }
881    }
882    if !extra.is_empty() {
883        node.insert("$extra".to_string(), Json::Object(extra));
884    }
885    Ok(node)
886}
887
888// The JSON forms of integer and decimal values (kanonak-protocol 4.11.0's
889// `numbers-in-json`, runtime#54). A JSON number can't carry every integer or
890// decimal exactly in every language, so a wide integer and a decimal travel as
891// a string of their digits, and a reader refuses a number that may already
892// have been rounded.
893
894/// How JSON carries a datatype's values.
895#[derive(Clone, Copy)]
896pub(crate) enum JsonNumberForm {
897    /// Written as a string; read from a string, or from a whole number within
898    /// ±(2^53 − 1).
899    Wide,
900    /// Written as a string; read from a string only.
901    Decimal,
902    /// Written as a number; read from a whole number only.
903    Narrow,
904}
905
906/// The datatypes JSON carries as a number or a string of digits, by
907/// versionless key.
908fn json_number_form(key: &str) -> Option<(JsonNumberForm, Option<i128>, Option<i128>)> {
909    xsd_number_form(key.strip_prefix("kanonak.org/core-xsd/")?)
910}
911
912/// THE table of the core-xsd datatypes JSON carries as a number or a string of
913/// digits, by local name: each one's form, and its range's bounds where it has
914/// them. Every bound fits in `i128`.
915pub(crate) fn xsd_number_form(local: &str) -> Option<(JsonNumberForm, Option<i128>, Option<i128>)> {
916    use JsonNumberForm::{Decimal, Narrow, Wide};
917    Some(match local {
918        "integer" => (Wide, None, None),
919        "nonNegativeInteger" => (Wide, Some(0), None),
920        "positiveInteger" => (Wide, Some(1), None),
921        "nonPositiveInteger" => (Wide, None, Some(0)),
922        "negativeInteger" => (Wide, None, Some(-1)),
923        "long" => (Wide, Some(i64::MIN.into()), Some(i64::MAX.into())),
924        "unsignedLong" => (Wide, Some(0), Some(u64::MAX.into())),
925        "decimal" => (Decimal, None, None),
926        "int" => (Narrow, Some(i32::MIN.into()), Some(i32::MAX.into())),
927        "short" => (Narrow, Some(i16::MIN.into()), Some(i16::MAX.into())),
928        "byte" => (Narrow, Some(i8::MIN.into()), Some(i8::MAX.into())),
929        "unsignedInt" => (Narrow, Some(0), Some(u32::MAX.into())),
930        "unsignedShort" => (Narrow, Some(0), Some(u16::MAX.into())),
931        "unsignedByte" => (Narrow, Some(0), Some(u8::MAX.into())),
932        _ => return None,
933    })
934}
935
936/// Whether an integer's canonical digits lie outside the bounds. Every bound
937/// fits in `i128`, so digits too long for one lie past the bound on their
938/// sign's side, if there is one.
939pub(crate) fn outside_bounds(digits: &str, min: Option<i128>, max: Option<i128>) -> bool {
940    match digits.parse::<i128>() {
941        Ok(n) => min.map_or(false, |min| n < min) || max.map_or(false, |max| n > max),
942        Err(_) if digits.starts_with('-') => min.is_some(),
943        Err(_) => max.is_some(),
944    }
945}
946
947/// A modeled field whose datatype JSON carries as a number or a string of
948/// digits.
949struct NumberField<'a> {
950    predicate: &'a str,
951    datatype: &'a str,
952    form: JsonNumberForm,
953    min: Option<i128>,
954    max: Option<i128>,
955}
956
957impl NumberField<'_> {
958    fn refuse<T>(&self, why: impl std::fmt::Display) -> Result<T, CodecError> {
959        err(format!("{}: {} [json-number-form]", self.predicate, why))
960    }
961
962    fn integer_of_lexical(&self, lexical: &str) -> Result<String, CodecError> {
963        canonical_integer(lexical)
964            .or_else(|_| self.refuse(format!("{:?} is not an integer lexical form", lexical)))
965    }
966
967    fn decimal_of_lexical(&self, lexical: &str) -> Result<String, CodecError> {
968        canonical_decimal(lexical)
969            .or_else(|_| self.refuse(format!("{:?} is not a decimal lexical form", lexical)))
970    }
971
972    /// An integer's canonical digits, refused outside the datatype's range.
973    fn in_range(&self, digits: String) -> Result<String, CodecError> {
974        if outside_bounds(&digits, self.min, self.max) {
975            return self.refuse(format!("{} is outside {}'s range", digits, self.datatype));
976        }
977        Ok(digits)
978    }
979
980    /// An integer as a node here holds it: a number where its datatype bounds
981    /// it on both sides, so it fits an `i64` or a `u64`; else a string of its
982    /// canonical digits, since no Rust integer holds every value.
983    fn held(&self, digits: String) -> Result<Json, CodecError> {
984        if self.min.is_none() || self.max.is_none() {
985            return Ok(Json::String(digits));
986        }
987        digits
988            .parse::<i64>()
989            .map(Json::from)
990            .or_else(|_| digits.parse::<u64>().map(Json::from))
991            .or_else(|_| self.refuse(format!("{} fits neither an i64 nor a u64", digits)))
992    }
993}
994
995/// The JSON number form of a modeled field's datatype, or `None` when JSON
996/// carries its values as they stand.
997fn number_field<'a>(name: &str, prop: &'a Json) -> Result<Option<NumberField<'a>>, CodecError> {
998    let datatype = match prop.get("datatype").and_then(|d| d.as_str()) {
999        Some(datatype) => datatype,
1000        None => return Ok(None),
1001    };
1002    let (form, min, max) =
1003        match lenient_versionless_key(datatype).and_then(|key| json_number_form(&key)) {
1004            Some(found) => found,
1005            None => return Ok(None),
1006        };
1007    let predicate = prop
1008        .get("predicate")
1009        .and_then(|p| p.as_str())
1010        .ok_or_else(|| CodecError::Malformed(format!("prop {} is missing 'predicate'", name)))?;
1011    Ok(Some(NumberField {
1012        predicate,
1013        datatype,
1014        form,
1015        min,
1016        max,
1017    }))
1018}
1019
1020/// A JSON number's value as an integer's canonical digits, when it is a whole
1021/// number.
1022fn whole_digits(value: &Json) -> Option<String> {
1023    let n = match value {
1024        Json::Number(n) => n,
1025        _ => return None,
1026    };
1027    if let Some(i) = n.as_i64() {
1028        return Some(i.to_string());
1029    }
1030    if let Some(u) = n.as_u64() {
1031        return Some(u.to_string());
1032    }
1033    let f = n.as_f64().filter(|f| f.fract() == 0.0)?;
1034    canonical_integer(&format!("{:.0}", f)).ok()
1035}
1036
1037/// Whether an integer lies within ±(2^53 − 1), where a double holds every
1038/// whole number exactly.
1039pub(crate) fn within_double_precision(digits: &str) -> bool {
1040    const MAX_SAFE_INTEGER: i128 = (1 << 53) - 1;
1041    digits
1042        .parse::<i128>()
1043        .map_or(false, |n| n.abs() <= MAX_SAFE_INTEGER)
1044}
1045
1046fn describe_json(value: &Json) -> String {
1047    let kind = match value {
1048        Json::Null => return "null".to_string(),
1049        Json::Bool(_) => "the boolean",
1050        Json::Number(_) => "the number",
1051        Json::String(_) => "the string",
1052        Json::Array(_) => "the list",
1053        Json::Object(_) => "the object",
1054    };
1055    format!("{} {}", kind, value)
1056}
1057
1058type NumberConversion = fn(&NumberField, &Json) -> Result<Json, CodecError>;
1059
1060/// A value as JSON carries it, to the type a node here holds it as.
1061fn read_json_number(field: &NumberField, value: &Json) -> Result<Json, CodecError> {
1062    match field.form {
1063        JsonNumberForm::Decimal => match value {
1064            Json::String(s) => Ok(Json::String(field.decimal_of_lexical(s)?)),
1065            other => field.refuse(format!(
1066                "a decimal is read from a JSON string only, so it can't have been rounded; got {}",
1067                describe_json(other)
1068            )),
1069        },
1070        JsonNumberForm::Wide => {
1071            let digits = match value {
1072                Json::String(s) => Some(field.integer_of_lexical(s)?),
1073                Json::Number(_) => whole_digits(value).filter(|d| within_double_precision(d)),
1074                _ => None,
1075            };
1076            match digits {
1077                Some(digits) => field.held(field.in_range(digits)?),
1078                None => field.refuse(format!(
1079                    "a wide integer is read from a string, or from a whole JSON number within \
1080                     ±(2^53 − 1); got {}",
1081                    describe_json(value)
1082                )),
1083            }
1084        }
1085        JsonNumberForm::Narrow => match whole_digits(value) {
1086            Some(digits) => field.held(field.in_range(digits)?),
1087            None => field.refuse(format!(
1088                "a narrow integer is read from a whole JSON number only; got {}",
1089                describe_json(value)
1090            )),
1091        },
1092    }
1093}
1094
1095/// A value as a node here holds it, to its JSON form.
1096fn write_json_number(field: &NumberField, value: &Json) -> Result<Json, CodecError> {
1097    match field.form {
1098        JsonNumberForm::Decimal => match value {
1099            Json::String(s) => Ok(Json::String(field.decimal_of_lexical(s)?)),
1100            other => field.refuse(format!(
1101                "pass a decimal as a string of its digits, which holds it exactly; got {}",
1102                describe_json(other)
1103            )),
1104        },
1105        JsonNumberForm::Wide => {
1106            let digits = match value {
1107                Json::String(s) => Some(field.integer_of_lexical(s)?),
1108                Json::Number(n) => {
1109                    whole_digits(value).filter(|d| !n.is_f64() || within_double_precision(d))
1110                }
1111                _ => None,
1112            };
1113            match digits {
1114                Some(digits) => Ok(Json::String(field.in_range(digits)?)),
1115                None => field.refuse(format!(
1116                    "pass a wide integer as a string of its digits, an i64 or u64 number, or a \
1117                     whole number within ±(2^53 − 1); got {}",
1118                    describe_json(value)
1119                )),
1120            }
1121        }
1122        JsonNumberForm::Narrow => match whole_digits(value) {
1123            Some(digits) => field.held(field.in_range(digits)?),
1124            None => field.refuse(format!(
1125                "pass a narrow integer as a whole number; got {}",
1126                describe_json(value)
1127            )),
1128        },
1129    }
1130}
1131
1132/// The modeled fields of the class `type_uri` names, found as [`deserialize`]
1133/// finds it.
1134fn props_to_convert<'a>(
1135    schema: &'a Json,
1136    type_uri: &str,
1137    what: &str,
1138) -> Result<&'a Map<String, Json>, CodecError> {
1139    let (cls_uri, cls) = class_for(classes_of(schema)?, type_uri)
1140        .map_err(|e| CodecError::Malformed(format!("Cannot convert {}: {}", what, e)))?;
1141    props_of(cls_uri, cls)
1142}
1143
1144fn props_of<'a>(cls_uri: &str, cls: &'a Json) -> Result<&'a Map<String, Json>, CodecError> {
1145    cls.get("props")
1146        .and_then(|p| p.as_object())
1147        .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", cls_uri)))
1148}
1149
1150/// Each modeled field's value, through lists and embedded values, with its
1151/// integers and decimals converted. `$extra`, the envelope and references are
1152/// left as they are.
1153fn convert_fields(
1154    fields: &Map<String, Json>,
1155    props: &Map<String, Json>,
1156    schema: &Json,
1157    convert: NumberConversion,
1158) -> Result<Map<String, Json>, CodecError> {
1159    let mut out = fields.clone();
1160    for (name, prop) in props {
1161        if let Some(value) = out.get_mut(name).filter(|v| !v.is_null()) {
1162            *value = convert_value(name, prop, value, schema, convert)?;
1163        }
1164    }
1165    Ok(out)
1166}
1167
1168fn convert_value(
1169    name: &str,
1170    prop: &Json,
1171    value: &Json,
1172    schema: &Json,
1173    convert: NumberConversion,
1174) -> Result<Json, CodecError> {
1175    if let Json::Array(items) = value {
1176        return items
1177            .iter()
1178            .map(|item| convert_value(name, prop, item, schema, convert))
1179            .collect::<Result<Vec<Json>, CodecError>>()
1180            .map(Json::Array);
1181    }
1182    if prop.get("kind").and_then(|k| k.as_str()) == Some("datatype") {
1183        return match number_field(name, prop)? {
1184            Some(field) => convert(&field, value),
1185            None => Ok(value.clone()),
1186        };
1187    }
1188    match value {
1189        Json::Object(map) if !map.contains_key("$ref") => {
1190            let cls_uri = map
1191                .get("$type")
1192                .and_then(|t| t.as_str())
1193                .or_else(|| prop.get("range").and_then(|r| r.as_str()));
1194            let found = match cls_uri {
1195                Some(cls_uri) => class_for(classes_of(schema)?, cls_uri).ok(),
1196                None => None,
1197            };
1198            // A value no class here types is carried as given, as `$extra` is;
1199            // hashing refuses it where it must (runtime#37).
1200            match found {
1201                Some((cls_uri, cls)) => Ok(Json::Object(convert_fields(
1202                    map,
1203                    props_of(cls_uri, cls)?,
1204                    schema,
1205                    convert,
1206                )?)),
1207                None => Ok(value.clone()),
1208            }
1209        }
1210        _ => Ok(value.clone()),
1211    }
1212}
1213
1214fn convert_node(
1215    node: &Node,
1216    schema: &Json,
1217    convert: NumberConversion,
1218) -> Result<Node, CodecError> {
1219    let type_uri = node
1220        .get("$type")
1221        .and_then(|t| t.as_str())
1222        .ok_or_else(|| {
1223            CodecError::Malformed("Cannot convert a node: missing string $type".into())
1224        })?;
1225    let what = format!(
1226        "node {}",
1227        node.get("$id").and_then(|i| i.as_str()).unwrap_or(type_uri)
1228    );
1229    convert_fields(node, props_to_convert(schema, type_uri, &what)?, schema, convert)
1230}
1231
1232/// Serialize a node to its normalized-JSON wire form. `$extra` entries ride as
1233/// sibling fields after the modeled ones; a modeled field wins a name
1234/// collision (`[JsonExtensionData]` semantics). No `$extra` key on the wire.
1235/// Each integer and decimal value is written in its JSON form
1236/// (kanonak-protocol 4.11.0's `numbers-in-json`, runtime#54), through lists and
1237/// embedded values: a wide integer (`integer`, its four sign-restricted forms,
1238/// `long`, `unsignedLong`) and a decimal as a string of its canonical digits, a
1239/// narrow integer (`int` and narrower) as a number. Takes a wide integer as a
1240/// string of digits, an `i64` or `u64` number, or a whole number within
1241/// ±(2^53 − 1); a decimal as a string of digits; a narrow integer as a whole
1242/// number. Anything else, or a value outside its datatype's range, is refused
1243/// with an error ending `[json-number-form]`. `$type` is required, and its
1244/// class is found as [`deserialize`] finds it; an embedded value whose class
1245/// the schema doesn't hold is carried as given. An invalid `$types` envelope
1246/// (at any depth) is a producer bug and fails at emit time, as does a fragment
1247/// reference (runtime#6).
1248pub fn serialize(node: &Node, schema: &Json) -> Result<Node, CodecError> {
1249    let where_ = node
1250        .get("$id")
1251        .or_else(|| node.get("$type"))
1252        .and_then(|v| v.as_str())
1253        .unwrap_or("(node)");
1254    assert_types_envelopes_map(node, &format!("serialize {}", where_))?;
1255    assert_reference_values_map(node, &format!("serialize {}", where_))?;
1256    Ok(project_node(&convert_node(node, schema, write_json_number)?))
1257}
1258
1259/// Parse normalized JSON into a typed node. `$`-envelope keys and fields modeled
1260/// on the node's `$type` stay top-level; every other key is collected into
1261/// `$extra` so a strongly-typed consumer round-trips it losslessly. Requires
1262/// `$type` (the one field that cannot be inferred) and a class in the schema
1263/// that can read it: the exact class, or the same class at a compatible later
1264/// version (runtime#28 — see [`type_matches`] for the same rule). The node
1265/// keeps the `$type` it was WRITTEN with; the producer's bytes are the
1266/// producer's.
1267///
1268/// Each integer and decimal value is read exactly (kanonak-protocol 4.11.0's
1269/// `numbers-in-json`, runtime#54), through lists and embedded values: a wide
1270/// integer from a string, or from a whole JSON number within ±(2^53 − 1); a
1271/// decimal from a string only; a narrow integer from a whole number only. The
1272/// node holds `integer`, its four sign-restricted forms and `decimal` as a
1273/// string of canonical digits, and `long`, `unsignedLong` and the narrow
1274/// integers as a number. A value JSON may already have rounded, or one outside
1275/// its datatype's range, is refused with an error ending `[json-number-form]`.
1276/// An embedded value whose class the schema doesn't hold is carried as given.
1277/// It judges the `Value`s it's given: without serde_json's
1278/// `arbitrary_precision`, `1.0000000000000000001` is already the `f64` `1.0`,
1279/// so a caller that must refuse it parses with it.
1280pub fn deserialize(json_obj: &Node, schema: &Json) -> Result<Node, CodecError> {
1281    convert_node(&parse_node(json_obj, schema)?, schema, read_json_number)
1282}