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}