Skip to main content

jsonld_context_processing/
lib.rs

1//! Implementation of the [JSON-LD context processing algorithm][1].
2//!
3//! Context processing turns a `@context` as it appears in a document — a
4//! syntactic [`jsonld_syntax::context::Context`] — into an *active context*: a
5//! [`Context`] holding the term definitions, base IRI, vocabulary mapping,
6//! default language and default base direction that expansion and compaction
7//! consult. Remote contexts referenced by IRI are fetched through a
8//! [`Loader`], and `@import` is resolved by merging the imported definition
9//! underneath the importing one.
10//!
11//! The entry point is the [`Process`] trait, implemented for
12//! [`jsonld_syntax::context::Context`]. [`Process::process`] uses a fresh
13//! active context and the default [`Options`];
14//! [`Process::process_full`] takes every parameter, including the warning
15//! handler. Documents that reuse the same `@context` across many nodes should
16//! go through [`Process::process_with_cache`] with a [`ProcessingCache`], which
17//! collapses repeated processing of an identical context to a single run.
18//!
19//! [1]: https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm
20use algorithm::{Action, RejectVocab};
21pub use jsonld_core::{Context, ProcessingMode, warning};
22use jsonld_core::{ExtractContextError, LoadError, Loader};
23use jsonld_syntax::ErrorCode;
24use rdfx::vocabulary::VocabularyMut;
25use std::{fmt, hash::Hash};
26
27/// The context processing algorithm itself, and the pieces it is built from.
28pub mod algorithm;
29mod cache;
30mod processed;
31mod stack;
32
33pub use cache::ProcessingCache;
34pub use processed::*;
35pub use stack::{MAX_REMOTE_CONTEXTS, ProcessingStack};
36
37/// Warnings that can be raised during context processing.
38///
39/// None of these abort processing; the specification asks a processor to report
40/// them and carry on.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum Warning {
43    /// A term being expanded is not a keyword but has the shape of one (it
44    /// starts with `@` and is otherwise alphabetic), so it expands to null.
45    ///
46    /// The specification allows this to be reported at that point; this crate
47    /// currently drops such terms silently and does not raise it.
48    KeywordLikeTerm(String),
49    /// The `@id` or `@reverse` value of a term definition is not a keyword but
50    /// has the shape of one, so the whole term definition is skipped.
51    KeywordLikeValue(String),
52    /// A value being expanded resolved to neither a well-formed IRI nor a blank
53    /// node identifier, and is retained verbatim as an invalid identifier.
54    MalformedIri(String),
55}
56
57impl fmt::Display for Warning {
58    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
59        match self {
60            Self::KeywordLikeTerm(s) => write!(f, "keyword-like term `{s}`"),
61            Self::KeywordLikeValue(s) => write!(f, "keyword-like value `{s}`"),
62            Self::MalformedIri(s) => write!(f, "malformed IRI `{s}`"),
63        }
64    }
65}
66
67impl<N> contextual::DisplayWithContext<N> for Warning {
68    fn fmt_with(&self, _: &N, f: &mut fmt::Formatter) -> fmt::Result {
69        fmt::Display::fmt(self, f)
70    }
71}
72
73/// Handlers that can receive the [`Warning`]s raised during context processing.
74///
75/// A blanket alias for [`jsonld_core::warning::Handler<N, Warning>`][h], so any
76/// handler of `Warning` qualifies — including [`warning::Print`] to print them
77/// to standard error, `()` to discard them, and [`warning::WarningBuf`] to
78/// collect them for inspection.
79///
80/// [h]: jsonld_core::warning::Handler
81/// [`warning::Print`]: jsonld_core::warning::Print
82/// [`warning::WarningBuf`]: jsonld_core::warning::WarningBuf
83pub trait WarningHandler<N>: jsonld_core::warning::Handler<N, Warning> {}
84
85impl<N, H> WarningHandler<N> for H where H: jsonld_core::warning::Handler<N, Warning> {}
86
87/// Errors that can happen during context processing.
88#[derive(Debug, thiserror::Error)]
89pub enum Error<E = std::convert::Infallible> {
90    #[error("Invalid context nullification")]
91    /// A `null` context tried to clear an active context that holds protected
92    /// term definitions, outside of a scope allowed to override them.
93    InvalidContextNullification,
94
95    #[error("Remote document loading failed")]
96    /// A remote `@context` IRI could not be resolved against the base URL.
97    ///
98    /// Also used as a guard by the synchronous fast path, which refuses to
99    /// continue if it meets a remote `@context` or an `@import` that the
100    /// loader would have to fetch.
101    LoadingDocumentFailed,
102
103    #[error("Processing mode conflict")]
104    /// A context declares `@version` while the processor is running in JSON-LD
105    /// 1.0 mode.
106    ProcessingModeConflict,
107
108    #[error("Invalid `@context` entry")]
109    /// A context entry is not permitted in the current processing mode.
110    ///
111    /// Raised for `@propagate`, `@import` and `@direction`, all of which are
112    /// JSON-LD 1.1 additions, and for an `@import`ed context that itself
113    /// contains `@import`.
114    InvalidContextEntry,
115
116    #[error("Invalid `@import` value")]
117    /// An `@import` value could not be resolved into an IRI against the base
118    /// URL.
119    InvalidImportValue,
120
121    #[error("Invalid remote context")]
122    /// An `@import`ed document's `@context` is not a single context definition
123    /// object.
124    InvalidRemoteContext,
125
126    #[error("Invalid base IRI")]
127    /// A `@base` value is neither an absolute IRI nor resolvable against the
128    /// context's current base IRI.
129    InvalidBaseIri,
130
131    #[error("Invalid vocabulary mapping")]
132    /// A `@vocab` value did not expand to an IRI or blank node identifier.
133    ///
134    /// Also raised in JSON-LD 1.0 mode for a document-relative `@vocab`, which
135    /// only became legal in 1.1.
136    InvalidVocabMapping,
137
138    #[error("Cyclic IRI mapping")]
139    /// A term definition depends on itself, directly or through a chain of
140    /// prefixes or `@id` values.
141    CyclicIriMapping,
142
143    #[error("Invalid term definition")]
144    /// A term definition is malformed or uses a JSON-LD 1.1 entry in 1.0 mode.
145    ///
146    /// Raised for an empty term; for `@protected`, `@context`, `@nest` or
147    /// `@prefix` under JSON-LD 1.0; for `@index` under JSON-LD 1.0 or without an
148    /// `@index` container; for `@prefix` on a term containing `:` or `/`; for
149    /// `@prefix` on a keyword alias; and for a stray `@propagate` inside a term
150    /// definition.
151    InvalidTermDefinition,
152
153    #[error("Keyword redefinition")]
154    /// A context tried to define a keyword as a term.
155    ///
156    /// `@type` is the one exception, and only under JSON-LD 1.1, where it may be
157    /// given `@container: @set` and `@protected`.
158    KeywordRedefinition,
159
160    #[error("Invalid `@protected` value")]
161    /// An `@protected` entry holds something other than a boolean.
162    ///
163    /// Rejected while parsing the context syntax, so context processing itself
164    /// never raises this variant.
165    InvalidProtectedValue,
166
167    #[error("Invalid type mapping")]
168    /// A `@type` entry in a term definition does not name a usable type.
169    ///
170    /// Raised when it expands to something that is not an IRI, `@id`, `@vocab`,
171    /// `@json` or `@none`; when it is `@json` or `@none` under JSON-LD 1.0; and
172    /// when a term with an `@type` container maps to a type other than `@id` or
173    /// `@vocab`.
174    InvalidTypeMapping,
175
176    #[error("Invalid reverse property")]
177    /// An `@reverse` term definition is malformed.
178    ///
179    /// Raised when it also carries `@id` or `@nest`, or when its `@container` is
180    /// anything other than `@set`, `@index` or null.
181    InvalidReverseProperty,
182
183    #[error("Invalid IRI mapping")]
184    /// A term could not be given an IRI mapping.
185    ///
186    /// Raised when an `@id` or `@reverse` value does not expand to an IRI or
187    /// blank node identifier, when a term that looks like a compact IRI or a
188    /// path does not expand back to its own `@id` (JSON-LD 1.1 only), and when a
189    /// term has no `@id` and cannot be resolved through a prefix or the
190    /// vocabulary mapping.
191    InvalidIriMapping,
192
193    #[error("Invalid keyword alias")]
194    /// A term definition tried to alias `@context`, which must never be
195    /// aliasable.
196    InvalidKeywordAlias,
197
198    #[error("Invalid container mapping")]
199    /// A `@container` value is not one of the permitted keywords or
200    /// combinations, or uses `@graph`, `@id`, `@type`, an array or null under
201    /// JSON-LD 1.0.
202    InvalidContainerMapping,
203
204    #[error("Invalid scoped context")]
205    /// A `@context` inside a term definition failed to process.
206    ///
207    /// The underlying error is deliberately replaced by this one, as the
208    /// specification requires. The one exception is
209    /// [`Self::ContextOverflow`], which is passed through unchanged: a
210    /// resource limit says nothing about the scoped context's validity.
211    InvalidScopedContext,
212
213    #[error("Protected term redefinition")]
214    /// A term marked `@protected` was redefined with a different meaning,
215    /// outside of a scope allowed to override protected terms.
216    ProtectedTermRedefinition,
217
218    #[error(transparent)]
219    /// The [`Loader`] failed to fetch a document referenced by a remote
220    /// `@context` or by `@import`.
221    ContextLoadingFailed(#[from] LoadError<E>),
222
223    #[error("Unable to extract JSON-LD context: {0}")]
224    /// A fetched document could not be reduced to a `@context`.
225    ///
226    /// Raised when the document is not JSON-LD, or has no top-level object with
227    /// a `@context` entry.
228    ContextExtractionFailed(ExtractContextError),
229
230    #[error("Recursive context inclusion")]
231    /// A remote context includes itself, directly or indirectly.
232    ///
233    /// JSON-LD 1.0 only. Under 1.1 a context already on the remote-context
234    /// stack is skipped while a scoped context is being validated and
235    /// processed again everywhere else (context processing step 5.2.2), so only
236    /// the [`Self::ContextOverflow`] limit constrains the chain.
237    RecursiveContextInclusion,
238
239    #[error("Context overflow")]
240    /// A processor-defined resource limit on context nesting was exceeded.
241    ///
242    /// Raised when the chain of remote contexts grows past
243    /// [`MAX_REMOTE_CONTEXTS`], which is the limit the [context processing
244    /// algorithm][1] mandates against unbounded remote context chains, and also
245    /// when the synchronous fast path reaches its recursion depth limit on a
246    /// deeply nested inline `@context`.
247    ///
248    /// [1]: <https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm>
249    ContextOverflow,
250
251    #[error("Use of forbidden `@vocab`")]
252    /// A term had to be expanded through the vocabulary mapping while
253    /// [`Options::vocab`] was set to [`Action::Reject`].
254    ForbiddenVocab,
255}
256
257impl<E> From<RejectVocab> for Error<E> {
258    fn from(_value: RejectVocab) -> Self {
259        Self::ForbiddenVocab
260    }
261}
262
263impl<E> Error<E> {
264    /// Returns the JSON-LD error code this error is reported under.
265    pub fn code(&self) -> ErrorCode {
266        match self {
267            Self::InvalidContextNullification => ErrorCode::InvalidContextNullification,
268            Self::LoadingDocumentFailed => ErrorCode::LoadingDocumentFailed,
269            Self::ProcessingModeConflict => ErrorCode::ProcessingModeConflict,
270            Self::InvalidContextEntry => ErrorCode::InvalidContextEntry,
271            Self::InvalidImportValue => ErrorCode::InvalidImportValue,
272            Self::InvalidRemoteContext => ErrorCode::InvalidRemoteContext,
273            Self::InvalidBaseIri => ErrorCode::InvalidBaseIri,
274            Self::InvalidVocabMapping => ErrorCode::InvalidVocabMapping,
275            Self::CyclicIriMapping => ErrorCode::CyclicIriMapping,
276            Self::InvalidTermDefinition => ErrorCode::InvalidTermDefinition,
277            Self::KeywordRedefinition => ErrorCode::KeywordRedefinition,
278            Self::InvalidProtectedValue => ErrorCode::InvalidProtectedValue,
279            Self::InvalidTypeMapping => ErrorCode::InvalidTypeMapping,
280            Self::InvalidReverseProperty => ErrorCode::InvalidReverseProperty,
281            Self::InvalidIriMapping => ErrorCode::InvalidIriMapping,
282            Self::InvalidKeywordAlias => ErrorCode::InvalidKeywordAlias,
283            Self::InvalidContainerMapping => ErrorCode::InvalidContainerMapping,
284            Self::InvalidScopedContext => ErrorCode::InvalidScopedContext,
285            Self::ProtectedTermRedefinition => ErrorCode::ProtectedTermRedefinition,
286            Self::ContextLoadingFailed(_) => ErrorCode::LoadingRemoteContextFailed,
287            Self::ContextExtractionFailed(_) => ErrorCode::LoadingRemoteContextFailed,
288            Self::RecursiveContextInclusion => ErrorCode::RecursiveContextInclusion,
289            Self::ContextOverflow => ErrorCode::ContextOverflow,
290            Self::ForbiddenVocab => ErrorCode::InvalidVocabMapping,
291        }
292    }
293}
294
295/// Result of context processing: the active context paired with the `@context` it
296/// was built from, or the error that stopped the algorithm.
297pub type ProcessingResult<'a, T, B, E> = Result<Processed<'a, T, B>, Error<E>>;
298
299/// Contexts that can be processed into an active context.
300pub trait Process {
301    /// Processes this context on top of `active_context`, taking every parameter
302    /// of the algorithm explicitly.
303    ///
304    /// `base_url` is the URL the context was written at; it is what relative
305    /// `@context` IRIs, `@import` values and `@base` are resolved against.
306    /// `loader` fetches remote contexts and `@import`ed documents. `warnings`
307    /// receives the [`Warning`]s the algorithm reports without aborting.
308    async fn process_full<N, L, W>(
309        &self,
310        vocabulary: &mut N,
311        active_context: &Context<N::Iri, N::BlankId>,
312        loader: &L,
313        base_url: Option<N::Iri>,
314        options: Options,
315        warnings: W,
316    ) -> Result<Processed<'_, N::Iri, N::BlankId>, Error<L::Error>>
317    where
318        N: VocabularyMut,
319        N::Iri: Clone + Eq + Hash,
320        N::BlankId: Clone + PartialEq,
321        L: Loader,
322        W: WarningHandler<N>;
323
324    /// Processes this context as [`Process::process_full`] does, but consults
325    /// `cache` first and stores the result on a miss.
326    ///
327    /// Worth using whenever the same `@context` is processed repeatedly against
328    /// the same active context — the usual shape of a document with many nodes.
329    /// See [`ProcessingCache`] for what makes a cached result reusable and for
330    /// the lifetime the cache has to be given.
331    async fn process_full_with_cache<N, L, W>(
332        &self,
333        vocabulary: &mut N,
334        active_context: &Context<N::Iri, N::BlankId>,
335        loader: &L,
336        base_url: Option<N::Iri>,
337        options: Options,
338        warnings: W,
339        cache: &ProcessingCache<N::Iri, N::BlankId>,
340    ) -> Result<Processed<'_, N::Iri, N::BlankId>, Error<L::Error>>
341    where
342        N: VocabularyMut,
343        N::Iri: Clone + Eq + Hash,
344        N::BlankId: Clone + Eq + Hash,
345        L: Loader,
346        W: WarningHandler<N>;
347
348    /// Processes this context as [`Process::process_full_with_cache`] does,
349    /// printing any warning to standard error.
350    async fn process_with_cache<N, L>(
351        &self,
352        vocabulary: &mut N,
353        active_context: &Context<N::Iri, N::BlankId>,
354        loader: &L,
355        base_url: Option<N::Iri>,
356        options: Options,
357        cache: &ProcessingCache<N::Iri, N::BlankId>,
358    ) -> Result<Processed<'_, N::Iri, N::BlankId>, Error<L::Error>>
359    where
360        N: VocabularyMut,
361        N::Iri: Clone + Eq + Hash,
362        N::BlankId: Clone + Eq + Hash,
363        L: Loader,
364    {
365        self.process_full_with_cache(vocabulary, active_context, loader, base_url, options, warning::Print, cache)
366            .await
367    }
368
369    /// Processes this context as [`Process::process_full`] does, printing any
370    /// warning to standard error.
371    async fn process_with<N, L>(
372        &self,
373        vocabulary: &mut N,
374        active_context: &Context<N::Iri, N::BlankId>,
375        loader: &L,
376        base_url: Option<N::Iri>,
377        options: Options,
378    ) -> ProcessingResult<'_, N::Iri, N::BlankId, L::Error>
379    where
380        N: VocabularyMut,
381        N::Iri: Clone + Eq + Hash,
382        N::BlankId: Clone + PartialEq,
383        L: Loader,
384    {
385        self.process_full(vocabulary, active_context, loader, base_url, options, warning::Print).await
386    }
387
388    /// Processes this context on top of a fresh, empty active context, with the
389    /// default [`Options`], printing any warning to standard error.
390    ///
391    /// This is the entry point for the `@context` of a document, as opposed to a
392    /// scoped context layered on top of an existing one.
393    async fn process<N, L>(&self, vocabulary: &mut N, loader: &L, base_url: Option<N::Iri>) -> Result<Processed<'_, N::Iri, N::BlankId>, Error<L::Error>>
394    where
395        N: VocabularyMut,
396        N::Iri: Clone + Eq + Hash,
397        N::BlankId: Clone + PartialEq,
398        L: Loader,
399    {
400        let active_context = Context::default();
401        self.process_full(vocabulary, &active_context, loader, base_url, Options::default(), warning::Print)
402            .await
403    }
404}
405
406/// Options of the context processing algorithm.
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
408pub struct Options {
409    /// Which version of JSON-LD to enforce.
410    ///
411    /// Under [`ProcessingMode::JsonLd1_0`] the 1.1 additions — `@import`,
412    /// `@propagate`, `@direction`, `@protected`, `@nest`, `@prefix`, scoped
413    /// contexts, document-relative `@vocab`, and the `@graph`/`@id`/`@type`
414    /// containers — are rejected rather than honoured.
415    pub processing_mode: ProcessingMode,
416
417    /// Whether this context may redefine terms marked `@protected`.
418    ///
419    /// Set for a scoped context, which is allowed to replace the definitions of
420    /// the context it is scoped within.
421    pub override_protected: bool,
422
423    /// Whether the processed context stays in effect for nested node objects.
424    ///
425    /// When `false`, the context in force before processing is retained as the
426    /// result's *previous context* and restored on descending into a new node
427    /// object. This is what limits a type-scoped context to the node it was
428    /// introduced on.
429    pub propagate: bool,
430
431    /// What to do when a term has to be expanded through the vocabulary
432    /// mapping.
433    ///
434    /// The default, [`Action::Keep`], is the specified behaviour.
435    /// [`Action::Drop`] and [`Action::Reject`] let a caller refuse terms that
436    /// only resolve because of `@vocab`, which is useful for validating that a
437    /// document's context covers every term it uses.
438    pub vocab: Action,
439}
440
441impl Options {
442    /// Returns these options with `override_protected` set to `true`.
443    #[must_use]
444    pub fn with_override(&self) -> Options {
445        let mut opt = *self;
446        opt.override_protected = true;
447        opt
448    }
449
450    /// Returns these options with `override_protected` set to `false`.
451    #[must_use]
452    pub fn with_no_override(&self) -> Options {
453        let mut opt = *self;
454        opt.override_protected = false;
455        opt
456    }
457
458    /// Returns these options with `propagate` set to `false`.
459    #[must_use]
460    pub fn without_propagation(&self) -> Options {
461        let mut opt = *self;
462        opt.propagate = false;
463        opt
464    }
465}
466
467impl Default for Options {
468    fn default() -> Options {
469        Options {
470            processing_mode: ProcessingMode::default(),
471            override_protected: false,
472            propagate: true,
473            vocab: Action::Keep,
474        }
475    }
476}