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