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; 6] = [
28 "$type",
29 "$id",
30 "$name",
31 "$contentHash",
32 "$version",
33 "$extra",
34];
35
36pub type Node = Map<String, Json>;
38
39#[derive(Debug)]
41pub enum CodecError {
42 Malformed(String),
44 Canon(CanonError),
46}
47
48impl std::fmt::Display for CodecError {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 CodecError::Malformed(m) => write!(f, "{}", m),
52 CodecError::Canon(e) => write!(f, "{}", e.0),
53 }
54 }
55}
56
57impl std::error::Error for CodecError {}
58
59impl From<CanonError> for CodecError {
60 fn from(e: CanonError) -> Self {
61 CodecError::Canon(e)
62 }
63}
64
65fn err<T>(msg: impl Into<String>) -> Result<T, CodecError> {
66 Err(CodecError::Malformed(msg.into()))
67}
68
69fn lexical(value: &Json) -> String {
74 match value {
75 Json::Bool(b) => {
76 if *b {
77 "true".to_string()
78 } else {
79 "false".to_string()
80 }
81 }
82 Json::String(s) => s.clone(),
83 Json::Number(n) => n.to_string(),
84 other => other.to_string(),
85 }
86}
87
88fn build_value(prop: &Json, raw: &Json, schema: &Json) -> Result<Value, CodecError> {
91 let kind = prop
92 .get("kind")
93 .and_then(|k| k.as_str())
94 .ok_or_else(|| CodecError::Malformed("schema prop is missing 'kind'".into()))?;
95 if kind == "object" {
96 if let Some(reference) = raw.get("$ref").and_then(|r| r.as_str()) {
98 return Ok(Value::Reference(reference.to_string()));
99 }
100 if let Some(map) = raw.as_object() {
101 return embedded_value(prop, map, schema);
102 }
103 return err(format!(
104 "Object property expects a reference ({{\"$ref\": ...}}) or an embedded \
105 node (a map), got {}",
106 raw
107 ));
108 }
109 let datatype = prop
110 .get("datatype")
111 .and_then(|d| d.as_str())
112 .ok_or_else(|| CodecError::Malformed("datatype prop is missing 'datatype'".into()))?;
113 match carrier_of(datatype) {
114 None => Ok(Value::Raw(lexical(raw))),
115 Some(carrier) => Ok(Value::Typed {
116 carrier,
117 lexical: lexical(raw),
118 }),
119 }
120}
121
122fn embedded_value(
129 prop: &Json,
130 map: &Map<String, Json>,
131 schema: &Json,
132) -> Result<Value, CodecError> {
133 if map.contains_key("$id") {
134 return err(
135 "An embedded value must not carry $id — to point at a named resource, \
136 pass a reference ({\"$ref\": ...}).",
137 );
138 }
139 let explicit_type = map.get("$type").and_then(|t| t.as_str());
140 let cls_uri = match explicit_type.or_else(|| prop.get("range").and_then(|r| r.as_str())) {
141 Some(uri) => uri,
142 None => {
143 return err(
144 "Cannot map embedded value: it carries no $type and the property \
145 declares no range.",
146 )
147 }
148 };
149 let cls = schema
150 .get("classes")
151 .and_then(|c| c.get(cls_uri))
152 .ok_or_else(|| CodecError::Malformed(format!("no schema for embedded type {}", cls_uri)))?;
153 let props = cls
154 .get("props")
155 .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", cls_uri)))?;
156
157 let mut statements = field_statements(map, props, schema)?;
158 if let Some(type_uri) = explicit_type {
159 let type_predicate = schema
160 .get("typePredicate")
161 .and_then(|p| p.as_str())
162 .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
163 statements.push(Statement {
164 predicate: type_predicate.to_string(),
165 value: Value::Reference(type_uri.to_string()),
166 });
167 }
168 let name = map
169 .get("$name")
170 .and_then(|n| n.as_str())
171 .filter(|s| !s.is_empty())
172 .map(|s| s.to_string());
173 Ok(Value::Embedded { name, statements })
174}
175
176fn field_statements(
180 source: &Map<String, Json>,
181 props: &Json,
182 schema: &Json,
183) -> Result<Vec<Statement>, CodecError> {
184 let mut out: Vec<Statement> = Vec::new();
185
186 for (key, raw) in source.iter() {
187 if ENVELOPE_KEYS.contains(&key.as_str()) || raw.is_null() {
188 continue;
189 }
190 match props.get(key) {
191 None => out.push(Statement {
192 predicate: key.clone(),
193 value: Value::Raw(lexical(raw)),
194 }),
195 Some(prop) => {
196 let predicate =
197 prop.get("predicate")
198 .and_then(|p| p.as_str())
199 .ok_or_else(|| {
200 CodecError::Malformed(format!("prop {} is missing 'predicate'", key))
201 })?;
202 let value = match raw.as_array() {
203 Some(items) => {
204 if items.is_empty() {
208 continue;
209 }
210 let mut list = Vec::with_capacity(items.len());
211 for item in items {
212 list.push(build_value(prop, item, schema)?);
213 }
214 Value::List(list)
215 }
216 None => build_value(prop, raw, schema)?,
217 };
218 out.push(Statement {
219 predicate: predicate.to_string(),
220 value,
221 });
222 }
223 }
224 }
225
226 if let Some(extra) = source.get("$extra") {
227 let extra = extra
228 .as_object()
229 .ok_or_else(|| CodecError::Malformed("$extra must be an object".into()))?;
230 for (predicate, raw) in extra.iter() {
231 if raw.is_null() {
232 continue;
233 }
234 out.push(Statement {
235 predicate: predicate.clone(),
236 value: Value::Raw(lexical(raw)),
237 });
238 }
239 }
240 Ok(out)
241}
242
243fn statements(node: &Node, schema: &Json) -> Result<Vec<Statement>, CodecError> {
245 let type_uri = node
246 .get("$type")
247 .and_then(|t| t.as_str())
248 .filter(|s| !s.is_empty())
249 .ok_or_else(|| CodecError::Malformed("node is missing $type".into()))?;
250
251 let classes = schema
252 .get("classes")
253 .ok_or_else(|| CodecError::Malformed("schema is missing 'classes'".into()))?;
254 let cls = classes
255 .get(type_uri)
256 .ok_or_else(|| CodecError::Malformed(format!("no schema for type {}", type_uri)))?;
257 let props = cls
258 .get("props")
259 .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", type_uri)))?;
260
261 let type_predicate = schema
262 .get("typePredicate")
263 .and_then(|p| p.as_str())
264 .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
265
266 let mut out: Vec<Statement> = vec![Statement {
267 predicate: type_predicate.to_string(),
268 value: Value::Reference(type_uri.to_string()),
269 }];
270 out.extend(field_statements(node, props, schema)?);
271 Ok(out)
272}
273
274pub fn build_package(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<Package, CodecError> {
278 let mut subjects: Vec<Subject> = Vec::with_capacity(nodes.len() + 1);
279 for node in nodes {
280 let id = node
281 .get("$id")
282 .and_then(|i| i.as_str())
283 .filter(|s| !s.is_empty())
284 .ok_or_else(|| CodecError::Malformed("node is missing $id".into()))?;
285 subjects.push(Subject {
286 uri: id.to_string(),
287 statements: statements(node, schema)?,
288 });
289 }
290
291 let publisher = pkg
292 .get("publisher")
293 .and_then(|p| p.as_str())
294 .ok_or_else(|| CodecError::Malformed("pkg is missing 'publisher'".into()))?;
295 let package_name = pkg
296 .get("packageName")
297 .and_then(|p| p.as_str())
298 .ok_or_else(|| CodecError::Malformed("pkg is missing 'packageName'".into()))?;
299 let version = pkg
300 .get("version")
301 .and_then(|p| p.as_str())
302 .ok_or_else(|| CodecError::Malformed("pkg is missing 'version'".into()))?;
303
304 let pkg_uri = format!(
305 "{}/{}@{}/{}",
306 publisher, package_name, version, package_name
307 );
308
309 let type_predicate = schema
310 .get("typePredicate")
311 .and_then(|p| p.as_str())
312 .ok_or_else(|| CodecError::Malformed("schema is missing 'typePredicate'".into()))?;
313 let label_predicate = schema
314 .get("labelPredicate")
315 .and_then(|p| p.as_str())
316 .ok_or_else(|| CodecError::Malformed("schema is missing 'labelPredicate'".into()))?;
317 let package_type_uri = schema
318 .get("packageTypeUri")
319 .and_then(|p| p.as_str())
320 .ok_or_else(|| CodecError::Malformed("schema is missing 'packageTypeUri'".into()))?;
321
322 let mut pkg_statements: Vec<Statement> = Vec::new();
323 if let Some(label) = pkg.get("label") {
324 if !label.is_null() {
325 let label = label
326 .as_str()
327 .ok_or_else(|| CodecError::Malformed("pkg label must be a string".into()))?;
328 pkg_statements.push(Statement {
329 predicate: label_predicate.to_string(),
330 value: Value::Raw(label.to_string()),
331 });
332 }
333 }
334 pkg_statements.push(Statement {
335 predicate: type_predicate.to_string(),
336 value: Value::Reference(package_type_uri.to_string()),
337 });
338 subjects.push(Subject {
339 uri: pkg_uri,
340 statements: pkg_statements,
341 });
342
343 Ok(Package { subjects })
344}
345
346pub fn canonical_form(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<String, CodecError> {
348 Ok(canonical_form_pkg(&build_package(nodes, schema, pkg)?)?)
349}
350
351pub fn content_hash(nodes: &[Node], schema: &Json, pkg: &Json) -> Result<String, CodecError> {
353 Ok(canonical_hash_pkg(&build_package(nodes, schema, pkg)?)?)
354}
355
356pub fn serialize(node: &Node) -> Node {
360 let mut out = Map::new();
361 for (key, value) in node.iter() {
362 if key == "$extra" || value.is_null() {
363 continue;
364 }
365 out.insert(key.clone(), value.clone());
366 }
367 if let Some(extra) = node.get("$extra").and_then(|e| e.as_object()) {
368 for (key, value) in extra.iter() {
369 if !value.is_null() && !out.contains_key(key) {
370 out.insert(key.clone(), value.clone());
371 }
372 }
373 }
374 out
375}
376
377pub fn deserialize(json_obj: &Node, schema: &Json) -> Result<Node, CodecError> {
381 let type_uri = json_obj
382 .get("$type")
383 .and_then(|t| t.as_str())
384 .ok_or_else(|| CodecError::Malformed("Cannot deserialize: missing string $type".into()))?;
385
386 let classes = schema
387 .get("classes")
388 .ok_or_else(|| CodecError::Malformed("schema is missing 'classes'".into()))?;
389 let cls = classes.get(type_uri).ok_or_else(|| {
390 CodecError::Malformed(format!(
391 "Cannot deserialize: no schema for type {}",
392 type_uri
393 ))
394 })?;
395 let props = cls
396 .get("props")
397 .ok_or_else(|| CodecError::Malformed(format!("class {} is missing 'props'", type_uri)))?;
398
399 let mut node = Map::new();
400 node.insert("$type".to_string(), Json::String(type_uri.to_string()));
401 let mut extra = Map::new();
402 for (key, value) in json_obj.iter() {
403 if key == "$type" {
404 continue;
405 }
406 if key.starts_with('$') || props.get(key).is_some() {
407 node.insert(key.clone(), value.clone());
408 } else {
409 extra.insert(key.clone(), value.clone());
410 }
411 }
412 if !extra.is_empty() {
413 node.insert("$extra".to_string(), Json::Object(extra));
414 }
415 Ok(node)
416}