json_ld_context_processing/algorithm/
mod.rs

1use std::hash::Hash;
2
3use crate::{
4	Error, Options, Process, Processed, ProcessingResult, ProcessingStack, WarningHandler,
5};
6use iref::IriRef;
7use json_ld_core::{Context, Environment, ExtractContext, Loader, ProcessingMode, Term};
8use json_ld_syntax::{self as syntax, Nullable};
9use rdf_types::{vocabulary::IriVocabularyMut, VocabularyMut};
10
11mod define;
12mod iri;
13mod merged;
14
15pub use define::*;
16pub use iri::*;
17pub use merged::*;
18use syntax::context::definition::KeyOrKeywordRef;
19
20impl Process for syntax::context::Context {
21	async fn process_full<N, L, W>(
22		&self,
23		vocabulary: &mut N,
24		active_context: &Context<N::Iri, N::BlankId>,
25		loader: &L,
26		base_url: Option<N::Iri>,
27		options: Options,
28		mut warnings: W,
29	) -> Result<Processed<N::Iri, N::BlankId>, Error>
30	where
31		N: VocabularyMut,
32		N::Iri: Clone + Eq + Hash,
33		N::BlankId: Clone + PartialEq,
34		L: Loader,
35		W: WarningHandler<N>,
36	{
37		process_context(
38			Environment {
39				vocabulary,
40				loader,
41				warnings: &mut warnings,
42			},
43			active_context,
44			self,
45			ProcessingStack::default(),
46			base_url,
47			options,
48		)
49		.await
50	}
51}
52
53/// Resolve `iri_ref` against the given base IRI.
54fn resolve_iri<I>(
55	vocabulary: &mut impl IriVocabularyMut<Iri = I>,
56	iri_ref: &IriRef,
57	base_iri: Option<&I>,
58) -> Option<I> {
59	match base_iri {
60		Some(base_iri) => {
61			let result = iri_ref.resolved(vocabulary.iri(base_iri).unwrap());
62			Some(vocabulary.insert(result.as_iri()))
63		}
64		None => iri_ref.as_iri().map(|iri| vocabulary.insert(iri)),
65	}
66}
67
68// This function tries to follow the recommended context processing algorithm.
69// See `https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm`.
70//
71// The recommended default value for `remote_contexts` is the empty set,
72// `false` for `override_protected`, and `true` for `propagate`.
73async fn process_context<'l: 'a, 'a, N, L, W>(
74	mut env: Environment<'a, N, L, W>,
75	active_context: &'a Context<N::Iri, N::BlankId>,
76	local_context: &'l syntax::context::Context,
77	mut remote_contexts: ProcessingStack<N::Iri>,
78	base_url: Option<N::Iri>,
79	mut options: Options,
80) -> ProcessingResult<'l, N::Iri, N::BlankId>
81where
82	N: VocabularyMut,
83	N::Iri: Clone + Eq + Hash,
84	N::BlankId: Clone + PartialEq,
85	L: Loader,
86	W: WarningHandler<N>,
87{
88	// 1) Initialize result to the result of cloning active context.
89	let mut result = active_context.clone();
90
91	// 2) If `local_context` is an object containing the member @propagate,
92	// its value MUST be boolean true or false, set `propagate` to that value.
93	if let syntax::context::Context::One(syntax::ContextEntry::Definition(def)) = local_context {
94		if let Some(propagate) = def.propagate {
95			if options.processing_mode == ProcessingMode::JsonLd1_0 {
96				return Err(Error::InvalidContextEntry);
97			}
98
99			options.propagate = propagate
100		}
101	}
102
103	// 3) If propagate is false, and result does not have a previous context,
104	// set previous context in result to active context.
105	if !options.propagate && result.previous_context().is_none() {
106		result.set_previous_context(active_context.clone());
107	}
108
109	// 4) If local context is not an array, set it to an array containing only local context.
110	// 5) For each item context in local context:
111	for context in local_context {
112		match context {
113			// 5.1) If context is null:
114			syntax::ContextEntry::Null => {
115				// If `override_protected` is false and `active_context` contains any protected term
116				// definitions, an invalid context nullification has been detected and processing
117				// is aborted.
118				if !options.override_protected && result.has_protected_items() {
119					return Err(Error::InvalidContextNullification);
120				} else {
121					// Otherwise, initialize result as a newly-initialized active context, setting
122					// previous_context in result to the previous value of result if propagate is
123					// false. Continue with the next context.
124					let previous_result = result;
125
126					// Initialize `result` as a newly-initialized active context, setting both
127					// `base_iri` and `original_base_url` to the value of `original_base_url` in
128					// active context, ...
129					result = Context::new(active_context.original_base_url().cloned());
130
131					// ... and, if `propagate` is `false`, `previous_context` in `result` to the
132					// previous value of `result`.
133					if !options.propagate {
134						result.set_previous_context(previous_result);
135					}
136				}
137			}
138
139			// 5.2) If context is a string,
140			syntax::ContextEntry::IriRef(iri_ref) => {
141				// Initialize `context` to the result of resolving context against base URL.
142				// If base URL is not a valid IRI, then context MUST be a valid IRI, otherwise
143				// a loading document failed error has been detected and processing is aborted.
144				let context_iri =
145					resolve_iri(env.vocabulary, iri_ref.as_iri_ref(), base_url.as_ref())
146						.ok_or(Error::LoadingDocumentFailed)?;
147
148				// If the number of entries in the `remote_contexts` array exceeds a processor
149				// defined limit, a context overflow error has been detected and processing is
150				// aborted; otherwise, add context to remote contexts.
151				//
152				// If context was previously dereferenced, then the processor MUST NOT do a further
153				// dereference, and context is set to the previously established internal
154				// representation: set `context_document` to the previously dereferenced document,
155				// and set loaded context to the value of the @context entry from the document in
156				// context document.
157				//
158				// Otherwise, set `context document` to the RemoteDocument obtained by dereferencing
159				// context using the LoadDocumentCallback, passing context for url, and
160				// http://www.w3.org/ns/json-ld#context for profile and for requestProfile.
161				//
162				// If context cannot be dereferenced, or the document from context document cannot
163				// be transformed into the internal representation , a loading remote context
164				// failed error has been detected and processing is aborted.
165				// If the document has no top-level map with an @context entry, an invalid remote
166				// context has been detected and processing is aborted.
167				// Set loaded context to the value of that entry.
168				if remote_contexts.push(context_iri.clone()) {
169					let loaded_context = env
170						.loader
171						.load_with(env.vocabulary, context_iri.clone())
172						.await?
173						.into_document()
174						.into_ld_context()
175						.map_err(Error::ContextExtractionFailed)?;
176
177					// Set result to the result of recursively calling this algorithm, passing result
178					// for active context, loaded context for local context, the documentUrl of context
179					// document for base URL, and a copy of remote contexts.
180					let new_options = Options {
181						processing_mode: options.processing_mode,
182						override_protected: false,
183						propagate: true,
184						vocab: options.vocab,
185					};
186
187					let r = Box::pin(process_context(
188						Environment {
189							vocabulary: env.vocabulary,
190							loader: env.loader,
191							warnings: env.warnings,
192						},
193						&result,
194						&loaded_context,
195						remote_contexts.clone(),
196						Some(context_iri),
197						new_options,
198					))
199					.await?;
200
201					result = r.into_processed();
202				}
203			}
204
205			// 5.4) Context definition.
206			syntax::ContextEntry::Definition(context) => {
207				// 5.5) If context has a @version entry:
208				if context.version.is_some() {
209					// 5.5.2) If processing mode is set to json-ld-1.0, a processing mode conflict
210					// error has been detected.
211					if options.processing_mode == ProcessingMode::JsonLd1_0 {
212						return Err(Error::ProcessingModeConflict);
213					}
214				}
215
216				// 5.6) If context has an @import entry:
217				let import_context = match &context.import {
218					Some(import_value) => {
219						// 5.6.1) If processing mode is json-ld-1.0, an invalid context entry error
220						// has been detected.
221						if options.processing_mode == ProcessingMode::JsonLd1_0 {
222							return Err(Error::InvalidContextEntry);
223						}
224
225						// 5.6.3) Initialize import to the result of resolving the value of
226						// @import.
227						let import = resolve_iri(
228							env.vocabulary,
229							import_value.as_iri_ref(),
230							base_url.as_ref(),
231						)
232						.ok_or(Error::InvalidImportValue)?;
233
234						// 5.6.4) Dereference import.
235						let import_context = env
236							.loader
237							.load_with(env.vocabulary, import.clone())
238							.await?
239							.into_document()
240							.into_ld_context()
241							.map_err(Error::ContextExtractionFailed)?;
242
243						// If the dereferenced document has no top-level map with an @context
244						// entry, or if the value of @context is not a context definition
245						// (i.e., it is not an map), an invalid remote context has been
246						// detected and processing is aborted; otherwise, set import context
247						// to the value of that entry.
248						match &import_context {
249							syntax::context::Context::One(syntax::ContextEntry::Definition(
250								import_context_def,
251							)) => {
252								// If `import_context` has a @import entry, an invalid context entry
253								// error has been detected and processing is aborted.
254								if import_context_def.import.is_some() {
255									return Err(Error::InvalidContextEntry);
256								}
257							}
258							_ => {
259								return Err(Error::InvalidRemoteContext);
260							}
261						}
262
263						// Set `context` to the result of merging context into
264						// `import_context`, replacing common entries with those from
265						// `context`.
266						Some(import_context)
267					}
268					None => None,
269				};
270
271				let context = Merged::new(context, import_context);
272
273				// 5.7) If context has a @base entry and remote contexts is empty, i.e.,
274				// the currently being processed context is not a remote context:
275				if remote_contexts.is_empty() {
276					// Initialize value to the value associated with the @base entry.
277					if let Some(value) = context.base() {
278						match value {
279							syntax::Nullable::Null => {
280								// If value is null, remove the base IRI of result.
281								result.set_base_iri(None);
282							}
283							syntax::Nullable::Some(iri_ref) => match iri_ref.as_iri() {
284								Some(iri) => result.set_base_iri(Some(env.vocabulary.insert(iri))),
285								None => {
286									let resolved =
287										resolve_iri(env.vocabulary, iri_ref, result.base_iri())
288											.ok_or(Error::InvalidBaseIri)?;
289									result.set_base_iri(Some(resolved))
290								}
291							},
292						}
293					}
294				}
295
296				// 5.8) If context has a @vocab entry:
297				// Initialize value to the value associated with the @vocab entry.
298				if let Some(value) = context.vocab() {
299					match value {
300						syntax::Nullable::Null => {
301							// If value is null, remove any vocabulary mapping from result.
302							result.set_vocabulary(None);
303						}
304						syntax::Nullable::Some(value) => {
305							// Otherwise, if value is an IRI or blank node identifier, the
306							// vocabulary mapping of result is set to the result of IRI
307							// expanding value using true for document relative. If it is not
308							// an IRI, or a blank node identifier, an invalid vocab mapping
309							// error has been detected and processing is aborted.
310							// NOTE: The use of blank node identifiers to value for @vocab is
311							// obsolete, and may be removed in a future version of JSON-LD.
312							match expand_iri_simple(
313								&mut env,
314								&result,
315								Nullable::Some(value.into()),
316								true,
317								Some(options.vocab),
318							)? {
319								Some(Term::Id(vocab)) => {
320									result.set_vocabulary(Some(Term::Id(vocab)))
321								}
322								_ => return Err(Error::InvalidVocabMapping),
323							}
324						}
325					}
326				}
327
328				// 5.9) If context has a @language entry:
329				if let Some(value) = context.language() {
330					match value {
331						Nullable::Null => {
332							// 5.9.2) If value is null, remove any default language from result.
333							result.set_default_language(None);
334						}
335						Nullable::Some(tag) => {
336							result.set_default_language(Some(tag.to_owned()));
337						}
338					}
339				}
340
341				// 5.10) If context has a @direction entry:
342				if let Some(value) = context.direction() {
343					// 5.10.1) If processing mode is json-ld-1.0, an invalid context entry error
344					// has been detected and processing is aborted.
345					if options.processing_mode == ProcessingMode::JsonLd1_0 {
346						return Err(Error::InvalidContextEntry);
347					}
348
349					match value {
350						Nullable::Null => {
351							// 5.10.3) If value is null, remove any base direction from result.
352							result.set_default_base_direction(None);
353						}
354						Nullable::Some(dir) => {
355							result.set_default_base_direction(Some(dir));
356						}
357					}
358				}
359
360				// 5.12) Create a map `defined` to keep track of whether or not a term
361				// has already been defined or is currently being defined during recursion.
362				let mut defined = DefinedTerms::new();
363				let protected = context.protected().unwrap_or(false);
364
365				// 5.13) For each key-value pair in context where key is not
366				// @base, @direction, @import, @language, @propagate, @protected, @version,
367				// or @vocab,
368				// invoke the Create Term Definition algorithm passing result for
369				// active context, context for local context, key, defined, base URL,
370				// and the value of the @protected entry from context, if any, for protected.
371				// (and the value of override protected)
372				if context.type_().is_some() {
373					define(
374						Environment {
375							vocabulary: env.vocabulary,
376							loader: env.loader,
377							warnings: env.warnings,
378						},
379						&mut result,
380						&context,
381						KeyOrKeywordRef::Keyword(syntax::Keyword::Type),
382						&mut defined,
383						remote_contexts.clone(),
384						base_url.clone(),
385						protected,
386						options,
387					)
388					.await?
389				}
390
391				for (key, _binding) in context.bindings() {
392					define(
393						Environment {
394							vocabulary: env.vocabulary,
395							loader: env.loader,
396							warnings: env.warnings,
397						},
398						&mut result,
399						&context,
400						key.into(),
401						&mut defined,
402						remote_contexts.clone(),
403						base_url.clone(),
404						protected,
405						options,
406					)
407					.await?
408				}
409			}
410		}
411	}
412
413	Ok(Processed::new(local_context, result))
414}