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