Skip to main content

jsonld_context_processing/algorithm/
merged.rs

1use iri_rs::IriRefBuf;
2use jsonld_syntax as syntax;
3use syntax::{
4    Nullable,
5    context::definition::{KeyOrKeyword, KeyOrKeywordRef},
6};
7
8/// A context definition viewed together with the definition it pulls in through
9/// `@import`.
10///
11/// The importing definition wins over the imported one for every entry, which is
12/// what the [specification][1] means by "merging context into import context,
13/// replacing common entries". Reading through this type lets the algorithm treat
14/// the pair as one definition without building a merged copy.
15///
16/// [1]: https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm
17pub struct Merged<'a> {
18    base: &'a syntax::context::Definition,
19    imported: Option<syntax::context::Context>,
20}
21
22impl<'a> Merged<'a> {
23    /// Views `base` together with the context it imports.
24    ///
25    /// `imported` is the already-dereferenced `@import` target, or `None` when
26    /// the definition has no `@import`.
27    #[must_use]
28    pub fn new(base: &'a syntax::context::Definition, imported: Option<syntax::context::Context>) -> Self {
29        Self { base, imported }
30    }
31
32    /// Returns the imported context definition, if there is one.
33    ///
34    /// Yields `None` when nothing was imported, and also when what was imported
35    /// is not a single context definition object — a shape the caller has
36    /// already rejected before reaching here.
37    #[must_use]
38    pub fn imported(&self) -> Option<&syntax::context::Definition> {
39        self.imported.as_ref().and_then(|imported| match imported {
40            syntax::context::Context::One(syntax::ContextEntry::Definition(import_context)) => Some(import_context),
41            _ => None,
42        })
43    }
44
45    /// Returns the `@base` entry, falling back to the imported definition's.
46    pub fn base(&self) -> Option<syntax::Nullable<&IriRefBuf>> {
47        self.base
48            .base
49            .as_ref()
50            .or_else(|| self.imported().and_then(|i| i.base.as_ref()))
51            .map(Nullable::as_ref)
52    }
53
54    /// Returns the `@vocab` entry, falling back to the imported definition's.
55    pub fn vocab(&self) -> Option<syntax::Nullable<&syntax::context::definition::Vocab>> {
56        self.base
57            .vocab
58            .as_ref()
59            .or_else(|| self.imported().and_then(|i| i.vocab.as_ref()))
60            .map(Nullable::as_ref)
61    }
62
63    /// Returns the `@language` entry, falling back to the imported definition's.
64    pub fn language(&self) -> Option<syntax::Nullable<&syntax::LenientLangTagBuf>> {
65        self.base
66            .language
67            .as_ref()
68            .or_else(|| self.imported().and_then(|i| i.language.as_ref()))
69            .map(Nullable::as_ref)
70    }
71
72    /// Returns the `@direction` entry, falling back to the imported
73    /// definition's.
74    #[must_use]
75    pub fn direction(&self) -> Option<syntax::Nullable<syntax::Direction>> {
76        self.base.direction.or_else(|| self.imported().and_then(|i| i.direction))
77    }
78
79    /// Returns the `@protected` entry, falling back to the imported
80    /// definition's.
81    ///
82    /// This is the context-wide default applied to every term it defines, not
83    /// the flag of an individual term definition.
84    #[must_use]
85    pub fn protected(&self) -> Option<bool> {
86        self.base.protected.or_else(|| self.imported().and_then(|i| i.protected))
87    }
88
89    /// Returns the `@type` entry, falling back to the imported definition's.
90    ///
91    /// `@type` is the one keyword a context may redefine, to give it an `@set`
92    /// container or mark it protected.
93    #[must_use]
94    pub fn type_(&self) -> Option<syntax::context::definition::Type> {
95        self.base.type_.or_else(|| self.imported().and_then(|i| i.type_))
96    }
97
98    /// Iterates over every term definition of both definitions, without
99    /// repeating a term the importing definition overrides.
100    #[must_use]
101    pub fn bindings(&self) -> MergedBindings<'_> {
102        MergedBindings {
103            base: self.base,
104            base_bindings: self.base.bindings.iter(),
105            imported_bindings: self.imported().map(|i| i.bindings.iter()),
106        }
107    }
108
109    /// Returns the entry bound to `key`, preferring the importing definition's.
110    #[must_use]
111    pub fn get(&self, key: &syntax::context::definition::KeyOrKeyword) -> Option<syntax::context::definition::EntryValueRef<'_>> {
112        self.base.get(key).or_else(|| self.imported().and_then(|i| i.get(key)))
113    }
114
115    /// Returns `term` as an owned key, or `None` for a term neither definition
116    /// binds.
117    ///
118    /// A bound term is a clone of the key its definition holds, which allocates
119    /// nothing, where building a key from the string could. A keyword
120    /// is always returned: whether a context may set it is decided by the
121    /// caller.
122    #[must_use]
123    pub fn bound_key(&self, term: KeyOrKeywordRef<'_>) -> Option<KeyOrKeyword> {
124        match term {
125            KeyOrKeywordRef::Keyword(keyword) => Some(KeyOrKeyword::Keyword(keyword)),
126            KeyOrKeywordRef::Key(key) => self
127                .base
128                .bindings
129                .get_key(key.as_str())
130                .or_else(|| self.imported().and_then(|i| i.bindings.get_key(key.as_str())))
131                .cloned()
132                .map(KeyOrKeyword::Key),
133        }
134    }
135}
136
137impl<'a> From<&'a syntax::context::Definition> for Merged<'a> {
138    fn from(base: &'a syntax::context::Definition) -> Self {
139        Self { base, imported: None }
140    }
141}
142
143type BindingRef<'a> = (&'a syntax::context::definition::Key, Nullable<&'a syntax::context::TermDefinition>);
144
145/// Iterator over the term definitions of a [`Merged`] context definition.
146///
147/// Yields the imported definition's terms first, skipping any the importing
148/// definition also defines, then the importing definition's own terms. Every
149/// term therefore appears exactly once, bound to the definition that wins.
150pub struct MergedBindings<'a> {
151    base: &'a syntax::context::Definition,
152    base_bindings: syntax::context::definition::BindingsIter<'a>,
153    imported_bindings: Option<syntax::context::definition::BindingsIter<'a>>,
154}
155
156impl<'a> Iterator for MergedBindings<'a> {
157    type Item = BindingRef<'a>;
158
159    fn next(&mut self) -> Option<Self::Item> {
160        match &mut self.imported_bindings {
161            Some(imported_bindings) => {
162                for (key_ref, def) in imported_bindings {
163                    if self.base.get_binding(key_ref.as_str()).is_none() {
164                        return Some((key_ref, def));
165                    }
166                }
167
168                self.base_bindings.next()
169            }
170            None => self.base_bindings.next(),
171        }
172    }
173}