Skip to main content

json_ld/algorithms/context_processing/
mod.rs

1//! JSON-LD context processing types and algorithms.
2use std::borrow::Cow;
3
4use rdf_syntax::Iri;
5
6mod define;
7mod iri;
8mod merged;
9mod options;
10mod stack;
11
12use define::*;
13use iri::*;
14use merged::Merged;
15pub use options::JsonLdContextProcessingOptions;
16use stack::ProcessingStack;
17
18use crate::{
19	AsyncJsonLdLoader, JsonLdBacktraceBuilder, JsonLdContextDocument, JsonLdError,
20	JsonLdLocatedError, JsonLdNullable, JsonLdProcessedContext, JsonLdProcessingMode,
21	JsonLdSourceFileRef, JsonLdTerm,
22	algorithms::AsyncJsonLdEnvironment,
23	context::RawJsonLdProcessedContext,
24	syntax::{JsonLdContext, JsonLdContextEntry, JsonLdKeyword, context::JsonLdKeyOrKeywordRef},
25};
26
27struct TargetProcessedContext<'a> {
28	pub value: &'a mut RawJsonLdProcessedContext,
29	pub defined: DefinedTerms,
30}
31
32struct ContextProcessor<'a> {
33	pub remote_contexts: ProcessingStack,
34	pub active_context: &'a RawJsonLdProcessedContext,
35	pub base_url: Option<&'a Iri>,
36	pub options: JsonLdContextProcessingOptions,
37}
38
39impl<'a> ContextProcessor<'a> {
40	fn for_recursive_definition<'b>(&'b self) -> ContextProcessor<'b> {
41		ContextProcessor {
42			remote_contexts: self.remote_contexts.clone(),
43			active_context: self.active_context,
44			base_url: None,
45			options: self.options.with_no_override(),
46		}
47	}
48
49	fn for_sub_context<'b>(
50		&'b self,
51		active_context: &'b RawJsonLdProcessedContext,
52		base_url: Option<&'b Iri>,
53		options: JsonLdContextProcessingOptions,
54	) -> ContextProcessor<'b> {
55		ContextProcessor {
56			remote_contexts: self.remote_contexts.clone(),
57			active_context,
58			base_url,
59			options,
60		}
61	}
62}
63
64impl JsonLdContextDocument {
65	/// Process this context with the default options.
66	///
67	/// See: <https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm>
68	pub async fn process(
69		&self,
70		env: impl AsyncJsonLdEnvironment,
71	) -> Result<JsonLdProcessedContext<'_>, JsonLdLocatedError> {
72		let loc = JsonLdBacktraceBuilder::new().file(JsonLdSourceFileRef::original(self.url()));
73		self.document
74			.context
75			.process_with(
76				env,
77				self.url(),
78				&RawJsonLdProcessedContext::new(None),
79				JsonLdContextProcessingOptions::default(),
80				loc,
81			)
82			.await
83	}
84}
85
86impl JsonLdContext {
87	/// Process this context with the default options.
88	///
89	/// See: <https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm>
90	pub async fn process(
91		&self,
92		env: impl AsyncJsonLdEnvironment,
93		base_url: Option<&Iri>,
94	) -> Result<JsonLdProcessedContext<'_>, JsonLdLocatedError> {
95		let active_context = RawJsonLdProcessedContext::new(None);
96		self.process_with(
97			env,
98			base_url,
99			&active_context,
100			JsonLdContextProcessingOptions::default(),
101			// No document URL available here; still need a `File` frame or the
102			// backtrace would be silently empty.
103			JsonLdBacktraceBuilder::new().file(JsonLdSourceFileRef::original(None)),
104		)
105		.await
106	}
107
108	/// Process this context with the given options.
109	///
110	/// See: <https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm>
111	pub async fn process_with(
112		&self,
113		env: impl AsyncJsonLdEnvironment,
114		base_url: Option<&Iri>,
115		active_context: &RawJsonLdProcessedContext,
116		options: JsonLdContextProcessingOptions,
117		location: JsonLdBacktraceBuilder<'_>,
118	) -> Result<JsonLdProcessedContext<'_>, JsonLdLocatedError> {
119		ContextProcessor {
120			options,
121			remote_contexts: ProcessingStack::new(),
122			base_url,
123			active_context,
124		}
125		.process(&env, self, location)
126		.await
127		.map(|raw| JsonLdProcessedContext::new(Cow::Borrowed(self), raw))
128	}
129}
130
131impl<'a> ContextProcessor<'a> {
132	async fn process(
133		mut self,
134		env: &impl AsyncJsonLdEnvironment,
135		local_context: &JsonLdContext,
136		location: JsonLdBacktraceBuilder<'_>,
137	) -> Result<RawJsonLdProcessedContext, JsonLdLocatedError> {
138		// 1) Initialize result to the result of cloning active context.
139		let mut result = self.active_context.clone();
140
141		// 2) If `local_context` is an object containing the member @propagate,
142		// its value MUST be boolean true or false, set `propagate` to that value.
143		if let JsonLdContext::One(JsonLdContextEntry::Definition(def)) = local_context
144			&& let Some(propagate) = def.propagate
145		{
146			if self.options.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
147				return Err(JsonLdError::InvalidContextEntry.at(location));
148			}
149
150			self.options.propagate = propagate
151		}
152
153		// 3) If propagate is false, and result does not have a previous context,
154		// set previous context in result to active context.
155		if !self.options.propagate && result.previous_context().is_none() {
156			result.set_previous_context(self.active_context.clone());
157		}
158
159		// 4) If local context is not an array, set it to an array containing only local context.
160		// 5) For each item context in local context:
161		for (i, context) in local_context.iter().enumerate() {
162			let entry_loc = location.array_index(i);
163			match context {
164				// 5.1) If context is null:
165				JsonLdContextEntry::Null => {
166					// If `override_protected` is false and `active_context` contains any protected term
167					// definitions, an invalid context nullification has been detected and processing
168					// is aborted.
169					if !self.options.override_protected && result.has_protected_items() {
170						return Err(JsonLdError::InvalidContextNullification.at(entry_loc));
171					} else {
172						// Otherwise, initialize result as a newly-initialized active context, setting
173						// previous_context in result to the previous value of result if propagate is
174						// false. Continue with the next context.
175						let previous_result = result;
176
177						// Initialize `result` as a newly-initialized active context, setting both
178						// `base_iri` and `original_base_url` to the value of `original_base_url` in
179						// active context, ...
180						result = RawJsonLdProcessedContext::new(
181							self.active_context
182								.original_base_url()
183								.map(ToOwned::to_owned),
184						);
185
186						// ... and, if `propagate` is `false`, `previous_context` in `result` to the
187						// previous value of `result`.
188						if !self.options.propagate {
189							result.set_previous_context(previous_result);
190						}
191					}
192				}
193
194				// 5.2) If context is a string,
195				JsonLdContextEntry::IriRef(iri_ref) => {
196					// Initialize `context` to the result of resolving context against base URL.
197					// If base URL is not a valid IRI, then context MUST be a valid IRI, otherwise
198					// a loading document failed error has been detected and processing is aborted.
199					let context_iri = resolve_iri(iri_ref, self.base_url)
200						.ok_or_else(|| JsonLdError::LoadingDocumentFailed.at(entry_loc))?;
201
202					// If the number of entries in the `remote_contexts` array exceeds a processor
203					// defined limit, a context overflow error has been detected and processing is
204					// aborted; otherwise, add context to remote contexts.
205					//
206					// If context was previously dereferenced, then the processor MUST NOT do a further
207					// dereference, and context is set to the previously established internal
208					// representation: set `context_document` to the previously dereferenced document,
209					// and set loaded context to the value of the @context entry from the document in
210					// context document.
211					//
212					// Otherwise, set `context document` to the RemoteDocument obtained by dereferencing
213					// context using the LoadDocumentCallback, passing context for url, and
214					// http://www.w3.org/ns/json-ld#context for profile and for requestProfile.
215					//
216					// If context cannot be dereferenced, or the document from context document cannot
217					// be transformed into the internal representation , a loading remote context
218					// failed error has been detected and processing is aborted.
219					// If the document has no top-level map with an @context entry, an invalid remote
220					// context has been detected and processing is aborted.
221					// Set loaded context to the value of that entry.
222					if self.remote_contexts.push(context_iri.clone()) {
223						let loaded_context = env
224							.loader()
225							.async_load(&context_iri)
226							.await
227							.map_err(|e| JsonLdError::ContextLoadingFailed(e).at(entry_loc))?
228							.try_into_context_document()
229							.map_err(|e| {
230								JsonLdError::RemoteContextSyntax(e.to_string()).at(entry_loc)
231							})?
232							.into_document()
233							.context;
234
235						// Set result to the result of recursively calling this algorithm, passing result
236						// for active context, loaded context for local context, the documentUrl of context
237						// document for base URL, and a copy of remote contexts.
238						let new_options = JsonLdContextProcessingOptions {
239							processing_mode: self.options.processing_mode,
240							override_protected: false,
241							propagate: true,
242						};
243
244						// Extend the caller's trace rather than starting a fresh one, so that
245						// errors raised while processing `loaded_context` still show which
246						// `@context` array slot, in which calling document, pulled it in.
247						let remote_loc =
248							entry_loc.file(JsonLdSourceFileRef::original(Some(&context_iri)));
249						result = Box::pin(
250							self.for_sub_context(&result, Some(&context_iri), new_options)
251								.process(env, &loaded_context, remote_loc),
252						)
253						.await?;
254					}
255				}
256
257				// 5.4) Context definition.
258				JsonLdContextEntry::Definition(context) => {
259					// 5.5) If context has a @version entry:
260					if context.version.is_some() {
261						// 5.5.2) If processing mode is set to json-ld-1.0, a processing mode conflict
262						// error has been detected.
263						if self.options.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
264							return Err(JsonLdError::ProcessingModeConflict.at(entry_loc));
265						}
266					}
267
268					// 5.6) If context has an @import entry:
269					let mut import_url = None;
270					let import_context = match &context.import {
271						Some(import_value) => {
272							// 5.6.1) If processing mode is json-ld-1.0, an invalid context entry error
273							// has been detected.
274							let import_loc = entry_loc.object_value(JsonLdKeyword::Import);
275
276							if self.options.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
277								return Err(JsonLdError::InvalidContextEntry.at(import_loc));
278							}
279
280							// 5.6.3) Initialize import to the result of resolving the value of
281							// @import.
282							let import = resolve_iri(import_value, self.base_url)
283								.ok_or_else(|| JsonLdError::InvalidImportValue.at(import_loc))?;
284
285							// 5.6.4) Dereference import.
286							let import_context = env
287								.loader()
288								.async_load(&import)
289								.await
290								.map_err(|e| JsonLdError::ContextLoadingFailed(e).at(import_loc))?
291								.try_into_context_document()
292								.map_err(|e| {
293									JsonLdError::RemoteContextSyntax(e.to_string()).at(import_loc)
294								})?
295								.into_document()
296								.context;
297
298							// If the dereferenced document has no top-level map with an @context
299							// entry, or if the value of @context is not a context definition
300							// (i.e., it is not an map), an invalid remote context has been
301							// detected and processing is aborted; otherwise, set import context
302							// to the value of that entry.
303							match &import_context {
304								JsonLdContext::One(JsonLdContextEntry::Definition(
305									import_context_def,
306								)) => {
307									// If `import_context` has a @import entry, an invalid context entry
308									// error has been detected and processing is aborted.
309									if import_context_def.import.is_some() {
310										return Err(JsonLdError::InvalidContextEntry.at(import_loc));
311									}
312								}
313								_ => {
314									return Err(JsonLdError::InvalidRemoteContext.at(import_loc));
315								}
316							}
317
318							// Set `context` to the result of merging context into
319							// `import_context`, replacing common entries with those from
320							// `context`.
321							import_url = Some(import);
322							Some(import_context)
323						}
324						None => None,
325					};
326
327					let context = Merged::new(context, import_context);
328
329					// 5.7) If context has a @base entry and remote contexts is empty, i.e.,
330					// the currently being processed context is not a remote context:
331					if self.remote_contexts.is_empty() {
332						// Initialize value to the value associated with the @base entry.
333						if let Some(value) = context.base() {
334							match value {
335								JsonLdNullable::Null => {
336									// If value is null, remove the base IRI of result.
337									result.set_base_iri(None);
338								}
339								JsonLdNullable::Some(iri_ref) => match iri_ref.as_iri() {
340									Some(iri) => result.set_base_iri(Some(iri.to_owned())),
341									None => {
342										let resolved = resolve_iri(iri_ref, result.base_iri())
343											.ok_or_else(|| {
344												JsonLdError::InvalidBaseIri
345													.at(entry_loc.object_value(JsonLdKeyword::Base))
346											})?;
347										result.set_base_iri(Some(resolved))
348									}
349								},
350							}
351						}
352					}
353
354					// 5.8) If context has a @vocab entry:
355					// Initialize value to the value associated with the @vocab entry.
356					if let Some(value) = context.vocab() {
357						match value {
358							JsonLdNullable::Null => {
359								// If value is null, remove any vocabulary mapping from result.
360								result.set_vocabulary(None);
361							}
362							JsonLdNullable::Some(value) => {
363								// Otherwise, if value is an IRI or blank node identifier, the
364								// vocabulary mapping of result is set to the result of IRI
365								// expanding value using true for document relative. If it is not
366								// an IRI, or a blank node identifier, an invalid vocab mapping
367								// error has been detected and processing is aborted.
368								// NOTE: The use of blank node identifiers to value for @vocab is
369								// obsolete, and may be removed in a future version of JSON-LD.
370								match result.expand_iri_with(
371									JsonLdNullable::Some(value.into()),
372									true,
373									true,
374									|w| env.warn(w),
375								) {
376									JsonLdTerm::Id(vocab) => {
377										result.set_vocabulary(Some(JsonLdTerm::Id(vocab)))
378									}
379									_ => {
380										return Err(JsonLdError::InvalidVocabMapping
381											.at(entry_loc.object_value(JsonLdKeyword::Vocab)));
382									}
383								}
384							}
385						}
386					}
387
388					// 5.9) If context has a @language entry:
389					if let Some(value) = context.language() {
390						match value {
391							JsonLdNullable::Null => {
392								// 5.9.2) If value is null, remove any default language from result.
393								result.set_default_language(None);
394							}
395							JsonLdNullable::Some(tag) => {
396								result.set_default_language(Some(tag.to_owned()));
397							}
398						}
399					}
400
401					// 5.10) If context has a @direction entry:
402					if let Some(value) = context.direction() {
403						// 5.10.1) If processing mode is json-ld-1.0, an invalid context entry error
404						// has been detected and processing is aborted.
405						if self.options.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
406							return Err(JsonLdError::InvalidContextEntry
407								.at(entry_loc.object_value(JsonLdKeyword::Direction)));
408						}
409
410						match value {
411							JsonLdNullable::Null => {
412								// 5.10.3) If value is null, remove any base direction from result.
413								result.set_default_base_direction(None);
414							}
415							JsonLdNullable::Some(dir) => {
416								result.set_default_base_direction(Some(dir));
417							}
418						}
419					}
420
421					// 5.12) Create a map `defined` to keep track of whether or not a term
422					// has already been defined or is currently being defined during recursion.
423					let mut result = TargetProcessedContext {
424						value: &mut result,
425						defined: DefinedTerms::new(),
426					};
427
428					let protected = context.protected().unwrap_or(false);
429
430					// 5.13) For each key-value pair in context where key is not
431					// @base, @direction, @import, @language, @propagate, @protected, @version,
432					// or @vocab,
433					// invoke the Create Term Definition algorithm passing result for
434					// active context, context for local context, key, defined, base URL,
435					// and the value of the @protected entry from context, if any, for protected.
436					// (and the value of override protected)
437					if context.type_().is_some() {
438						self.define(
439							env,
440							&mut result,
441							&context,
442							JsonLdKeyOrKeywordRef::Keyword(JsonLdKeyword::Type),
443							protected,
444							entry_loc,
445						)
446						.await?
447					}
448
449					for (key, _binding) in context.bindings() {
450						let binding_loc = if context.key_is_imported(key) {
451							entry_loc.file(JsonLdSourceFileRef::original(import_url.as_deref()))
452						} else {
453							entry_loc
454						};
455
456						self.define(
457							env,
458							&mut result,
459							&context,
460							key.into(),
461							protected,
462							binding_loc,
463						)
464						.await?
465					}
466				}
467			}
468		}
469
470		Ok(result)
471	}
472}
473
474// /// Result of context processing functions.
475// pub type ProcessingResult<'a, T, B> = Result<Processed<'a, T, B>, Error>;
476
477// pub trait Process {
478// 	/// Process the local context with specific options.
479// 	#[allow(async_fn_in_trait)]
480// 	async fn process_full<N, L, W>(
481// 		&self,
482// 		vocabulary: &mut N,
483// 		active_context: &Context<N::Iri, N::BlankId>,
484// 		loader: &L,
485// 		base_url: Option<N::Iri>,
486// 		options: Options,
487// 		warnings: W,
488// 	) -> Result<Processed<N::Iri, N::BlankId>, Error>
489// 	where
490// 		N: VocabularyMut,
491// 		N::Iri: Clone + Eq + Hash,
492// 		N::BlankId: Clone + PartialEq,
493// 		L: Loader,
494// 		W: WarningHandler<N>;
495
496// 	/// Process the local context with specific options.
497// 	#[allow(clippy::type_complexity)]
498// 	#[allow(async_fn_in_trait)]
499// 	async fn process_with<N, L>(
500// 		&self,
501// 		vocabulary: &mut N,
502// 		active_context: &Context<N::Iri, N::BlankId>,
503// 		loader: &L,
504// 		base_url: Option<N::Iri>,
505// 		options: Options,
506// 	) -> Result<Processed<N::Iri, N::BlankId>, Error>
507// 	where
508// 		N: VocabularyMut,
509// 		N::Iri: Clone + Eq + Hash,
510// 		N::BlankId: Clone + PartialEq,
511// 		L: Loader,
512// 	{
513// 		self.process_full(
514// 			vocabulary,
515// 			active_context,
516// 			loader,
517// 			base_url,
518// 			options,
519// 			warning::Print,
520// 		)
521// 		.await
522// 	}
523
524// 	/// Process the local context with the given initial active context with the default options:
525// 	/// `is_remote` is `false`, `override_protected` is `false` and `propagate` is `true`.
526// 	#[allow(async_fn_in_trait)]
527// 	async fn process<N, L>(
528// 		&self,
529// 		vocabulary: &mut N,
530// 		loader: &L,
531// 		base_url: Option<N::Iri>,
532// 	) -> Result<Processed<N::Iri, N::BlankId>, Error>
533// 	where
534// 		N: VocabularyMut,
535// 		N::Iri: Clone + Eq + Hash,
536// 		N::BlankId: Clone + PartialEq,
537// 		L: Loader,
538// 	{
539// 		let active_context = Context::default();
540// 		self.process_full(
541// 			vocabulary,
542// 			&active_context,
543// 			loader,
544// 			base_url,
545// 			Options::default(),
546// 			warning::Print,
547// 		)
548// 		.await
549// 	}
550// }