Skip to main content

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}