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// }