jsonld_context_processing/algorithm/mod.rs
1//! # Two implementations, one algorithm: keep `sync.rs` in step
2//!
3//! This module, together with `define.rs` and `iri.rs`, holds the `async`
4//! implementation of the [context processing algorithm][1] — the one that can
5//! call the [`Loader`] to fetch remote contexts and `@import`ed documents.
6//! `sync.rs`, in this same directory, holds a second, hand-maintained
7//! implementation of the *same* algorithm with `async`, `.await` and the boxed
8//! `Recursion` stripped out and recursion made direct. It is used as a fast path whenever
9//! `requires_loader` proves the input contains no remote `@context` IRI and no
10//! `@import`, which is the common case in real payloads.
11//!
12//! Which of the two runs is invisible to callers: for any input both must
13//! produce the same active context and the same error. **A change to the
14//! algorithm here therefore has to be mirrored in `sync.rs`, and a change there
15//! mirrored back here.** Nothing enforces this — the two are separate function
16//! bodies with no shared code, so a fix applied to only one silently makes the
17//! result depend on whether the input happened to reference a remote context.
18//! The mirrored pairs are:
19//!
20//! | this module | `sync.rs` |
21//! |---|---|
22//! | `process_context` (`mod.rs`) | `process_context_sync` |
23//! | `define` (`define.rs`) | `define_sync` |
24//! | `expand_iri_with` (`iri.rs`) | `expand_iri_with_sync` |
25//!
26//! `sync.rs` does guard the one class of divergence it can detect — reaching a
27//! code path that would need the loader — by returning
28//! [`Error::LoadingDocumentFailed`]. That protects against `requires_loader`
29//! drifting out of step with the algorithm body; it says nothing about the two
30//! implementations otherwise agreeing.
31//!
32//! [1]: https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm
33
34use std::{hash::Hash, sync::Arc};
35
36use crate::{Error, Options, Process, Processed, ProcessingCache, ProcessingResult, ProcessingStack, WarningHandler, cache::cache_key};
37use iri_rs::{Iri, IriBuf};
38use jsonld_core::{Context, Dereference, Environment, ExtractContext, Loader, ProcessingMode, Term};
39use jsonld_syntax::{self as syntax, Nullable, native_stack::Recursion};
40use rdfx::vocabulary::VocabularyMut;
41
42mod define;
43mod iri;
44mod merged;
45mod sync;
46
47pub use define::*;
48pub use iri::*;
49pub use merged::*;
50use sync::{process_context_sync, requires_loader};
51use syntax::context::definition::KeyOrKeywordRef;
52
53/// Checks an `@vocab` value against the JSON-LD 1.0 rule.
54///
55/// 1.0 required the value to be an absolute IRI or a blank node identifier;
56/// document-relative resolution only arrived in 1.1. A bare `_:` prefix counts,
57/// as it is what makes `@vocab` map properties to blank nodes.
58pub(crate) fn is_valid_vocab_json_ld_1_0(value: &syntax::context::definition::Vocab) -> bool {
59 value.as_str().starts_with("_:") || Iri::new(value.as_str()).is_ok()
60}
61
62impl Process for syntax::context::Context {
63 async fn process_full<N, L, W>(
64 &self,
65 vocabulary: &mut N,
66 active_context: &Context<N::Iri, N::BlankId>,
67 loader: &L,
68 base_url: Option<N::Iri>,
69 options: Options,
70 mut warnings: W,
71 ) -> Result<Processed<'_, N::Iri, N::BlankId>, Error<L::Error>>
72 where
73 N: VocabularyMut,
74 N::Iri: Clone + Eq + Hash,
75 N::BlankId: Clone + PartialEq,
76 L: Loader,
77 W: WarningHandler<N>,
78 {
79 // Fast path: no remote `@context` IRIs and no `@import` anywhere in
80 // the tree means the entire algorithm runs synchronously — no
81 // boxed `Recursion` per level, no future state machine.
82 if !requires_loader(self) {
83 return process_context_sync(
84 Environment {
85 vocabulary,
86 loader,
87 warnings: &mut warnings,
88 },
89 active_context,
90 self,
91 ProcessingStack::default(),
92 base_url,
93 options,
94 0,
95 );
96 }
97
98 process_context(
99 Environment {
100 vocabulary,
101 loader,
102 warnings: &mut warnings,
103 },
104 active_context,
105 self,
106 ProcessingStack::default(),
107 base_url,
108 options,
109 true,
110 )
111 .await
112 }
113
114 async fn process_full_with_cache<N, L, W>(
115 &self,
116 vocabulary: &mut N,
117 active_context: &Context<N::Iri, N::BlankId>,
118 loader: &L,
119 base_url: Option<N::Iri>,
120 options: Options,
121 mut warnings: W,
122 cache: &ProcessingCache<N::Iri, N::BlankId>,
123 ) -> Result<Processed<'_, N::Iri, N::BlankId>, Error<L::Error>>
124 where
125 N: VocabularyMut,
126 N::Iri: Clone + Eq + Hash,
127 N::BlankId: Clone + Eq + Hash,
128 L: Loader,
129 W: WarningHandler<N>,
130 {
131 let key = cache_key(active_context, self, base_url.as_ref(), options);
132
133 if let Some(cached) = cache.get(key, active_context, self, base_url.as_ref(), options) {
134 return Ok(Processed::new(self, (*cached).clone()));
135 }
136
137 // Retained for the cache entry: `base_url` is moved into processing.
138 let entry_active = active_context.clone();
139 let entry_base_url = base_url.clone();
140
141 let processed = if requires_loader(self) {
142 process_context(
143 Environment {
144 vocabulary,
145 loader,
146 warnings: &mut warnings,
147 },
148 active_context,
149 self,
150 ProcessingStack::default(),
151 base_url,
152 options,
153 true,
154 )
155 .await?
156 } else {
157 process_context_sync(
158 Environment {
159 vocabulary,
160 loader,
161 warnings: &mut warnings,
162 },
163 active_context,
164 self,
165 ProcessingStack::default(),
166 base_url,
167 options,
168 0,
169 )?
170 };
171
172 cache.insert(key, entry_active, self.clone(), entry_base_url, options, Arc::new(processed.processed.clone()));
173 Ok(processed)
174 }
175}
176
177/// Resolves `iri_ref` against `base_iri` and interns the result in `vocabulary`.
178///
179/// With no base IRI, `iri_ref` must already be an absolute IRI. Returns `None`
180/// when the reference cannot be turned into one either way.
181fn resolve_iri<V, I>(vocabulary: &mut V, iri_ref: iri_rs::IriRef<&str>, base_iri: Option<&I>) -> Option<I>
182where
183 V: rdfx::vocabulary::IriVocabularyMut<Iri = I>,
184{
185 if let Some(base_iri) = base_iri {
186 let base = vocabulary.iri(base_iri)?;
187 let resolved = iri_ref.resolved(&base).ok()?;
188 let iri_buf = IriBuf::try_from(resolved).ok()?;
189 Some(vocabulary.insert_owned(iri_buf))
190 } else {
191 let iri = Iri::try_from(iri_ref).ok()?;
192 Some(vocabulary.insert(iri))
193 }
194}
195
196/// Runs the [context processing algorithm][1], layering `local_context` on top
197/// of `active_context`.
198///
199/// Recurses for each remote `@context` IRI, fetching it through the loader, and
200/// for each term definition's scoped `@context` via `define`. `remote_contexts`
201/// carries the chain of remote context URLs entered so far, which bounds that
202/// recursion; a top-level call starts from an empty [`ProcessingStack`].
203///
204/// `validate_scoped_context` is the algorithm's input of the same name: `true`
205/// for a document's own `@context` and for contexts applied while expanding,
206/// `false` only when Create Term Definition validates a scoped `@context`
207/// (step 21.3). Only then is a context already in `remote_contexts` skipped
208/// (step 5.2.2); anywhere else a repeat is processed again, because JSON-LD
209/// processes the entries of a `@context` array in order.
210///
211/// Every mutation is applied to a clone of `active_context`, so the caller's
212/// context is left untouched.
213///
214/// This is the `async` half of a mirrored pair; see the module documentation
215/// before changing it.
216///
217/// [1]: https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm
218async fn process_context<'l: 'a, 'a, N, L, W>(
219 mut env: Environment<'a, N, L, W>,
220 active_context: &'a Context<N::Iri, N::BlankId>,
221 local_context: &'l syntax::context::Context,
222 mut remote_contexts: ProcessingStack<N::Iri>,
223 base_url: Option<N::Iri>,
224 mut options: Options,
225 validate_scoped_context: bool,
226) -> ProcessingResult<'l, N::Iri, N::BlankId, L::Error>
227where
228 N: VocabularyMut,
229 N::Iri: Clone + Eq + Hash,
230 N::BlankId: Clone + PartialEq,
231 L: Loader,
232 W: WarningHandler<N>,
233{
234 // 1) Initialize result to the result of cloning active context.
235 let mut result = active_context.clone();
236 result.set_processing_mode(options.processing_mode);
237
238 // 2) If `local_context` is an object containing the member @propagate,
239 // its value MUST be boolean true or false, set `propagate` to that value.
240 if let syntax::context::Context::One(syntax::ContextEntry::Definition(def)) = local_context
241 && let Some(propagate) = def.propagate
242 {
243 if options.processing_mode == ProcessingMode::JsonLd1_0 {
244 return Err(Error::InvalidContextEntry);
245 }
246
247 options.propagate = propagate;
248 }
249
250 // 3) If propagate is false, and result does not have a previous context,
251 // set previous context in result to active context.
252 if !options.propagate && result.previous_context().is_none() {
253 result.set_previous_context(active_context.clone());
254 }
255
256 // 4) If local context is not an array, set it to an array containing only local context.
257 // 5) For each item context in local context:
258 for context in local_context {
259 match context {
260 // 5.1) If context is null:
261 syntax::ContextEntry::Null => {
262 // If `override_protected` is false and `active_context` contains any protected term
263 // definitions, an invalid context nullification has been detected and processing
264 // is aborted.
265 if !options.override_protected && result.has_protected_items() {
266 return Err(Error::InvalidContextNullification);
267 }
268 // Otherwise, initialize result as a newly-initialized active context, setting
269 // previous_context in result to the previous value of result if propagate is
270 // false. Continue with the next context.
271 let previous_result = result;
272
273 // Initialize `result` as a newly-initialized active context, setting both
274 // `base_iri` and `original_base_url` to the value of `original_base_url` in
275 // active context, ...
276 result = Context::new(active_context.original_base_url().cloned());
277 result.set_processing_mode(options.processing_mode);
278
279 // ... and, if `propagate` is `false`, `previous_context` in `result` to the
280 // previous value of `result`.
281 if !options.propagate {
282 result.set_previous_context(previous_result);
283 }
284 }
285
286 // 5.2) If context is a string,
287 syntax::ContextEntry::IriRef(iri_ref) => {
288 // Initialize `context` to the result of resolving context against base URL.
289 // If base URL is not a valid IRI, then context MUST be a valid IRI, otherwise
290 // a loading document failed error has been detected and processing is aborted.
291 let context_iri = resolve_iri(env.vocabulary, iri_ref.as_ref(), base_url.as_ref()).ok_or(Error::LoadingDocumentFailed)?;
292
293 // 5.2.2) If validate scoped context is false, and remote contexts already
294 // includes context do not process context further and continue to any next
295 // context in local context.
296 //
297 // JSON-LD 1.0 had no such allowance: a context already in remote contexts is a
298 // recursive context inclusion error, whatever the caller.
299 if remote_contexts.cycle(&context_iri) {
300 if options.processing_mode == ProcessingMode::JsonLd1_0 {
301 return Err(Error::RecursiveContextInclusion);
302 }
303 if !validate_scoped_context {
304 continue;
305 }
306 }
307
308 // 5.2.3) If the number of entries in the `remote_contexts` array exceeds a
309 // processor defined limit, a context overflow error has been detected and
310 // processing is aborted; otherwise, add context to remote contexts.
311 if remote_contexts.len() >= crate::MAX_REMOTE_CONTEXTS {
312 return Err(Error::ContextOverflow);
313 }
314 remote_contexts.enter(context_iri.clone());
315
316 // 5.2.4) If context was previously dereferenced, then the processor MUST NOT do a
317 // further dereference, and context is set to the previously established internal
318 // representation: set `context_document` to the previously dereferenced document,
319 // and set loaded context to the value of the @context entry from the document in
320 // context document.
321 //
322 // 5.2.5) Otherwise, set `context document` to the RemoteDocument obtained by
323 // dereferencing context using the LoadDocumentCallback, passing context for url,
324 // and http://www.w3.org/ns/json-ld#context for profile and for requestProfile.
325 //
326 // If context cannot be dereferenced, or the document from context document cannot
327 // be transformed into the internal representation , a loading remote context
328 // failed error has been detected and processing is aborted.
329 // If the document has no top-level map with an @context entry, an invalid remote
330 // context has been detected and processing is aborted.
331 // Set loaded context to the value of that entry.
332 let loaded_context = if let Some(previously) = remote_contexts.dereferenced(&context_iri) {
333 previously
334 } else {
335 let loaded = Arc::new(
336 env.loader
337 .dereference(env.vocabulary, context_iri.clone())
338 .await?
339 .into_document()
340 .into_ld_context()
341 .map_err(Error::ContextExtractionFailed)?,
342 );
343 remote_contexts.remember_dereferenced(context_iri.clone(), Arc::clone(&loaded));
344 loaded
345 };
346
347 // 5.2.6) Set result to the result of recursively calling this algorithm, passing
348 // result for active context, loaded context for local context, the documentUrl of
349 // context document for base URL, a copy of remote contexts, and validate scoped
350 // context.
351 let new_options = Options {
352 processing_mode: options.processing_mode,
353 override_protected: false,
354 propagate: true,
355 vocab: options.vocab,
356 };
357
358 let r = Recursion::new(process_context(
359 Environment {
360 vocabulary: env.vocabulary,
361 loader: env.loader,
362 warnings: env.warnings,
363 },
364 &result,
365 &loaded_context,
366 remote_contexts.clone(),
367 Some(context_iri),
368 new_options,
369 validate_scoped_context,
370 ))
371 .await?;
372
373 result = r.into_processed();
374 }
375
376 // 5.4) Context definition.
377 syntax::ContextEntry::Definition(context) => {
378 // 5.5) If context has a @version entry:
379 if context.version.is_some() {
380 // 5.5.2) If processing mode is set to json-ld-1.0, a processing mode conflict
381 // error has been detected.
382 if options.processing_mode == ProcessingMode::JsonLd1_0 {
383 return Err(Error::ProcessingModeConflict);
384 }
385 }
386
387 // 5.6) If context has an @import entry:
388 let import_context = match &context.import {
389 Some(import_value) => {
390 // 5.6.1) If processing mode is json-ld-1.0, an invalid context entry error
391 // has been detected.
392 if options.processing_mode == ProcessingMode::JsonLd1_0 {
393 return Err(Error::InvalidContextEntry);
394 }
395
396 // 5.6.3) Initialize import to the result of resolving the value of
397 // @import.
398 let import = resolve_iri(env.vocabulary, import_value.as_ref(), base_url.as_ref()).ok_or(Error::InvalidImportValue)?;
399
400 // 5.6.4) Dereference import.
401 let import_context = env
402 .loader
403 .dereference(env.vocabulary, import.clone())
404 .await?
405 .into_document()
406 .into_ld_context()
407 .map_err(Error::ContextExtractionFailed)?;
408
409 // If the dereferenced document has no top-level map with an @context
410 // entry, or if the value of @context is not a context definition
411 // (i.e., it is not an map), an invalid remote context has been
412 // detected and processing is aborted; otherwise, set import context
413 // to the value of that entry.
414 match &import_context {
415 syntax::context::Context::One(syntax::ContextEntry::Definition(import_context_def)) => {
416 // If `import_context` has a @import entry, an invalid context entry
417 // error has been detected and processing is aborted.
418 if import_context_def.import.is_some() {
419 return Err(Error::InvalidContextEntry);
420 }
421 }
422 _ => {
423 return Err(Error::InvalidRemoteContext);
424 }
425 }
426
427 // Set `context` to the result of merging context into
428 // `import_context`, replacing common entries with those from
429 // `context`.
430 Some(import_context)
431 }
432 None => None,
433 };
434
435 let context = Merged::new(context, import_context);
436
437 // 5.7) If context has a @base entry and remote contexts is empty, i.e.,
438 // the currently being processed context is not a remote context:
439 if remote_contexts.is_empty() {
440 // Initialize value to the value associated with the @base entry.
441 if let Some(value) = context.base() {
442 match value {
443 syntax::Nullable::Null => {
444 // If value is null, remove the base IRI of result.
445 result.set_base_iri(None);
446 }
447 syntax::Nullable::Some(iri_ref) => {
448 if let Ok(iri) = Iri::try_from(iri_ref.as_ref()) {
449 result.set_base_iri(Some(env.vocabulary.insert(iri)));
450 } else {
451 let resolved = resolve_iri(env.vocabulary, iri_ref.as_ref(), result.base_iri()).ok_or(Error::InvalidBaseIri)?;
452 result.set_base_iri(Some(resolved));
453 }
454 }
455 }
456 }
457 }
458
459 // 5.8) If context has a @vocab entry:
460 // Initialize value to the value associated with the @vocab entry.
461 if let Some(value) = context.vocab() {
462 match value {
463 syntax::Nullable::Null => {
464 // If value is null, remove any vocabulary mapping from result.
465 result.set_vocabulary(None);
466 }
467 syntax::Nullable::Some(value) => {
468 // Otherwise, if value is an IRI or blank node identifier, the
469 // vocabulary mapping of result is set to the result of IRI
470 // expanding value using true for document relative. If it is not
471 // an IRI, or a blank node identifier, an invalid vocab mapping
472 // error has been detected and processing is aborted.
473 // NOTE: The use of blank node identifiers to value for @vocab is
474 // obsolete, and may be removed in a future version of JSON-LD.
475 //
476 // Document-relative expansion of `@vocab` is a JSON-LD 1.1 addition:
477 // 1.0 required an absolute IRI or blank node identifier, and rejects
478 // `""` and `/relative` (`expand#t0115`, `expand#t0116`). A bare `_:`
479 // prefix stays valid in 1.0 (`expand#t0075`).
480 let json_ld_1_0 = options.processing_mode == ProcessingMode::JsonLd1_0;
481 if json_ld_1_0 && !is_valid_vocab_json_ld_1_0(value) {
482 return Err(Error::InvalidVocabMapping);
483 }
484
485 match expand_iri_simple(&mut env, &result, Nullable::Some(value.into()), !json_ld_1_0, Some(options.vocab))? {
486 Some(arc) if matches!(arc.as_ref(), Term::Id(_)) => {
487 let term = std::sync::Arc::try_unwrap(arc).unwrap_or_else(|a| (*a).clone());
488 result.set_vocabulary(Some(term));
489 }
490 _ => return Err(Error::InvalidVocabMapping),
491 }
492 }
493 }
494 }
495
496 // 5.9) If context has a @language entry:
497 if let Some(value) = context.language() {
498 match value {
499 Nullable::Null => {
500 // 5.9.2) If value is null, remove any default language from result.
501 result.set_default_language(None);
502 }
503 Nullable::Some(tag) => {
504 result.set_default_language(Some(tag.to_owned()));
505 }
506 }
507 }
508
509 // 5.10) If context has a @direction entry:
510 if let Some(value) = context.direction() {
511 // 5.10.1) If processing mode is json-ld-1.0, an invalid context entry error
512 // has been detected and processing is aborted.
513 if options.processing_mode == ProcessingMode::JsonLd1_0 {
514 return Err(Error::InvalidContextEntry);
515 }
516
517 match value {
518 Nullable::Null => {
519 // 5.10.3) If value is null, remove any base direction from result.
520 result.set_default_base_direction(None);
521 }
522 Nullable::Some(dir) => {
523 result.set_default_base_direction(Some(dir));
524 }
525 }
526 }
527
528 // 5.12) Create a map `defined` to keep track of whether or not a term
529 // has already been defined or is currently being defined during recursion.
530 let mut defined = DefinedTerms::new();
531 let protected = context.protected().unwrap_or(false);
532
533 // 5.13) For each key-value pair in context where key is not
534 // @base, @direction, @import, @language, @propagate, @protected, @version,
535 // or @vocab,
536 // invoke the Create Term Definition algorithm passing result for
537 // active context, context for local context, key, defined, base URL,
538 // and the value of the @protected entry from context, if any, for protected.
539 // (and the value of override protected)
540 if context.type_().is_some() {
541 define(
542 Environment {
543 vocabulary: env.vocabulary,
544 loader: env.loader,
545 warnings: env.warnings,
546 },
547 &mut result,
548 &context,
549 KeyOrKeywordRef::Keyword(syntax::Keyword::Type),
550 &mut defined,
551 remote_contexts.clone(),
552 base_url.clone(),
553 protected,
554 options,
555 )
556 .await?;
557 }
558
559 for (key, _binding) in context.bindings() {
560 define(
561 Environment {
562 vocabulary: env.vocabulary,
563 loader: env.loader,
564 warnings: env.warnings,
565 },
566 &mut result,
567 &context,
568 key.into(),
569 &mut defined,
570 remote_contexts.clone(),
571 base_url.clone(),
572 protected,
573 options,
574 )
575 .await?;
576 }
577 }
578 }
579 }
580
581 Ok(Processed::new(local_context, result))
582}