1use kanonak_canonical::{
17 canonical_form as canonical_form_pkg, canonical_hash as canonical_hash_pkg, carrier_of,
18 CanonError, Package, Statement, Subject, Value,
19};
20use serde_json::{Map, Value as Json};
21
22mod typed;
23pub use typed::{to_node, KanonakNode, KanonakResource, Ref};
24
25const ENVELOPE_KEYS: [&str; 7] = [
29 "$type",
30 "$types",
31 "$id",
32 "$name",
33 "$contentHash",
34 "$version",
35 "$extra",
36];
37
38pub type Node = Map<String, Json>;
40
41#[derive(Debug)]
43pub enum CodecError {
44 Malformed(String),
46 Canon(CanonError),
48}
49
50impl std::fmt::Display for CodecError {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 CodecError::Malformed(m) => write!(f, "{}", m),
54 CodecError::Canon(e) => write!(f, "{}", e.0),
55 }
56 }
57}
58
59impl std::error::Error for CodecError {}
60
61impl From<CanonError> for CodecError {
62 fn from(e: CanonError) -> Self {
63 CodecError::Canon(e)
64 }
65}
66
67fn err<T>(msg: impl Into<String>) -> Result<T, CodecError> {
68 Err(CodecError::Malformed(msg.into()))
69}
70
71fn lexical(value: &Json) -> String {
76 match value {
77 Json::Bool(b) => {
78 if *b {
79 "true".to_string()
80 } else {
81 "false".to_string()
82 }
83 }
84 Json::String(s) => s.clone(),
85 Json::Number(n) => n.to_string(),
86 other => other.to_string(),
87 }
88}
89
90fn validated_types(
98 map: &Map<String, Json>,
99 where_: &str,
100) -> Result<Option<Vec<String>>, CodecError> {
101 let raw = match map.get("$types") {
102 None | Some(Json::Null) => return Ok(None),
103 Some(v) => v,
104 };
105 let items = match raw.as_array() {
106 Some(items) => items,
107 None => {
108 return err(format!(
109 "{}: $types must be a list of non-empty type URIs",
110 where_
111 ))
112 }
113 };
114 let mut types: Vec<String> = Vec::with_capacity(items.len());
115 for item in items {
116 match item.as_str() {
117 Some(s) if !s.is_empty() => types.push(s.to_string()),
118 _ => {
119 return err(format!(
120 "{}: $types must be a list of non-empty type URIs",
121 where_
122 ))
123 }
124 }
125 }
126 if types.len() < 2 {
127 return err(format!(
128 "{}: $types with {} member(s) is forbidden — a single-typed node carries \
129 only $type (a second encoding of the same content would be hash-ambiguous)",
130 where_,
131 types.len()
132 ));
133 }
134 for pair in types.windows(2) {
135 match pair[0].as_bytes().cmp(pair[1].as_bytes()) {
136 std::cmp::Ordering::Equal => {
137 return err(format!(
138 "{}: $types carries duplicate member {}",
139 where_, pair[1]
140 ))
141 }
142 std::cmp::Ordering::Greater => {
143 return err(format!(
144 "{}: $types is not sorted by UTF-8 bytes ({} sorts after {}) — \
145 ordering is the producer's job, never the reader's",
146 where_, pair[0], pair[1]
147 ))
148 }
149 std::cmp::Ordering::Less => {}
150 }
151 }
152 match map.get("$type").and_then(|t| t.as_str()) {
153 Some(primary) if types.iter().any(|t| t == primary) => Ok(Some(types)),
154 other => err(format!(
155 "{}: $type ({:?}) must be present and a member of $types",
156 where_, other
157 )),
158 }
159}
160
161fn assert_types_envelopes(value: &Json, where_: &str) -> Result<(), CodecError> {
166 match value {
167 Json::Array(items) => {
168 for (i, item) in items.iter().enumerate() {
169 assert_types_envelopes(item, &format!("{}[{}]", where_, i))?;
170 }
171 Ok(())
172 }
173 Json::Object(map) => assert_types_envelopes_map(map, where_),
174 _ => Ok(()),
175 }
176}
177
178fn assert_types_envelopes_map(map: &Map<String, Json>, where_: &str) -> Result<(), CodecError> {
179 if map.contains_key("$types") {
180 validated_types(map, where_)?;
181 }
182 for (key, value) in map.iter() {
183 if key != "$types" {
184 assert_types_envelopes(value, &format!("{}.{}", where_, key))?;
185 }
186 }
187 Ok(())
188}
189
190fn build_value(prop: &Json, raw: &Json, schema: &Json) -> Result<Value, CodecError> {
193 let kind = prop
194 .get("kind")
195 .and_then(|k| k.as_str())
196 .ok_or_else(|| CodecError::Malformed("schema prop is missing 'kind'".into()))?;
197 if kind == "object" {
198 if let Some(reference) = raw.get("$ref").and_then(|r| r.as_str()) {
200 return Ok(Value::Reference(reference.to_string()));
201 }
202 if let Some(map) = raw.as_object() {
203 return embedded_value(prop, map, schema);
204 }
205 return err(format!(
206 "Object property expects a reference ({{\"$ref\": ...}}) or an embedded \
207 node (a map), got {}",
208 raw
209 ));
210 }
211 let datatype = prop
212 .get("datatype")
213 .and_then(|d| d.as_str())
214 .ok_or_else(|| CodecError::Malformed("datatype prop is missing 'datatype'".into()))?;
215 match carrier_of(datatype) {
216 None => Ok(Value::Raw(lexical(raw))),
217 Some(carrier) => Ok(Value::Typed {
218 carrier,
219 lexical: lexical(raw),
220 }),
221 }
222}
223
224fn embedded_value(
231 prop: &Json,
232 map: &Map<String, Json>,
233 schema: &Json,
234) -> Result<Value, CodecError> {
235 if map.contains_key("$id") {
236 return err(
237 "An embedded value must not carry $id — to point at a named resource, \
238 pass a reference ({\"$ref\": ...}).",
239 );
240 }
241 let types = validated_types(map, "Embedded value")?;
242 let explicit_type = map.get("$type").and_then(|t| t.as_str());
243 let cls_uri = match explicit_type.or_else(|| prop.get("range").and_then(|r| r.as_str())) {
244 Some(uri) => uri,
245 None => {
246 return err(
247 "Cannot map embedded value: it carries no $type and the property \
248 declares no range.",
249 )
250 }
251 };
252 let cls = schema
253 .get("classes")
254 .and_then(|c| c.get(cls_uri))
255 .ok_or_else(|| CodecError::Malformed(format!("no schema for embedded type {}", cls_uri)))?;
256 let props = cls
257 .get("props")
258 .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", cls_uri)))?;
259
260 let mut statements = field_statements(map, props, schema)?;
261 if let Some(members) = types {
262 let type_predicate = schema
265 .get("typePredicate")
266 .and_then(|p| p.as_str())
267 .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
268 for member in members {
269 statements.push(Statement {
270 predicate: type_predicate.to_string(),
271 value: Value::Reference(member),
272 });
273 }
274 } else if let Some(type_uri) = explicit_type {
275 let type_predicate = schema
276 .get("typePredicate")
277 .and_then(|p| p.as_str())
278 .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
279 statements.push(Statement {
280 predicate: type_predicate.to_string(),
281 value: Value::Reference(type_uri.to_string()),
282 });
283 }
284 let name = map
285 .get("$name")
286 .and_then(|n| n.as_str())
287 .filter(|s| !s.is_empty())
288 .map(|s| s.to_string());
289 Ok(Value::Embedded { name, statements })
290}
291
292fn field_statements(
296 source: &Map<String, Json>,
297 props: &Json,
298 schema: &Json,
299) -> Result<Vec<Statement>, CodecError> {
300 let mut out: Vec<Statement> = Vec::new();
301
302 for (key, raw) in source.iter() {
303 if ENVELOPE_KEYS.contains(&key.as_str()) || raw.is_null() {
304 continue;
305 }
306 match props.get(key) {
307 None => out.push(Statement {
308 predicate: key.clone(),
309 value: Value::Raw(lexical(raw)),
310 }),
311 Some(prop) => {
312 let predicate =
313 prop.get("predicate")
314 .and_then(|p| p.as_str())
315 .ok_or_else(|| {
316 CodecError::Malformed(format!("prop {} is missing 'predicate'", key))
317 })?;
318 let value = match raw.as_array() {
319 Some(items) => {
320 if items.is_empty() {
324 continue;
325 }
326 let mut list = Vec::with_capacity(items.len());
327 for item in items {
328 list.push(build_value(prop, item, schema)?);
329 }
330 Value::List(list)
331 }
332 None => build_value(prop, raw, schema)?,
333 };
334 out.push(Statement {
335 predicate: predicate.to_string(),
336 value,
337 });
338 }
339 }
340 }
341
342 if let Some(extra) = source.get("$extra") {
343 let extra = extra
344 .as_object()
345 .ok_or_else(|| CodecError::Malformed("$extra must be an object".into()))?;
346 for (predicate, raw) in extra.iter() {
347 if raw.is_null() {
348 continue;
349 }
350 out.push(Statement {
351 predicate: predicate.clone(),
352 value: Value::Raw(lexical(raw)),
353 });
354 }
355 }
356 Ok(out)
357}
358
359fn statements(node: &Node, schema: &Json) -> Result<Vec<Statement>, CodecError> {
361 let types = {
362 let id = node
363 .get("$id")
364 .and_then(|i| i.as_str())
365 .unwrap_or("(no $id)");
366 validated_types(node, &format!("Node {}", id))?
367 };
368 let type_uri = node
369 .get("$type")
370 .and_then(|t| t.as_str())
371 .filter(|s| !s.is_empty())
372 .ok_or_else(|| CodecError::Malformed("node is missing $type".into()))?;
373
374 let classes = schema
375 .get("classes")
376 .ok_or_else(|| CodecError::Malformed("schema is missing 'classes'".into()))?;
377 let cls = classes
378 .get(type_uri)
379 .ok_or_else(|| CodecError::Malformed(format!("no schema for type {}", type_uri)))?;
380 let props = cls
381 .get("props")
382 .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", type_uri)))?;
383
384 let type_predicate = schema
385 .get("typePredicate")
386 .and_then(|p| p.as_str())
387 .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
388
389 let members = types.unwrap_or_else(|| vec![type_uri.to_string()]);
392 let mut out: Vec<Statement> = members
393 .into_iter()
394 .map(|member| Statement {
395 predicate: type_predicate.to_string(),
396 value: Value::Reference(member),
397 })
398 .collect();
399 out.extend(field_statements(node, props, schema)?);
400 Ok(out)
401}
402
403pub fn build_package(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<Package, CodecError> {
407 let mut subjects: Vec<Subject> = Vec::with_capacity(nodes.len() + 1);
408 for node in nodes {
409 let id = node
410 .get("$id")
411 .and_then(|i| i.as_str())
412 .filter(|s| !s.is_empty())
413 .ok_or_else(|| CodecError::Malformed("node is missing $id".into()))?;
414 subjects.push(Subject {
415 uri: id.to_string(),
416 statements: statements(node, schema)?,
417 });
418 }
419
420 let publisher = pkg
421 .get("publisher")
422 .and_then(|p| p.as_str())
423 .ok_or_else(|| CodecError::Malformed("pkg is missing 'publisher'".into()))?;
424 let package_name = pkg
425 .get("packageName")
426 .and_then(|p| p.as_str())
427 .ok_or_else(|| CodecError::Malformed("pkg is missing 'packageName'".into()))?;
428 let version = pkg
429 .get("version")
430 .and_then(|p| p.as_str())
431 .ok_or_else(|| CodecError::Malformed("pkg is missing 'version'".into()))?;
432
433 let pkg_uri = format!(
434 "{}/{}@{}/{}",
435 publisher, package_name, version, package_name
436 );
437
438 let type_predicate = schema
439 .get("typePredicate")
440 .and_then(|p| p.as_str())
441 .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
442 let label_predicate = schema
443 .get("labelPredicate")
444 .and_then(|p| p.as_str())
445 .ok_or_else(|| CodecError::Malformed("schema is missing 'labelPredicate'".into()))?;
446 let package_type_uri = schema
447 .get("packageTypeUri")
448 .and_then(|p| p.as_str())
449 .ok_or_else(|| CodecError::Malformed("schema is missing 'packageTypeUri'".into()))?;
450
451 let mut pkg_statements: Vec<Statement> = Vec::new();
452 if let Some(label) = pkg.get("label") {
453 if !label.is_null() {
454 let label = label
455 .as_str()
456 .ok_or_else(|| CodecError::Malformed("pkg label must be a string".into()))?;
457 pkg_statements.push(Statement {
458 predicate: label_predicate.to_string(),
459 value: Value::Raw(label.to_string()),
460 });
461 }
462 }
463 pkg_statements.push(Statement {
464 predicate: type_predicate.to_string(),
465 value: Value::Reference(package_type_uri.to_string()),
466 });
467 subjects.push(Subject {
468 uri: pkg_uri,
469 statements: pkg_statements,
470 });
471
472 Ok(Package { subjects })
473}
474
475pub fn canonical_form(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<String, CodecError> {
477 Ok(canonical_form_pkg(&build_package(nodes, schema, pkg)?)?)
478}
479
480pub fn content_hash(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<String, CodecError> {
482 Ok(canonical_hash_pkg(&build_package(nodes, schema, pkg)?)?)
483}
484
485pub fn serialize(node: &Node) -> Result<Node, CodecError> {
491 let where_ = node
492 .get("$id")
493 .or_else(|| node.get("$type"))
494 .and_then(|v| v.as_str())
495 .unwrap_or("(node)");
496 assert_types_envelopes_map(node, &format!("serialize {}", where_))?;
497 let mut out = Map::new();
498 for (key, value) in node.iter() {
499 if key == "$extra" || value.is_null() {
500 continue;
501 }
502 out.insert(key.clone(), value.clone());
503 }
504 if let Some(extra) = node.get("$extra").and_then(|e| e.as_object()) {
505 for (key, value) in extra.iter() {
506 if !value.is_null() && !out.contains_key(key) {
507 out.insert(key.clone(), value.clone());
508 }
509 }
510 }
511 Ok(out)
512}
513
514pub fn deserialize(json_obj: &Node, schema: &Json) -> Result<Node, CodecError> {
518 let type_uri = json_obj
519 .get("$type")
520 .and_then(|t| t.as_str())
521 .ok_or_else(|| CodecError::Malformed("Cannot deserialize: missing string $type".into()))?;
522
523 {
528 let where_ = json_obj
529 .get("$id")
530 .and_then(|i| i.as_str())
531 .unwrap_or(type_uri);
532 assert_types_envelopes_map(json_obj, &format!("deserialize {}", where_))?;
533 }
534
535 let classes = schema
536 .get("classes")
537 .ok_or_else(|| CodecError::Malformed("schema is missing 'classes'".into()))?;
538 let cls = classes.get(type_uri).ok_or_else(|| {
539 CodecError::Malformed(format!(
540 "Cannot deserialize: no schema for type {}",
541 type_uri
542 ))
543 })?;
544 let props = cls
545 .get("props")
546 .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", type_uri)))?;
547
548 let mut node = Map::new();
549 node.insert("$type".to_string(), Json::String(type_uri.to_string()));
550 let mut extra = Map::new();
551 for (key, value) in json_obj.iter() {
552 if key == "$type" {
553 continue;
554 }
555 if key.starts_with('$') || props.get(key).is_some() {
556 node.insert(key.clone(), value.clone());
557 } else {
558 extra.insert(key.clone(), value.clone());
559 }
560 }
561 if !extra.is_empty() {
562 node.insert("$extra".to_string(), Json::Object(extra));
563 }
564 Ok(node)
565}