1use super::{Fact, GraphName, ObjectTerm};
4use crate::node_address::{NodeRoot, NodeUri};
5use serde::de::{Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
6use serde_json::{Map, Value};
7use sha2::{Digest, Sha256};
8use std::collections::{BTreeMap, BTreeSet};
9use std::fmt;
10
11const MAX_CONTEXT_IMPORT_DEPTH: usize = 128;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum Coercion {
16 #[default]
18 None,
19 NodeId,
21 Json,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
27pub enum ContextError {
28 #[error("invalid context form")]
30 InvalidForm,
31 #[error("unsupported context keyword: {0}")]
33 UnsupportedKeyword(String),
34 #[error("context term collision: {0}")]
36 TermCollision(String),
37 #[error("context vocabulary collision")]
39 VocabCollision,
40 #[error("protected term redefinition: {0}")]
42 ProtectedTermRedefinition(String),
43 #[error("duplicate context import: {0}")]
45 DuplicateImport(String),
46 #[error("context import cycle: {0}")]
48 ImportCycle(String),
49 #[error("context import depth exceeds {0}")]
51 ImportDepthExceeded(usize),
52 #[error("context path escapes the supplied root")]
54 PathEscape,
55 #[error("context resource unavailable: {0}")]
57 ResourceUnavailable(String),
58 #[error("context resource untrusted: {0}")]
60 ResourceUntrusted(String),
61 #[error("context digest mismatch: {0}")]
63 DigestMismatch(String),
64 #[error("remote context forbidden: {0}")]
66 RemoteForbidden(String),
67 #[error("relative import without a file base")]
69 RelativeImportWithoutBase,
70 #[error("invalid context target: {0}")]
72 InvalidTarget(String),
73 #[error("unbound context key: {0}")]
75 UnboundKey(String),
76 #[error("invalid node-link value")]
78 InvalidNodeLink,
79 #[error("@json coercion requires a declared datatype")]
81 MissingJsonDatatype,
82 #[error("fact object must be @value or @id; structured data requires @json")]
84 InvalidFactObject,
85 #[error("invalid context resource: {0}")]
87 InvalidResource(String),
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91struct Binding {
92 target: String,
93 prefix: bool,
94 coercion: Coercion,
95 protected: bool,
96}
97
98#[derive(Debug, Clone, Default, PartialEq, Eq)]
100pub struct EffectiveContext {
101 terms: BTreeMap<String, Binding>,
102 vocab: Option<String>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct ExpandedKey {
108 uri: NodeUri,
109 coercion: Coercion,
110}
111
112impl ExpandedKey {
113 pub fn uri(&self) -> &NodeUri {
115 &self.uri
116 }
117
118 pub fn coercion(&self) -> Coercion {
120 self.coercion
121 }
122}
123
124impl EffectiveContext {
125 pub fn to_inline_value(&self) -> Value {
129 let mut inline = Map::new();
130 if let Some(vocab) = &self.vocab {
131 inline.insert("@vocab".to_owned(), Value::String(vocab.clone()));
132 }
133 for (name, binding) in &self.terms {
134 let value =
135 if !binding.prefix && !binding.protected && binding.coercion == Coercion::None {
136 Value::String(binding.target.clone())
137 } else {
138 let mut definition = Map::new();
139 definition.insert("@id".to_owned(), Value::String(binding.target.clone()));
140 if binding.prefix {
141 definition.insert("@prefix".to_owned(), Value::Bool(true));
142 }
143 if binding.protected {
144 definition.insert("@protected".to_owned(), Value::Bool(true));
145 }
146 match binding.coercion {
147 Coercion::None => {}
148 Coercion::NodeId => {
149 definition.insert("@type".to_owned(), Value::String("@id".to_owned()));
150 }
151 Coercion::Json => {
152 definition
153 .insert("@type".to_owned(), Value::String("@json".to_owned()));
154 }
155 }
156 Value::Object(definition)
157 };
158 inline.insert(name.clone(), value);
159 }
160 Value::Object(inline)
161 }
162
163 pub fn expand_key(&self, key: &str) -> Result<ExpandedKey, ContextError> {
165 if let Some(binding) = self.terms.get(key) {
166 return Ok(ExpandedKey {
167 uri: parse_uri(&binding.target)?,
168 coercion: binding.coercion,
169 });
170 }
171 if key.starts_with("morphir://ir/") {
172 return Ok(ExpandedKey {
173 uri: parse_uri(key)?,
174 coercion: Coercion::None,
175 });
176 }
177 if let Some((prefix, suffix)) = key.split_once(':')
178 && let Some(binding) = self.terms.get(prefix).filter(|binding| binding.prefix)
179 {
180 return Ok(ExpandedKey {
181 uri: parse_uri(&format!("{}{suffix}", binding.target))?,
182 coercion: Coercion::None,
183 });
184 }
185 if let Some(vocab) = &self.vocab {
186 return Ok(ExpandedKey {
187 uri: parse_uri(&format!("{vocab}{key}"))?,
188 coercion: Coercion::None,
189 });
190 }
191 Err(ContextError::UnboundKey(key.to_owned()))
192 }
193
194 pub fn vocab(&self) -> Option<&str> {
196 self.vocab.as_deref()
197 }
198}
199
200#[derive(Debug, Clone)]
201struct LoadedResource {
202 bytes: Vec<u8>,
203 trusted: bool,
204}
205
206#[derive(Debug, Clone)]
208pub struct ContextResources {
209 root: String,
210 loaded: BTreeMap<String, LoadedResource>,
211}
212
213impl ContextResources {
214 pub fn new(root: impl Into<String>) -> Self {
221 let root = root.into();
222 Self {
223 root: normalize_lexical_path(&root).unwrap_or(root),
224 loaded: BTreeMap::new(),
225 }
226 }
227
228 pub fn insert_local(&mut self, path: impl Into<String>, bytes: Vec<u8>) {
230 let path = path.into();
231 let identity = normalize_lexical_path(&path).unwrap_or(path);
232 self.loaded.insert(
233 identity,
234 LoadedResource {
235 bytes,
236 trusted: true,
237 },
238 );
239 }
240
241 pub fn insert_verified(&mut self, reference: &str, bytes: Vec<u8>, trusted: bool) {
243 self.loaded
244 .insert(reference.to_owned(), LoadedResource { bytes, trusted });
245 }
246}
247
248pub fn resolve_context(
265 parent: Option<&EffectiveContext>,
266 authored: &Value,
267 resources: &ContextResources,
268 base_file: Option<&str>,
269) -> Result<EffectiveContext, ContextError> {
270 let mut seen = BTreeSet::new();
271 let mut active = BTreeSet::new();
272 resolve_value(
273 parent.cloned().unwrap_or_default(),
274 authored,
275 resources,
276 base_file
277 .map(ContextBase::Local)
278 .unwrap_or(ContextBase::Workspace),
279 &mut seen,
280 &mut active,
281 )
282}
283
284pub fn inline_document_contexts(
289 document: &Value,
290 resources: &ContextResources,
291 source_file: Option<&str>,
292) -> Result<Value, ContextError> {
293 let mut result = document.clone();
294 let parent = if let Some(context) = result
295 .get_mut("$meta")
296 .and_then(|meta| meta.get_mut("@context"))
297 {
298 let effective = resolve_context(None, context, resources, source_file)?;
299 *context = effective.to_inline_value();
300 effective
301 } else {
302 EffectiveContext::default()
303 };
304 inline_node_contexts(&mut result, &parent, resources, source_file)?;
305 Ok(result)
306}
307
308fn inline_node_contexts(
309 value: &mut Value,
310 parent: &EffectiveContext,
311 resources: &ContextResources,
312 source_file: Option<&str>,
313) -> Result<(), ContextError> {
314 match value {
315 Value::Object(object) => {
316 for (name, child) in object {
317 if matches!(
318 name.as_str(),
319 "$meta" | "@context" | "facts" | "@graph" | "assertionSources" | "extensions"
320 ) {
321 continue;
322 }
323 if matches!(name.as_str(), "attributes" | "annotations")
324 && let Some(context) = child.get_mut("@context")
325 {
326 let effective = resolve_context(Some(parent), context, resources, source_file)?;
327 *context = effective.to_inline_value();
328 }
329 inline_node_contexts(child, parent, resources, source_file)?;
330 }
331 }
332 Value::Array(items) => {
333 for child in items {
334 inline_node_contexts(child, parent, resources, source_file)?;
335 }
336 }
337 _ => {}
338 }
339 Ok(())
340}
341
342#[derive(Clone, Copy)]
343enum ContextBase<'a> {
344 Workspace,
345 Local(&'a str),
346 ContentAddressed,
347}
348
349pub fn expand_object(
352 coercion: Coercion,
353 value: Value,
354 json_datatype: Option<NodeUri>,
355) -> Result<ObjectTerm, ContextError> {
356 match coercion {
357 Coercion::None => Ok(ObjectTerm::value(value)),
358 Coercion::NodeId => value
359 .as_str()
360 .and_then(|s| NodeUri::parse(s).ok())
361 .map(ObjectTerm::NodeRef)
362 .ok_or(ContextError::InvalidNodeLink),
363 Coercion::Json => json_datatype
364 .map(|uri| ObjectTerm::typed_json(value, uri))
365 .ok_or(ContextError::MissingJsonDatatype),
366 }
367}
368
369pub fn expand_properties<'a>(
374 subject: &NodeUri,
375 properties: impl IntoIterator<Item = (&'a str, &'a Value)>,
376 context: &EffectiveContext,
377 json_datatype: impl Fn(&NodeUri) -> Option<NodeUri>,
378) -> Result<Vec<Fact>, ContextError> {
379 let mut facts = Vec::new();
380 for (key, authored) in properties {
381 let (predicate, objects) = expand_property_objects(key, authored, context, &json_datatype)?;
382 facts.extend(objects.into_iter().map(|object| {
383 Fact::new(
384 subject.clone(),
385 predicate.uri().clone(),
386 object,
387 GraphName::Default,
388 )
389 }));
390 }
391 Ok(facts)
392}
393
394pub(crate) fn expand_property_objects(
398 key: &str,
399 authored: &Value,
400 context: &EffectiveContext,
401 json_datatype: &impl Fn(&NodeUri) -> Option<NodeUri>,
402) -> Result<(ExpandedKey, Vec<ObjectTerm>), ContextError> {
403 let predicate = context.expand_key(key)?;
404 let datatype = || json_datatype(predicate.uri());
405 let objects = if predicate.coercion() == Coercion::Json {
406 vec![expand_object(Coercion::Json, authored.clone(), datatype())?]
407 } else {
408 let values: Vec<&Value> = match authored {
409 Value::Array(items) => items.iter().collect(),
410 _ => vec![authored],
411 };
412 values
413 .into_iter()
414 .filter(|value| !value.is_null())
415 .map(|value| expand_fact_object(predicate.coercion(), value, datatype()))
416 .collect::<Result<Vec<_>, _>>()?
417 };
418 Ok((predicate, objects))
419}
420
421fn expand_fact_object(
422 coercion: Coercion,
423 authored: &Value,
424 json_datatype: Option<NodeUri>,
425) -> Result<ObjectTerm, ContextError> {
426 match authored {
427 Value::Array(_) => Err(ContextError::InvalidFactObject),
428 Value::Object(members) if members.len() == 1 && members.contains_key("@id") => {
429 expand_object(Coercion::NodeId, members["@id"].clone(), None)
430 }
431 Value::Object(members) if members.len() == 1 && members.contains_key("@value") => {
432 match &members["@value"] {
433 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {
434 Ok(ObjectTerm::value(members["@value"].clone()))
435 }
436 _ => Err(ContextError::InvalidFactObject),
437 }
438 }
439 Value::Object(members)
440 if members.len() == 2
441 && members.get("@type") == Some(&Value::String("@json".to_owned()))
442 && members.contains_key("@value") =>
443 {
444 expand_object(Coercion::Json, members["@value"].clone(), json_datatype)
445 }
446 Value::Object(_) => Err(ContextError::InvalidFactObject),
447 _ => expand_object(coercion, authored.clone(), json_datatype),
448 }
449}
450
451fn resolve_value(
452 mut parent: EffectiveContext,
453 authored: &Value,
454 resources: &ContextResources,
455 base: ContextBase<'_>,
456 seen: &mut BTreeSet<String>,
457 active: &mut BTreeSet<String>,
458) -> Result<EffectiveContext, ContextError> {
459 match authored {
460 Value::Object(object) => {
461 apply_inline(&mut parent, object)?;
462 Ok(parent)
463 }
464 Value::String(reference) => {
465 let imported = resolve_import(reference, resources, base, seen, active)?;
466 apply_imports(&mut parent, imported)?;
467 Ok(parent)
468 }
469 Value::Array(items) if !items.is_empty() => {
470 if !items[0].is_string() {
471 return Err(ContextError::InvalidForm);
472 }
473 let mut imports = EffectiveContext::default();
474 let mut inline = None;
475 for (index, item) in items.iter().enumerate() {
476 match item {
477 Value::String(reference) if inline.is_none() => {
478 let imported = resolve_import(reference, resources, base, seen, active)?;
479 merge_import(&mut imports, imported)?;
480 }
481 Value::Object(object) if index + 1 == items.len() && inline.is_none() => {
482 inline = Some(object)
483 }
484 _ => return Err(ContextError::InvalidForm),
485 }
486 }
487 apply_imports(&mut parent, imports)?;
488 if let Some(object) = inline {
489 apply_inline(&mut parent, object)?;
490 }
491 Ok(parent)
492 }
493 _ => Err(ContextError::InvalidForm),
494 }
495}
496
497fn resolve_import(
498 reference: &str,
499 resources: &ContextResources,
500 base: ContextBase<'_>,
501 seen: &mut BTreeSet<String>,
502 active: &mut BTreeSet<String>,
503) -> Result<EffectiveContext, ContextError> {
504 let hashed = reference.starts_with("morphir://context/sha256/");
505 let identity = if hashed {
506 let digest = reference
507 .strip_prefix("morphir://context/sha256/")
508 .expect("hashed reference has the checked prefix");
509 if digest.len() != 64
510 || !digest
511 .bytes()
512 .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
513 {
514 return Err(ContextError::InvalidForm);
515 }
516 reference.to_owned()
517 } else {
518 if reference.contains("://") {
519 return Err(ContextError::RemoteForbidden(reference.to_owned()));
520 }
521 if reference.starts_with('/') {
522 return Err(ContextError::PathEscape);
523 }
524 if !reference.ends_with(".jsonld") {
525 return Err(ContextError::InvalidForm);
526 }
527 let candidate = match base {
528 ContextBase::Workspace if reference.starts_with(&format!("{}/", resources.root)) => {
529 reference.to_owned()
530 }
531 ContextBase::Workspace => format!("{}/{}", resources.root, reference),
532 ContextBase::Local(file) => format!(
533 "{}/{}",
534 file.rsplit_once('/').map(|(dir, _)| dir).unwrap_or("."),
535 reference
536 ),
537 ContextBase::ContentAddressed => return Err(ContextError::RelativeImportWithoutBase),
538 };
539 normalize_path(&candidate, &resources.root)?
540 };
541 if active.contains(&identity) {
542 return Err(ContextError::ImportCycle(identity));
543 }
544 if active.len() >= MAX_CONTEXT_IMPORT_DEPTH {
545 return Err(ContextError::ImportDepthExceeded(MAX_CONTEXT_IMPORT_DEPTH));
546 }
547 if !seen.insert(identity.clone()) {
548 return Err(ContextError::DuplicateImport(identity));
549 }
550 let loaded = resources
551 .loaded
552 .get(&identity)
553 .ok_or_else(|| ContextError::ResourceUnavailable(identity.clone()))?;
554 if hashed {
555 if !loaded.trusted {
556 return Err(ContextError::ResourceUntrusted(identity));
557 }
558 let actual = Sha256::digest(&loaded.bytes)
559 .iter()
560 .map(|byte| format!("{byte:02x}"))
561 .collect::<String>();
562 if !reference.ends_with(&actual) {
563 return Err(ContextError::DigestMismatch(reference.to_owned()));
564 }
565 }
566 let document: Value = serde_json::from_slice::<UniqueValue>(&loaded.bytes)
567 .map(|value| value.0)
568 .map_err(|_| ContextError::InvalidResource(identity.clone()))?;
569 let envelope = document
570 .as_object()
571 .filter(|object| object.len() == 1)
572 .and_then(|object| object.get("@context"))
573 .ok_or_else(|| ContextError::InvalidResource(identity.clone()))?;
574 active.insert(identity.clone());
575 let result = resolve_value(
576 EffectiveContext::default(),
577 envelope,
578 resources,
579 if hashed {
580 ContextBase::ContentAddressed
581 } else {
582 ContextBase::Local(&identity)
583 },
584 seen,
585 active,
586 );
587 active.remove(&identity);
588 result
589}
590
591fn normalize_path(path: &str, root: &str) -> Result<String, ContextError> {
592 let normalized = normalize_lexical_path(path)?;
593 let within_root = match root {
594 "." => !normalized.starts_with('/'),
595 "/" => normalized.starts_with('/'),
596 _ => normalized == root || normalized.starts_with(&format!("{root}/")),
597 };
598 if within_root {
599 Ok(normalized)
600 } else {
601 Err(ContextError::PathEscape)
602 }
603}
604
605fn normalize_lexical_path(path: &str) -> Result<String, ContextError> {
606 let absolute = path.starts_with('/');
607 let mut parts = Vec::new();
608 for part in path.split('/') {
609 match part {
610 "" | "." => {}
611 ".." => {
612 if parts.pop().is_none() {
613 return Err(ContextError::PathEscape);
614 }
615 }
616 _ => parts.push(part),
617 }
618 }
619 let joined = parts.join("/");
620 if absolute {
621 Ok(format!("/{joined}"))
622 } else if joined.is_empty() {
623 Ok(".".to_owned())
624 } else {
625 Ok(joined)
626 }
627}
628
629fn merge_import(
630 target: &mut EffectiveContext,
631 imported: EffectiveContext,
632) -> Result<(), ContextError> {
633 for (key, binding) in imported.terms {
634 if target.terms.get(&key).is_some_and(|old| old != &binding) {
635 return Err(ContextError::TermCollision(key));
636 }
637 target.terms.insert(key, binding);
638 }
639 if let Some(vocab) = imported.vocab {
640 if target.vocab.as_ref().is_some_and(|old| old != &vocab) {
641 return Err(ContextError::VocabCollision);
642 }
643 target.vocab = Some(vocab);
644 }
645 Ok(())
646}
647
648fn apply_imports(
649 target: &mut EffectiveContext,
650 imported: EffectiveContext,
651) -> Result<(), ContextError> {
652 for (key, binding) in imported.terms {
653 if target
654 .terms
655 .get(&key)
656 .is_some_and(|old| old.protected && old != &binding)
657 {
658 return Err(ContextError::ProtectedTermRedefinition(key));
659 }
660 target.terms.insert(key, binding);
661 }
662 if imported.vocab.is_some() {
663 target.vocab = imported.vocab;
664 }
665 Ok(())
666}
667
668fn apply_inline(
669 target: &mut EffectiveContext,
670 object: &Map<String, Value>,
671) -> Result<(), ContextError> {
672 if let Some(key) = object
673 .keys()
674 .find(|key| key.starts_with('@') && key.as_str() != "@vocab")
675 {
676 return Err(ContextError::UnsupportedKeyword(key.clone()));
677 }
678 if let Some(vocab) = object.get("@vocab") {
679 let stem = vocab.as_str().ok_or(ContextError::InvalidForm)?;
680 validate_stem(stem)?;
681 target.vocab = Some(stem.to_owned());
682 }
683 for (key, value) in object.iter().filter(|(key, _)| !key.starts_with('@')) {
686 if value.as_object().and_then(|obj| obj.get("@prefix")) == Some(&Value::Bool(true)) {
687 install_binding(target, key, value)?;
688 }
689 }
690 for (key, value) in object.iter().filter(|(key, value)| {
691 !key.starts_with('@')
692 && value.as_object().and_then(|obj| obj.get("@prefix")) != Some(&Value::Bool(true))
693 }) {
694 install_binding(target, key, value)?;
695 }
696 Ok(())
697}
698
699fn install_binding(
700 target: &mut EffectiveContext,
701 key: &str,
702 value: &Value,
703) -> Result<(), ContextError> {
704 if key.is_empty()
705 || key.chars().any(char::is_whitespace)
706 || key.split_once(':').is_some_and(|(prefix, suffix)| {
707 prefix.is_empty() || suffix.is_empty() || suffix.contains(':')
708 })
709 {
710 return Err(ContextError::InvalidForm);
711 }
712 let (raw, prefix, coercion, protected) = match value {
713 Value::String(raw) => (raw.as_str(), false, Coercion::None, false),
714 Value::Object(object) => {
715 if let Some(member) = object.keys().find(|member| {
716 !matches!(member.as_str(), "@id" | "@type" | "@prefix" | "@protected")
717 }) {
718 return Err(if member.starts_with('@') {
719 ContextError::UnsupportedKeyword(member.clone())
720 } else {
721 ContextError::InvalidForm
722 });
723 }
724 let raw = object
725 .get("@id")
726 .and_then(Value::as_str)
727 .ok_or(ContextError::InvalidForm)?;
728 let prefix = object
729 .get("@prefix")
730 .map(Value::as_bool)
731 .transpose_bool()?
732 .unwrap_or(false);
733 if object.contains_key("@prefix") && !prefix {
734 return Err(ContextError::InvalidForm);
735 }
736 let protected = object
737 .get("@protected")
738 .map(Value::as_bool)
739 .transpose_bool()?
740 .unwrap_or(false);
741 let coercion = match object.get("@type") {
742 None => Coercion::None,
743 Some(Value::String(value)) if value == "@id" => Coercion::NodeId,
744 Some(Value::String(value)) if value == "@json" => Coercion::Json,
745 _ => return Err(ContextError::InvalidForm),
746 };
747 (raw, prefix, coercion, protected)
748 }
749 _ => return Err(ContextError::InvalidForm),
750 };
751 let expanded = if raw.starts_with("morphir://ir/") {
752 raw.to_owned()
753 } else if let Some((head, tail)) = raw.split_once(':') {
754 target
755 .terms
756 .get(head)
757 .filter(|binding| binding.prefix)
758 .map(|binding| format!("{}{tail}", binding.target))
759 .ok_or_else(|| ContextError::InvalidTarget(raw.to_owned()))?
760 } else {
761 return Err(ContextError::InvalidTarget(raw.to_owned()));
762 };
763 if prefix {
764 if coercion != Coercion::None {
765 return Err(ContextError::InvalidForm);
766 }
767 validate_stem(&expanded)?;
768 } else {
769 parse_uri(&expanded)?;
770 }
771 let binding = Binding {
772 target: expanded,
773 prefix,
774 coercion,
775 protected,
776 };
777 if target
778 .terms
779 .get(key)
780 .is_some_and(|old| old.protected && old != &binding)
781 {
782 return Err(ContextError::ProtectedTermRedefinition(key.to_owned()));
783 }
784 target.terms.insert(key.to_owned(), binding);
785 Ok(())
786}
787
788fn parse_uri(raw: &str) -> Result<NodeUri, ContextError> {
789 let uri = NodeUri::parse(raw).map_err(|_| ContextError::InvalidTarget(raw.to_owned()))?;
790 if !matches!(uri.root(), NodeRoot::Type { .. } | NodeRoot::Value { .. })
791 || !uri.steps().is_empty()
792 {
793 return Err(ContextError::InvalidTarget(raw.to_owned()));
794 }
795 Ok(uri)
796}
797
798fn validate_stem(stem: &str) -> Result<(), ContextError> {
799 if !stem.ends_with('/') {
800 return Err(ContextError::InvalidTarget(stem.to_owned()));
801 }
802 parse_uri(&format!("{stem}sample"))
803 .map_err(|_| ContextError::InvalidTarget(stem.to_owned()))?;
804 Ok(())
805}
806
807struct UniqueValue(Value);
810
811impl<'de> Deserialize<'de> for UniqueValue {
812 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
813 deserializer.deserialize_any(UniqueValueVisitor)
814 }
815}
816
817struct UniqueValueVisitor;
818
819impl<'de> Visitor<'de> for UniqueValueVisitor {
820 type Value = UniqueValue;
821
822 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
823 formatter.write_str("a context JSON value without duplicate members")
824 }
825
826 fn visit_bool<E: serde::de::Error>(self, value: bool) -> Result<Self::Value, E> {
827 Ok(UniqueValue(Value::Bool(value)))
828 }
829
830 fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
831 Ok(UniqueValue(Value::Number(value.into())))
832 }
833
834 fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
835 Ok(UniqueValue(Value::Number(value.into())))
836 }
837
838 fn visit_f64<E: serde::de::Error>(self, value: f64) -> Result<Self::Value, E> {
839 serde_json::Number::from_f64(value)
840 .map(|number| UniqueValue(Value::Number(number)))
841 .ok_or_else(|| E::custom("non-finite JSON number"))
842 }
843
844 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
845 Ok(UniqueValue(Value::String(value.to_owned())))
846 }
847
848 fn visit_string<E: serde::de::Error>(self, value: String) -> Result<Self::Value, E> {
849 Ok(UniqueValue(Value::String(value)))
850 }
851
852 fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
853 Ok(UniqueValue(Value::Null))
854 }
855
856 fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
857 Ok(UniqueValue(Value::Null))
858 }
859
860 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
861 let mut values = Vec::new();
862 while let Some(value) = seq.next_element::<UniqueValue>()? {
863 values.push(value.0);
864 }
865 Ok(UniqueValue(Value::Array(values)))
866 }
867
868 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
869 let mut values = Map::new();
870 while let Some(key) = map.next_key::<String>()? {
871 if values.contains_key(&key) {
872 return Err(serde::de::Error::custom(format!(
873 "duplicate context member: {key}"
874 )));
875 }
876 values.insert(key, map.next_value::<UniqueValue>()?.0);
877 }
878 if values.len() == 1
879 && let Some(Value::String(lexeme)) = values.get("$serde_json::private::Number")
880 {
881 return lexeme
882 .parse::<serde_json::Number>()
883 .map(|number| UniqueValue(Value::Number(number)))
884 .map_err(serde::de::Error::custom);
885 }
886 Ok(UniqueValue(Value::Object(values)))
887 }
888}
889
890trait BoolValue {
891 fn transpose_bool(self) -> Result<Option<bool>, ContextError>;
892}
893impl BoolValue for Option<Option<bool>> {
894 fn transpose_bool(self) -> Result<Option<bool>, ContextError> {
895 self.map(|value| value.ok_or(ContextError::InvalidForm))
896 .transpose()
897 }
898}
899
900#[cfg(test)]
901mod tests {
902 use super::UniqueValue;
903 use serde_json::Value;
904
905 #[test]
906 fn imported_json_numbers_keep_their_numeric_kind_and_lexeme() {
907 for source in ["1", "1.25", "18446744073709551616"] {
908 let value = serde_json::from_slice::<UniqueValue>(source.as_bytes())
909 .unwrap()
910 .0;
911 assert!(matches!(value, Value::Number(_)));
912 assert_eq!(value.to_string(), source);
913 }
914 }
915}