json_ld_context_processing_next/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_next::{Context, Environment, ExtractContext, Loader, ProcessingMode, Term};
8use json_ld_syntax_next::{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}