jsonld_expansion/lib.rs
1//! Implementation of the JSON-LD 1.1
2//! [Expansion algorithm](https://www.w3.org/TR/json-ld11-api/#expansion-algorithms).
3//!
4//! Expansion rewrites a JSON-LD document into a regular form: terms and
5//! compact IRIs become full IRIs, the defaults a context sets (`@base`,
6//! `@vocab`, `@language`...) are applied to every value, and the `@context`
7//! entries themselves disappear. It is the first step of almost every other
8//! JSON-LD algorithm, since it removes the many ways the syntax offers to say
9//! the same thing.
10//!
11//! The algorithm operates on the JSON syntax tree of the `jstrict` crate and
12//! produces the [`ExpandedDocument`] type of the `jsonld-core` crate.
13//!
14//! # Usage
15//!
16//! Expansion is exposed by the [`Expand`] trait, implemented for
17//! [`jstrict::Value`] (a parsed JSON document) and for [`RemoteDocument`]
18//! (a parsed JSON document along with the URL it comes from, used to resolve
19//! relative IRI references).
20//!
21//! Most users will not depend on this crate directly but on the
22//! [`jsonld` crate](https://crates.io/crates/jsonld), which re-exports
23//! [`Expand`] next to the other JSON-LD algorithms.
24use std::hash::Hash;
25
26// Re-exported because they appear in the public `Expand` signatures:
27// standalone users should not need to depend on `jsonld-core` and
28// `jsonld-context-processing` just to name them.
29pub use jsonld_context_processing::Context;
30pub use jsonld_core::{Environment, ExpandedDocument, Loader, RemoteDocument};
31
32use jstrict::Value;
33use rdfx::{
34 BlankIdBuf,
35 vocabulary::{self, BlankIdVocabulary, VocabularyMut},
36};
37
38mod array;
39mod document;
40mod element;
41mod error;
42mod expanded;
43mod literal;
44mod node;
45mod options;
46mod slice;
47mod value;
48mod warning;
49
50pub use error::*;
51pub use expanded::*;
52pub use literal::{LiteralExpansionError, NotALiteral};
53pub use options::*;
54pub use slice::{SliceExpansionError, expand_slice};
55pub use value::InvalidValue;
56pub use warning::*;
57
58pub(crate) use array::expand_array;
59pub(crate) use document::filter_top_level_item;
60pub(crate) use element::{ActiveProperty, ExpandedEntry, expand_element};
61pub(crate) use jsonld_context_processing::algorithm::expand_iri_simple as expand_iri;
62pub(crate) use literal::{GivenLiteralValue, LiteralValue, expand_literal};
63pub(crate) use node::{expand_node, node_id_of_term};
64pub(crate) use value::expand_value;
65
66/// Result of a document expansion, where `E` is the error type of the document
67/// loader used to fetch the remote contexts.
68pub type ExpansionResult<T, B, E> = Result<ExpandedDocument<T, B>, Error<E>>;
69
70/// Handler for the warnings emitted while expanding a JSON-LD document.
71///
72/// This is an alias for a [`jsonld_core::warning::Handler`] of [`Warning`],
73/// automatically implemented by every such handler. The unit type `()` is a
74/// handler that discards every warning.
75pub trait WarningHandler<N: BlankIdVocabulary>: jsonld_core::warning::Handler<N, Warning<N::BlankId>> {}
76
77impl<N: BlankIdVocabulary, H> WarningHandler<N> for H where H: jsonld_core::warning::Handler<N, Warning<N::BlankId>> {}
78
79/// Document expansion.
80///
81/// Provides the functions expanding a JSON-LD document into an
82/// [`ExpandedDocument`]. It is implemented by [`jstrict::Value`], for a
83/// document parsed from JSON, and by [`RemoteDocument`], for a parsed document
84/// carrying the URL it comes from.
85///
86/// # Example
87///
88/// ```
89/// use iri_rs::iri;
90/// use jsonld::{Expand, NoLoader, syntax::Parse};
91///
92/// # #[tokio::main(flavor = "current_thread")]
93/// # async fn main() {
94/// // Parse the input JSON-LD document.
95/// let (json, _) = jsonld::syntax::Value::parse_str(
96/// r#"
97/// {
98/// "@context": { "label": "http://example.org/vocab#label" },
99/// "@id": "http://example.org/book",
100/// "label": "Chapter One"
101/// }
102/// "#)
103/// .unwrap();
104///
105/// // Expanding this document requires no remote context, so a loader that
106/// // fetches nothing will do.
107/// let loader = NoLoader;
108///
109/// let expanded = json.expand(&loader).await.unwrap();
110///
111/// // The `label` term has been replaced by the IRI it is defined as.
112/// let object = expanded.into_iter().next().unwrap();
113/// let label = object
114/// .as_node().unwrap()
115/// .get_any(&iri!("http://example.org/vocab#label")).unwrap()
116/// .as_str().unwrap();
117///
118/// assert_eq!(label, "Chapter One");
119/// # }
120/// ```
121pub trait Expand<Iri> {
122 /// Returns the base URL the document is expanded against when none is
123 /// given explicitly, as in [`Expand::expand`] and [`Expand::expand_with`].
124 ///
125 /// It is also the base URL of the empty initial context those two methods
126 /// build.
127 fn default_base_url(&self) -> Option<&Iri>;
128
129 /// Expands the document, controlling every parameter of the algorithm.
130 ///
131 /// - `vocabulary` interprets the identifiers of the input and mints those
132 /// of the result;
133 /// - `context` is the initial active context;
134 /// - `base_url` is the initial base URL, against which relative IRI
135 /// references are resolved;
136 /// - `loader` fetches the remote documents (contexts, mostly) the input
137 /// refers to;
138 /// - `options` tune the algorithm: processing mode, key expansion policy
139 /// and entry ordering;
140 /// - `warnings_handler` is called for each warning raised during
141 /// expansion, including the warnings raised while processing scoped and
142 /// local `@context`s, which arrive wrapped in
143 /// [`Warning::ContextProcessing`].
144 async fn expand_full<N, L, W>(
145 &self,
146 vocabulary: &mut N,
147 context: Context<Iri, N::BlankId>,
148 base_url: Option<&N::Iri>,
149 loader: &L,
150 options: Options,
151 warnings_handler: W,
152 ) -> ExpansionResult<N::Iri, N::BlankId, L::Error>
153 where
154 N: VocabularyMut<Iri = Iri>,
155 Iri: Clone + Eq + Hash,
156 N::BlankId: Clone + Eq + Hash,
157 L: Loader,
158 W: WarningHandler<N>;
159
160 /// Expands the document, interpreting identifiers with the given
161 /// `vocabulary`.
162 ///
163 /// The algorithm starts from an empty context whose base URL is
164 /// [`Expand::default_base_url`], runs with the default [`Options`] and
165 /// discards warnings. The given `loader` fetches the remote documents
166 /// (contexts, mostly) the input refers to.
167 async fn expand_with<'a, N, L>(&'a self, vocabulary: &'a mut N, loader: &'a L) -> ExpansionResult<Iri, N::BlankId, L::Error>
168 where
169 N: VocabularyMut<Iri = Iri>,
170 Iri: 'a + Clone + Eq + Hash,
171 N::BlankId: 'a + Clone + Eq + Hash,
172 L: Loader,
173 {
174 self.expand_full(
175 vocabulary,
176 Context::<N::Iri, N::BlankId>::new(self.default_base_url().cloned()),
177 self.default_base_url(),
178 loader,
179 Options::default(),
180 (),
181 )
182 .await
183 }
184
185 /// Expands the document, keeping identifiers as `IriBuf` and
186 /// [`BlankIdBuf`] values instead of interning them in a vocabulary.
187 ///
188 /// Otherwise behaves like [`Expand::expand_with`]: the algorithm starts
189 /// from an empty context whose base URL is [`Expand::default_base_url`],
190 /// runs with the default [`Options`] and discards warnings. The given
191 /// `loader` fetches the remote documents (contexts, mostly) the input
192 /// refers to.
193 async fn expand<'a, L>(&'a self, loader: &'a L) -> ExpansionResult<Iri, BlankIdBuf, L::Error>
194 where
195 (): VocabularyMut<Iri = Iri>,
196 Iri: 'a + Clone + Eq + Hash,
197 L: Loader,
198 {
199 self.expand_with(vocabulary::no_vocabulary_mut(), loader).await
200 }
201}
202
203/// A parsed JSON document has no URL of its own, hence no default base URL:
204/// relative IRI references it contains can only be resolved against a base URL
205/// given explicitly to [`Expand::expand_full`].
206impl<Iri> Expand<Iri> for Value {
207 fn default_base_url(&self) -> Option<&Iri> {
208 None
209 }
210
211 async fn expand_full<N, L, W>(
212 &self,
213 vocabulary: &mut N,
214 context: Context<Iri, N::BlankId>,
215 base_url: Option<&Iri>,
216 loader: &L,
217 options: Options,
218 mut warnings_handler: W,
219 ) -> ExpansionResult<Iri, N::BlankId, L::Error>
220 where
221 N: VocabularyMut<Iri = Iri>,
222 Iri: Clone + Eq + Hash,
223 N::BlankId: Clone + Eq + Hash,
224 L: Loader,
225 W: WarningHandler<N>,
226 {
227 document::expand(
228 Environment {
229 vocabulary,
230 loader,
231 warnings: &mut warnings_handler,
232 },
233 self,
234 context,
235 base_url,
236 options,
237 )
238 .await
239 }
240}
241
242/// A remote document knows where it comes from: its own URL is used as default
243/// base URL, so relative IRI references resolve as they would for a consumer
244/// fetching the document.
245impl<Iri> Expand<Iri> for RemoteDocument<Iri> {
246 fn default_base_url(&self) -> Option<&Iri> {
247 self.url()
248 }
249
250 async fn expand_full<N, L, W>(
251 &self,
252 vocabulary: &mut N,
253 context: Context<Iri, N::BlankId>,
254 base_url: Option<&Iri>,
255 loader: &L,
256 options: Options,
257 warnings_handler: W,
258 ) -> ExpansionResult<Iri, N::BlankId, L::Error>
259 where
260 N: VocabularyMut<Iri = Iri>,
261 Iri: Clone + Eq + Hash,
262 N::BlankId: Clone + Eq + Hash,
263 L: Loader,
264 W: WarningHandler<N>,
265 {
266 self.document()
267 .expand_full(vocabulary, context, base_url, loader, options, warnings_handler)
268 .await
269 }
270}