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 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}