1use 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
31const ENVELOPE_KEYS: [&str; 7] = [
35 "$type",
36 "$types",
37 "$id",
38 "$name",
39 "$contentHash",
40 "$version",
41 "$extra",
42];
43
44pub type Node = Map<String, Json>;
46
47#[derive(Debug)]
53#[non_exhaustive]
54pub enum CodecError {
55 Malformed(String),
57 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
82fn 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
101fn 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
114fn 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
122fn 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
136fn 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
209fn 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
239pub 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
269pub(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#[derive(Debug, Clone, PartialEq)]
325#[non_exhaustive]
326pub struct EnumMemberMatch {
327 pub enum_type: String,
329 pub uri: String,
331 pub member: Json,
333}
334
335pub 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
381fn 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
452fn 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
481fn 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
522fn 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 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
556fn 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 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
621fn 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 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 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
685fn 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
698fn 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 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
740pub 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
812pub fn canonical_form(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<String, CodecError> {
814 Ok(canonical_form_pkg(&build_package(nodes, schema, pkg)?)?)
815}
816
817pub fn content_hash(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<String, CodecError> {
819 Ok(canonical_hash_pkg(&build_package(nodes, schema, pkg)?)?)
820}
821
822fn 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
842pub(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 {
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#[derive(Clone, Copy)]
896pub(crate) enum JsonNumberForm {
897 Wide,
900 Decimal,
902 Narrow,
904}
905
906fn json_number_form(key: &str) -> Option<(JsonNumberForm, Option<i128>, Option<i128>)> {
909 xsd_number_form(key.strip_prefix("kanonak.org/core-xsd/")?)
910}
911
912pub(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
936pub(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
947struct 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 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 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
995fn 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
1020fn 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
1037pub(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
1060fn 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
1095fn 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
1132fn 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
1150fn 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 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
1232pub 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
1259pub fn deserialize(json_obj: &Node, schema: &Json) -> Result<Node, CodecError> {
1281 convert_node(&parse_node(json_obj, schema)?, schema, read_json_number)
1282}