1use kanonak_canonical::{
17 canonical_form as canonical_form_pkg, canonical_hash as canonical_hash_pkg, carrier_of,
18 is_readable_by, parse_coordinate, versionless_key, CanonError, CoordinateVersion, Package,
19 Statement, Subject, Value,
20};
21use serde_json::{Map, Value as Json};
22
23mod typed;
24pub use typed::{to_node, KanonakNode, KanonakResource, Ref};
25
26const ENVELOPE_KEYS: [&str; 7] = [
30 "$type",
31 "$types",
32 "$id",
33 "$name",
34 "$contentHash",
35 "$version",
36 "$extra",
37];
38
39pub type Node = Map<String, Json>;
41
42#[derive(Debug)]
44pub enum CodecError {
45 Malformed(String),
47 Canon(CanonError),
49}
50
51impl std::fmt::Display for CodecError {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 CodecError::Malformed(m) => write!(f, "{}", m),
55 CodecError::Canon(e) => write!(f, "{}", e.0),
56 }
57 }
58}
59
60impl std::error::Error for CodecError {}
61
62impl From<CanonError> for CodecError {
63 fn from(e: CanonError) -> Self {
64 CodecError::Canon(e)
65 }
66}
67
68fn err<T>(msg: impl Into<String>) -> Result<T, CodecError> {
69 Err(CodecError::Malformed(msg.into()))
70}
71
72fn lexical(value: &Json) -> String {
77 match value {
78 Json::Bool(b) => {
79 if *b {
80 "true".to_string()
81 } else {
82 "false".to_string()
83 }
84 }
85 Json::String(s) => s.clone(),
86 Json::Number(n) => n.to_string(),
87 other => other.to_string(),
88 }
89}
90
91fn classes_of(schema: &Json) -> Result<&Map<String, Json>, CodecError> {
98 schema
99 .get("classes")
100 .and_then(|c| c.as_object())
101 .ok_or_else(|| CodecError::Malformed("schema is missing 'classes'".into()))
102}
103
104fn versioned_coordinate(uri: &str) -> Option<(String, CoordinateVersion)> {
107 let c = parse_coordinate(uri).ok()?;
108 let version = c.version?;
109 Some((format!("{}/{}/{}", c.publisher, c.package, c.name), version))
110}
111
112fn identity_of(uri: &str) -> Option<String> {
115 versionless_key(uri).ok()
116}
117
118fn version_order(v: CoordinateVersion) -> (u64, u64, u64) {
119 (v.major, v.minor, v.patch)
120}
121
122fn format_version(v: CoordinateVersion) -> String {
123 format!("{}.{}.{}", v.major, v.minor, v.patch)
124}
125
126fn class_for<'a>(
138 classes: &'a Map<String, Json>,
139 type_uri: &str,
140) -> Result<(&'a str, &'a Json), String> {
141 if let Some((key, cls)) = classes.get_key_value(type_uri) {
142 return Ok((key.as_str(), cls));
143 }
144 let unknown = || format!("no schema for type {} [unknown-type]", type_uri);
145 let (key, written) = match versioned_coordinate(type_uri) {
146 Some(found) => found,
147 None => return Err(unknown()),
148 };
149
150 let mut readable: Option<(&'a str, &'a Json, CoordinateVersion)> = None;
151 let mut nearest: Option<CoordinateVersion> = None;
152 for (uri, cls) in classes.iter() {
153 let version = match versioned_coordinate(uri) {
154 Some((k, v)) if k == key => v,
155 _ => continue,
156 };
157 if is_readable_by(written, version) {
158 if readable.map_or(true, |(_, _, best)| {
159 version_order(version) > version_order(best)
160 }) {
161 readable = Some((uri.as_str(), cls, version));
162 }
163 } else if nearest.map_or(true, |n| version_order(version) > version_order(n)) {
164 nearest = Some(version);
165 }
166 }
167 if let Some((uri, cls, _)) = readable {
168 return Ok((uri, cls));
169 }
170 match nearest {
171 Some(r) => Err(incompatible_version(type_uri, written, r)),
172 None => Err(unknown()),
173 }
174}
175
176fn incompatible_version(type_uri: &str, w: CoordinateVersion, r: CoordinateVersion) -> String {
177 let (ws, rs) = (format_version(w), format_version(r));
178 if w.major != r.major {
179 return format!(
180 "{} is written at {}, a different major version than this codec's schema ({}) \
181 [other-major]",
182 type_uri, ws, rs
183 );
184 }
185 if w.major == 0 && w.minor != r.minor {
186 return format!(
187 "{} is written at {}; below 1.0.0 a different minor is a different version line \
188 than this codec's schema ({}) [other-minor-line]",
189 type_uri, ws, rs
190 );
191 }
192 format!(
193 "{} is written at {}, newer than this codec's schema ({}); upgrade the codec to read it \
194 [newer-version]",
195 type_uri, ws, rs
196 )
197}
198
199fn hash_class_for<'a>(
207 schema: &'a Json,
208 type_uri: &str,
209 what: &str,
210) -> Result<&'a Json, CodecError> {
211 let classes = classes_of(schema)?;
212 if let Some(cls) = classes.get(type_uri) {
213 return Ok(cls);
214 }
215 match class_for(classes, type_uri) {
216 Ok((found, _)) => err(format!(
217 "Cannot hash {} {}: this codec's schema has it at {}, and a content hash is \
218 computed with the producer's schema version — hashing needs the exact class \
219 [hash-needs-exact-version]",
220 what, type_uri, found
221 )),
222 Err(_) => err(format!(
223 "no schema for {} {} [unknown-type]",
224 what, type_uri
225 )),
226 }
227}
228
229pub fn type_matches(node: &Node, class_uri: &str, schema: &Json) -> Result<bool, CodecError> {
240 let members: Vec<&str> = match node.get("$types") {
241 None | Some(Json::Null) => node
242 .get("$type")
243 .and_then(|t| t.as_str())
244 .filter(|t| !t.is_empty())
245 .into_iter()
246 .collect(),
247 Some(Json::Array(items)) => items
248 .iter()
249 .map(|t| t.as_str())
250 .collect::<Option<Vec<&str>>>()
251 .ok_or_else(|| {
252 CodecError::Malformed("type_matches: $types must be a list of type URIs".into())
253 })?,
254 Some(_) => return err("type_matches: $types must be a list of type URIs"),
255 };
256 type_matches_members(&members, class_uri, schema)
257}
258
259pub(crate) fn type_matches_members(
262 members: &[&str],
263 class_uri: &str,
264 schema: &Json,
265) -> Result<bool, CodecError> {
266 let target = identity_of(class_uri).ok_or_else(|| {
267 CodecError::Malformed(format!(
268 "type_matches: '{}' is not a class coordinate",
269 class_uri
270 ))
271 })?;
272 let classes = classes_of(schema)?;
273
274 for &member in members {
275 let (key, cls) = match class_for(classes, member) {
276 Ok(found) => found,
277 Err(_) => continue,
278 };
279 let type_uri = cls
280 .get("typeUri")
281 .and_then(|t| t.as_str())
282 .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'typeUri'", key)))?;
283 if identity_of(type_uri).as_deref() == Some(target.as_str()) {
284 return Ok(true);
285 }
286 match cls.get("ancestors") {
287 None | Some(Json::Null) => {}
288 Some(Json::Array(ancestors)) => {
289 for ancestor in ancestors {
290 let ancestor = ancestor.as_str().ok_or_else(|| {
291 CodecError::Malformed(format!(
292 "class {}: 'ancestors' must be a list of class URIs",
293 key
294 ))
295 })?;
296 if identity_of(ancestor).as_deref() == Some(target.as_str()) {
297 return Ok(true);
298 }
299 }
300 }
301 Some(_) => {
302 return err(format!(
303 "class {}: 'ancestors' must be a list of class URIs",
304 key
305 ))
306 }
307 }
308 }
309 Ok(false)
310}
311
312#[derive(Debug, Clone, PartialEq)]
314pub struct EnumMemberMatch {
315 pub enum_type: String,
317 pub uri: String,
319 pub member: Json,
321}
322
323pub fn enum_member(schema: &Json, reference: &str) -> Option<EnumMemberMatch> {
330 let enums = schema.get("enums").and_then(|e| e.as_object())?;
331 for (enum_type, e) in enums.iter() {
332 if let Some(member) = e.get("members").and_then(|m| m.get(reference)) {
333 return Some(EnumMemberMatch {
334 enum_type: enum_type.clone(),
335 uri: reference.to_string(),
336 member: member.clone(),
337 });
338 }
339 }
340 let (key, written) = versioned_coordinate(reference)?;
341 let mut best: Option<(&str, &str, &Json, CoordinateVersion)> = None;
342 for (enum_type, e) in enums.iter() {
343 let members = match e.get("members").and_then(|m| m.as_object()) {
344 Some(members) => members,
345 None => continue,
346 };
347 for (uri, member) in members.iter() {
348 let version = match versioned_coordinate(uri) {
349 Some((k, v)) if k == key => v,
350 _ => continue,
351 };
352 if !is_readable_by(written, version) {
353 continue;
354 }
355 if best.map_or(true, |(_, _, _, b)| {
356 version_order(version) > version_order(b)
357 }) {
358 best = Some((enum_type.as_str(), uri.as_str(), member, version));
359 }
360 }
361 }
362 best.map(|(enum_type, uri, member, _)| EnumMemberMatch {
363 enum_type: enum_type.to_string(),
364 uri: uri.to_string(),
365 member: member.clone(),
366 })
367}
368
369fn validated_types(
377 map: &Map<String, Json>,
378 where_: &str,
379) -> Result<Option<Vec<String>>, CodecError> {
380 let raw = match map.get("$types") {
381 None | Some(Json::Null) => return Ok(None),
382 Some(v) => v,
383 };
384 let items = match raw.as_array() {
385 Some(items) => items,
386 None => {
387 return err(format!(
388 "{}: $types must be a list of non-empty type URIs",
389 where_
390 ))
391 }
392 };
393 let mut types: Vec<String> = Vec::with_capacity(items.len());
394 for item in items {
395 match item.as_str() {
396 Some(s) if !s.is_empty() => types.push(s.to_string()),
397 _ => {
398 return err(format!(
399 "{}: $types must be a list of non-empty type URIs",
400 where_
401 ))
402 }
403 }
404 }
405 if types.len() < 2 {
406 return err(format!(
407 "{}: $types with {} member(s) is forbidden — a single-typed node carries \
408 only $type (a second encoding of the same content would be hash-ambiguous)",
409 where_,
410 types.len()
411 ));
412 }
413 for pair in types.windows(2) {
414 match pair[0].as_bytes().cmp(pair[1].as_bytes()) {
415 std::cmp::Ordering::Equal => {
416 return err(format!(
417 "{}: $types carries duplicate member {}",
418 where_, pair[1]
419 ))
420 }
421 std::cmp::Ordering::Greater => {
422 return err(format!(
423 "{}: $types is not sorted by UTF-8 bytes ({} sorts after {}) — \
424 ordering is the producer's job, never the reader's",
425 where_, pair[0], pair[1]
426 ))
427 }
428 std::cmp::Ordering::Less => {}
429 }
430 }
431 match map.get("$type").and_then(|t| t.as_str()) {
432 Some(primary) if types.iter().any(|t| t == primary) => Ok(Some(types)),
433 other => err(format!(
434 "{}: $type ({:?}) must be present and a member of $types",
435 where_, other
436 )),
437 }
438}
439
440fn assert_types_envelopes(value: &Json, where_: &str) -> Result<(), CodecError> {
445 match value {
446 Json::Array(items) => {
447 for (i, item) in items.iter().enumerate() {
448 assert_types_envelopes(item, &format!("{}[{}]", where_, i))?;
449 }
450 Ok(())
451 }
452 Json::Object(map) => assert_types_envelopes_map(map, where_),
453 _ => Ok(()),
454 }
455}
456
457fn assert_types_envelopes_map(map: &Map<String, Json>, where_: &str) -> Result<(), CodecError> {
458 if map.contains_key("$types") {
459 validated_types(map, where_)?;
460 }
461 for (key, value) in map.iter() {
462 if key != "$types" {
463 assert_types_envelopes(value, &format!("{}.{}", where_, key))?;
464 }
465 }
466 Ok(())
467}
468
469fn build_value(prop: &Json, raw: &Json, schema: &Json) -> Result<Value, CodecError> {
472 let kind = prop
473 .get("kind")
474 .and_then(|k| k.as_str())
475 .ok_or_else(|| CodecError::Malformed("schema prop is missing 'kind'".into()))?;
476 if kind == "object" {
477 if let Some(reference) = raw.get("$ref").and_then(|r| r.as_str()) {
479 return Ok(Value::Reference(reference.to_string()));
480 }
481 if let Some(map) = raw.as_object() {
482 return embedded_value(prop, map, schema);
483 }
484 return err(format!(
485 "Object property expects a reference ({{\"$ref\": ...}}) or an embedded \
486 node (a map), got {}",
487 raw
488 ));
489 }
490 let datatype = prop
491 .get("datatype")
492 .and_then(|d| d.as_str())
493 .ok_or_else(|| CodecError::Malformed("datatype prop is missing 'datatype'".into()))?;
494 match carrier_of(datatype) {
495 None => Ok(Value::Raw(lexical(raw))),
496 Some(carrier) => Ok(Value::Typed {
497 carrier,
498 lexical: lexical(raw),
499 }),
500 }
501}
502
503fn embedded_value(
510 prop: &Json,
511 map: &Map<String, Json>,
512 schema: &Json,
513) -> Result<Value, CodecError> {
514 if map.contains_key("$id") {
515 return err(
516 "An embedded value must not carry $id — to point at a named resource, \
517 pass a reference ({\"$ref\": ...}).",
518 );
519 }
520 let types = validated_types(map, "Embedded value")?;
521 let explicit_type = map.get("$type").and_then(|t| t.as_str());
522 let cls_uri = match explicit_type.or_else(|| prop.get("range").and_then(|r| r.as_str())) {
523 Some(uri) => uri,
524 None => {
525 return err(
526 "Cannot map embedded value: it carries no $type and the property \
527 declares no range.",
528 )
529 }
530 };
531 let cls = hash_class_for(schema, cls_uri, "embedded type")?;
532 let props = cls
533 .get("props")
534 .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", cls_uri)))?;
535
536 let mut statements = field_statements(map, props, schema)?;
537 if let Some(members) = types {
538 let type_predicate = schema
541 .get("typePredicate")
542 .and_then(|p| p.as_str())
543 .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
544 for member in members {
545 statements.push(Statement {
546 predicate: type_predicate.to_string(),
547 value: Value::Reference(member),
548 });
549 }
550 } else if let Some(type_uri) = explicit_type {
551 let type_predicate = schema
552 .get("typePredicate")
553 .and_then(|p| p.as_str())
554 .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
555 statements.push(Statement {
556 predicate: type_predicate.to_string(),
557 value: Value::Reference(type_uri.to_string()),
558 });
559 }
560 let name = map
561 .get("$name")
562 .and_then(|n| n.as_str())
563 .filter(|s| !s.is_empty())
564 .map(|s| s.to_string());
565 Ok(Value::Embedded { name, statements })
566}
567
568fn field_statements(
572 source: &Map<String, Json>,
573 props: &Json,
574 schema: &Json,
575) -> Result<Vec<Statement>, CodecError> {
576 let mut out: Vec<Statement> = Vec::new();
577
578 for (key, raw) in source.iter() {
579 if ENVELOPE_KEYS.contains(&key.as_str()) || raw.is_null() {
580 continue;
581 }
582 match props.get(key) {
583 None => out.push(Statement {
584 predicate: key.clone(),
585 value: Value::Raw(lexical(raw)),
586 }),
587 Some(prop) => {
588 let predicate =
589 prop.get("predicate")
590 .and_then(|p| p.as_str())
591 .ok_or_else(|| {
592 CodecError::Malformed(format!("prop {} is missing 'predicate'", key))
593 })?;
594 let value = match raw.as_array() {
595 Some(items) => {
596 if items.is_empty() {
600 continue;
601 }
602 let mut list = Vec::with_capacity(items.len());
603 for item in items {
604 list.push(build_value(prop, item, schema)?);
605 }
606 Value::List(list)
607 }
608 None => build_value(prop, raw, schema)?,
609 };
610 out.push(Statement {
611 predicate: predicate.to_string(),
612 value,
613 });
614 }
615 }
616 }
617
618 if let Some(extra) = source.get("$extra") {
619 let extra = extra
620 .as_object()
621 .ok_or_else(|| CodecError::Malformed("$extra must be an object".into()))?;
622 for (predicate, raw) in extra.iter() {
623 if raw.is_null() {
624 continue;
625 }
626 out.push(Statement {
627 predicate: predicate.clone(),
628 value: Value::Raw(lexical(raw)),
629 });
630 }
631 }
632 Ok(out)
633}
634
635fn statements(node: &Node, schema: &Json) -> Result<Vec<Statement>, CodecError> {
637 let types = {
638 let id = node
639 .get("$id")
640 .and_then(|i| i.as_str())
641 .unwrap_or("(no $id)");
642 validated_types(node, &format!("Node {}", id))?
643 };
644 let type_uri = node
645 .get("$type")
646 .and_then(|t| t.as_str())
647 .filter(|s| !s.is_empty())
648 .ok_or_else(|| CodecError::Malformed("node is missing $type".into()))?;
649
650 let cls = hash_class_for(schema, type_uri, "type")?;
651 let props = cls
652 .get("props")
653 .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", type_uri)))?;
654
655 let type_predicate = schema
656 .get("typePredicate")
657 .and_then(|p| p.as_str())
658 .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
659
660 let members = types.unwrap_or_else(|| vec![type_uri.to_string()]);
663 let mut out: Vec<Statement> = members
664 .into_iter()
665 .map(|member| Statement {
666 predicate: type_predicate.to_string(),
667 value: Value::Reference(member),
668 })
669 .collect();
670 out.extend(field_statements(node, props, schema)?);
671 Ok(out)
672}
673
674pub fn build_package(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<Package, CodecError> {
678 let mut subjects: Vec<Subject> = Vec::with_capacity(nodes.len() + 1);
679 for node in nodes {
680 let id = node
681 .get("$id")
682 .and_then(|i| i.as_str())
683 .filter(|s| !s.is_empty())
684 .ok_or_else(|| CodecError::Malformed("node is missing $id".into()))?;
685 subjects.push(Subject {
686 uri: id.to_string(),
687 statements: statements(node, schema)?,
688 });
689 }
690
691 let publisher = pkg
692 .get("publisher")
693 .and_then(|p| p.as_str())
694 .ok_or_else(|| CodecError::Malformed("pkg is missing 'publisher'".into()))?;
695 let package_name = pkg
696 .get("packageName")
697 .and_then(|p| p.as_str())
698 .ok_or_else(|| CodecError::Malformed("pkg is missing 'packageName'".into()))?;
699 let version = pkg
700 .get("version")
701 .and_then(|p| p.as_str())
702 .ok_or_else(|| CodecError::Malformed("pkg is missing 'version'".into()))?;
703
704 let pkg_uri = format!(
705 "{}/{}@{}/{}",
706 publisher, package_name, version, package_name
707 );
708
709 let type_predicate = schema
710 .get("typePredicate")
711 .and_then(|p| p.as_str())
712 .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
713 let label_predicate = schema
714 .get("labelPredicate")
715 .and_then(|p| p.as_str())
716 .ok_or_else(|| CodecError::Malformed("schema is missing 'labelPredicate'".into()))?;
717 let package_type_uri = schema
718 .get("packageTypeUri")
719 .and_then(|p| p.as_str())
720 .ok_or_else(|| CodecError::Malformed("schema is missing 'packageTypeUri'".into()))?;
721
722 let mut pkg_statements: Vec<Statement> = Vec::new();
723 if let Some(label) = pkg.get("label") {
724 if !label.is_null() {
725 let label = label
726 .as_str()
727 .ok_or_else(|| CodecError::Malformed("pkg label must be a string".into()))?;
728 pkg_statements.push(Statement {
729 predicate: label_predicate.to_string(),
730 value: Value::Raw(label.to_string()),
731 });
732 }
733 }
734 pkg_statements.push(Statement {
735 predicate: type_predicate.to_string(),
736 value: Value::Reference(package_type_uri.to_string()),
737 });
738 subjects.push(Subject {
739 uri: pkg_uri,
740 statements: pkg_statements,
741 });
742
743 Ok(Package { subjects })
744}
745
746pub fn canonical_form(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<String, CodecError> {
748 Ok(canonical_form_pkg(&build_package(nodes, schema, pkg)?)?)
749}
750
751pub fn content_hash(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<String, CodecError> {
753 Ok(canonical_hash_pkg(&build_package(nodes, schema, pkg)?)?)
754}
755
756pub fn serialize(node: &Node) -> Result<Node, CodecError> {
762 let where_ = node
763 .get("$id")
764 .or_else(|| node.get("$type"))
765 .and_then(|v| v.as_str())
766 .unwrap_or("(node)");
767 assert_types_envelopes_map(node, &format!("serialize {}", where_))?;
768 let mut out = Map::new();
769 for (key, value) in node.iter() {
770 if key == "$extra" || value.is_null() {
771 continue;
772 }
773 out.insert(key.clone(), value.clone());
774 }
775 if let Some(extra) = node.get("$extra").and_then(|e| e.as_object()) {
776 for (key, value) in extra.iter() {
777 if !value.is_null() && !out.contains_key(key) {
778 out.insert(key.clone(), value.clone());
779 }
780 }
781 }
782 Ok(out)
783}
784
785pub fn deserialize(json_obj: &Node, schema: &Json) -> Result<Node, CodecError> {
794 let type_uri = json_obj
795 .get("$type")
796 .and_then(|t| t.as_str())
797 .ok_or_else(|| CodecError::Malformed("Cannot deserialize: missing string $type".into()))?;
798
799 {
804 let where_ = json_obj
805 .get("$id")
806 .and_then(|i| i.as_str())
807 .unwrap_or(type_uri);
808 assert_types_envelopes_map(json_obj, &format!("deserialize {}", where_))?;
809 }
810
811 let (cls_uri, cls) = class_for(classes_of(schema)?, type_uri)
812 .map_err(|e| CodecError::Malformed(format!("Cannot deserialize: {}", e)))?;
813 let props = cls
814 .get("props")
815 .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", cls_uri)))?;
816
817 let mut node = Map::new();
818 node.insert("$type".to_string(), Json::String(type_uri.to_string()));
819 let mut extra = Map::new();
820 for (key, value) in json_obj.iter() {
821 if key == "$type" {
822 continue;
823 }
824 if key.starts_with('$') || props.get(key).is_some() {
825 node.insert(key.clone(), value.clone());
826 } else {
827 extra.insert(key.clone(), value.clone());
828 }
829 }
830 if !extra.is_empty() {
831 node.insert("$extra".to_string(), Json::Object(extra));
832 }
833 Ok(node)
834}