oxirs_core/jsonld/
context.rs

1use super::error::{JsonLdErrorCode, JsonLdSyntaxError};
2use super::{JsonLdProcessingMode, JsonLdProfile, JsonLdProfileSet};
3use json_event_parser::{JsonEvent, JsonSyntaxError, SliceJsonParser};
4use oxiri::Iri;
5use std::borrow::Cow;
6use std::collections::hash_map::Entry;
7use std::collections::HashMap;
8use std::error::Error;
9use std::panic::{RefUnwindSafe, UnwindSafe};
10use std::slice;
11use std::sync::{Arc, Mutex};
12
13type LoadDocumentCallback = dyn Fn(
14        &str,
15        &JsonLdLoadDocumentOptions,
16    ) -> Result<JsonLdRemoteDocument, Box<dyn Error + Send + Sync>>
17    + Send
18    + Sync
19    + UnwindSafe
20    + RefUnwindSafe;
21
22// Type alias for the complex remote context cache type
23type RemoteContextCache = Arc<Mutex<HashMap<String, (Option<Iri<String>>, JsonNode)>>>;
24
25#[derive(Eq, PartialEq, Debug, Clone)]
26pub enum JsonNode {
27    String(String),
28    Number(String),
29    Boolean(bool),
30    Null,
31    Array(Vec<JsonNode>),
32    Object(HashMap<String, JsonNode>),
33}
34
35#[derive(Default, Clone)]
36pub struct JsonLdContext {
37    pub base_iri: Option<Iri<String>>,
38    pub original_base_url: Option<Iri<String>>,
39    pub vocabulary_mapping: Option<String>,
40    pub default_language: Option<String>,
41    pub default_direction: Option<&'static str>,
42    pub term_definitions: HashMap<String, JsonLdTermDefinition>,
43    pub previous_context: Option<Box<JsonLdContext>>,
44}
45
46impl JsonLdContext {
47    pub fn new_empty(original_base_url: Option<Iri<String>>) -> Self {
48        JsonLdContext {
49            base_iri: original_base_url.clone(),
50            original_base_url,
51            vocabulary_mapping: None,
52            default_language: None,
53            default_direction: None,
54            term_definitions: HashMap::new(),
55            previous_context: None,
56        }
57    }
58}
59
60#[derive(Clone)]
61pub struct JsonLdTermDefinition {
62    // In the fields, None is unset Some(None) is set to null
63    pub iri_mapping: Option<Option<String>>,
64    pub prefix_flag: bool,
65    pub protected: bool,
66    pub reverse_property: bool,
67    pub base_url: Option<Iri<String>>,
68    pub context: Option<JsonNode>,
69    pub container_mapping: &'static [&'static str],
70    pub direction_mapping: Option<Option<&'static str>>,
71    pub index_mapping: Option<String>,
72    pub language_mapping: Option<Option<String>>,
73    pub nest_value: Option<String>,
74    pub type_mapping: Option<String>,
75}
76
77pub struct JsonLdContextProcessor {
78    pub processing_mode: JsonLdProcessingMode,
79    pub lenient: bool, // Custom option to ignore invalid base IRIs
80    pub max_context_recursion: usize,
81    pub remote_context_cache: RemoteContextCache,
82    pub load_document_callback: Option<Arc<LoadDocumentCallback>>,
83}
84
85/// Used to pass various options to the LoadDocumentCallback.
86pub struct JsonLdLoadDocumentOptions {
87    /// One or more IRIs to use in the request as a profile parameter.
88    pub request_profile: JsonLdProfileSet,
89}
90
91/// Returned information about a remote JSON-LD document or context.
92pub struct JsonLdRemoteDocument {
93    /// The retrieved document
94    pub document: Vec<u8>,
95    /// The final URL of the loaded document. This is important to handle HTTP redirects properly
96    pub document_url: String,
97}
98
99impl JsonLdContextProcessor {
100    /// [Context Processing Algorithm](https://www.w3.org/TR/json-ld-api/#algorithm)
101    #[allow(clippy::too_many_arguments)]
102    pub fn process_context(
103        &self,
104        active_context: &JsonLdContext,
105        local_context: JsonNode,
106        base_url: Option<&Iri<String>>,
107        remote_contexts: &mut Vec<String>,
108        override_protected: bool,
109        mut propagate: bool,
110        validate_scoped_context: bool,
111        errors: &mut Vec<JsonLdSyntaxError>,
112    ) -> JsonLdContext {
113        // 1)
114        let mut result = active_context.clone();
115        // 2)
116        if let JsonNode::Object(local_context) = &local_context {
117            if let Some(propagate_node) = local_context.get("@propagate") {
118                if let JsonNode::Boolean(new) = propagate_node {
119                    propagate = *new;
120                } else {
121                    errors.push(JsonLdSyntaxError::msg("@propagate value must be a boolean"))
122                }
123            }
124        }
125        // 3)
126        if !propagate && result.previous_context.is_none() {
127            result.previous_context = Some(Box::new(active_context.clone()));
128        }
129        // 4)
130        let local_context = if let JsonNode::Array(c) = local_context {
131            c
132        } else {
133            vec![local_context]
134        };
135        // 5)
136        for context in local_context {
137            let mut context = match context {
138                // 5.1)
139                JsonNode::Null => {
140                    // 5.1.1)
141                    if !override_protected {
142                        for (name, def) in &active_context.term_definitions {
143                            if def.protected {
144                                errors.push(JsonLdSyntaxError::msg_and_code(format!("Definition of {name} will be overridden even if it's protected"), JsonLdErrorCode::InvalidContextNullification));
145                            }
146                        }
147                    }
148                    // 5.1.2)
149                    result = JsonLdContext::new_empty(active_context.original_base_url.clone());
150                    // 5.1.3)
151                    continue;
152                }
153                // 5.2)
154                JsonNode::String(context) => {
155                    // 5.2.1)
156                    let context = match if let Some(base_url) = base_url {
157                        base_url.resolve(&context)
158                    } else {
159                        Iri::parse(context.clone())
160                    } {
161                        Ok(url) => url.into_inner(),
162                        Err(e) => {
163                            errors.push(JsonLdSyntaxError::msg_and_code(
164                                format!("Invalid remote context URL '{context}': {e}"),
165                                JsonLdErrorCode::LoadingDocumentFailed,
166                            ));
167                            continue;
168                        }
169                    };
170                    // 5.2.2)
171                    if !validate_scoped_context && remote_contexts.contains(&context) {
172                        continue;
173                    }
174                    // 5.2.3)
175                    if remote_contexts.len() >= self.max_context_recursion {
176                        errors.push(JsonLdSyntaxError::msg_and_code(
177                            format!(
178                                "This processor only allows {} remote context, threshold exceeded",
179                                self.max_context_recursion
180                            ),
181                            JsonLdErrorCode::ContextOverflow,
182                        ));
183                        continue;
184                    }
185                    remote_contexts.push(context.clone());
186                    let (loaded_context_base, loaded_context_content) =
187                        match self.load_remote_context(&context) {
188                            Ok(r) => r,
189                            Err(e) => {
190                                errors.push(e);
191                                continue;
192                            }
193                        };
194                    // 5.2.6)
195                    result = self.process_context(
196                        &result,
197                        loaded_context_content,
198                        loaded_context_base.as_ref(),
199                        remote_contexts,
200                        false,
201                        true,
202                        validate_scoped_context,
203                        errors,
204                    );
205                    assert_eq!(
206                        remote_contexts.pop(),
207                        Some(context),
208                        "The remote context stack is invalid"
209                    );
210                    continue;
211                }
212                // 5.3)
213                JsonNode::Array(_) | JsonNode::Number(_) | JsonNode::Boolean(_) => {
214                    errors.push(JsonLdSyntaxError::msg_and_code(
215                        "@context value must be null, a string or an object",
216                        JsonLdErrorCode::InvalidLocalContext,
217                    ));
218                    continue;
219                }
220                // 5.4)
221                JsonNode::Object(context) => context,
222            };
223            let mut protected = false;
224            // 5.5)
225            if let Some(value) = context.remove("@version") {
226                // 5.5.1)
227                if let JsonNode::Number(version) = value {
228                    if version != "1.1" {
229                        errors.push(JsonLdSyntaxError::msg_and_code(
230                            format!("The only supported @version value is 1.1, found {version}"),
231                            JsonLdErrorCode::InvalidVersionValue,
232                        ));
233                    }
234                } else {
235                    errors.push(JsonLdSyntaxError::msg_and_code(
236                        "@version value must be a number",
237                        JsonLdErrorCode::InvalidVersionValue,
238                    ));
239                }
240                // 5.5.2)
241                if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
242                    errors.push(JsonLdSyntaxError::msg_and_code(
243                        "@version is only supported in JSON-LD 1.1",
244                        JsonLdErrorCode::ProcessingModeConflict,
245                    ));
246                }
247            }
248            // 5.6)
249            if let Some(value) = context.remove("@import") {
250                // 5.6.1)
251                if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
252                    errors.push(JsonLdSyntaxError::msg_and_code(
253                        "@import is only supported in JSON-LD 1.1",
254                        JsonLdErrorCode::InvalidContextEntry,
255                    ));
256                }
257                // 5.6.2)
258                let JsonNode::String(import) = value else {
259                    errors.push(JsonLdSyntaxError::msg_and_code(
260                        "@import must be a string",
261                        JsonLdErrorCode::InvalidImportValue,
262                    ));
263                    continue;
264                };
265                // 5.6.3)
266                let import = match if let Some(base_url) = base_url {
267                    base_url.resolve(&import)
268                } else {
269                    Iri::parse(import.clone())
270                } {
271                    Ok(import) => import,
272                    Err(e) => {
273                        errors.push(JsonLdSyntaxError::msg_and_code(
274                            format!("Invalid @import iri {import}: {e}"),
275                            JsonLdErrorCode::InvalidImportValue,
276                        ));
277                        continue;
278                    }
279                };
280                // 5.6.4)
281                let (_, loaded_context_content) = match self.load_remote_context(import.as_str()) {
282                    Ok(r) => r,
283                    Err(e) => {
284                        errors.push(e);
285                        continue;
286                    }
287                };
288                // 5.6.6)
289                let JsonNode::Object(loaded_context_content) = loaded_context_content else {
290                    errors.push(JsonLdSyntaxError::msg_and_code(
291                        format!("Imported context {import} must be an object"),
292                        JsonLdErrorCode::InvalidRemoteContext,
293                    ));
294                    continue;
295                };
296                // 5.6.7)
297                if loaded_context_content.contains_key("@import") {
298                    errors.push(JsonLdSyntaxError::msg_and_code(
299                        format!("Imported context {import} must not contain an @import key"),
300                        JsonLdErrorCode::InvalidContextEntry,
301                    ));
302                    continue;
303                }
304                // 5.6.8)
305                for (key, value) in loaded_context_content {
306                    if let Entry::Vacant(e) = context.entry(key) {
307                        e.insert(value);
308                    }
309                }
310            }
311            // 5.7)
312            if let Some(value) = context.remove("@base") {
313                if remote_contexts.is_empty() {
314                    match value {
315                        // 5.7.2)
316                        JsonNode::Null => {
317                            result.base_iri = None;
318                        }
319                        // 5.7.3) and 5.7.4)
320                        JsonNode::String(value) => {
321                            if self.lenient {
322                                result.base_iri = Some(if let Some(base_iri) = &result.base_iri {
323                                    Iri::parse_unchecked(
324                                        base_iri.resolve_unchecked(&value).to_string(),
325                                    )
326                                } else {
327                                    Iri::parse_unchecked(value.clone())
328                                })
329                            } else {
330                                match if let Some(base_iri) = &result.base_iri {
331                                    base_iri.resolve(&value)
332                                } else {
333                                    Iri::parse(value.clone())
334                                } {
335                                    Ok(iri) => result.base_iri = Some(iri),
336                                    Err(e) => errors.push(JsonLdSyntaxError::msg_and_code(
337                                        format!("Invalid @base '{value}': {e}"),
338                                        JsonLdErrorCode::InvalidBaseIri,
339                                    )),
340                                }
341                            }
342                        }
343                        _ => errors.push(JsonLdSyntaxError::msg_and_code(
344                            "@base value must be a string",
345                            JsonLdErrorCode::InvalidBaseIri,
346                        )),
347                    }
348                }
349            }
350            // 5.8)
351            if let Some(value) = context.remove("@vocab") {
352                match value {
353                    // 5.8.2)
354                    JsonNode::Null => {
355                        result.vocabulary_mapping = None;
356                    }
357                    // 5.8.3)
358                    JsonNode::String(value) => {
359                        if let Some(vocab) = self
360                            .expand_iri(
361                                &mut result,
362                                value.as_str().into(),
363                                true,
364                                true,
365                                None,
366                                &mut HashMap::new(),
367                                errors,
368                            )
369                            .filter(|iri| !has_keyword_form(iri))
370                        {
371                            result.vocabulary_mapping = Some(vocab.into());
372                        } else {
373                            errors.push(JsonLdSyntaxError::msg_and_code(
374                                format!("Invalid @vocab '{value}'"),
375                                JsonLdErrorCode::InvalidVocabMapping,
376                            ));
377                        };
378                    }
379                    _ => errors.push(JsonLdSyntaxError::msg_and_code(
380                        "@vocab value must be a string",
381                        JsonLdErrorCode::InvalidVocabMapping,
382                    )),
383                }
384            }
385            // 5.9)
386            if let Some(value) = context.remove("@language") {
387                match value {
388                    // 5.9.2)
389                    JsonNode::Null => {
390                        result.default_language = None;
391                    }
392                    // 5.9.3)
393                    JsonNode::String(value) => result.default_language = Some(value),
394                    _ => errors.push(JsonLdSyntaxError::msg_and_code(
395                        "@language value must be a string or null",
396                        JsonLdErrorCode::InvalidDefaultLanguage,
397                    )),
398                }
399            }
400            // 5.10)
401            if let Some(value) = context.remove("@direction") {
402                // 5.10.1)
403                if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
404                    errors.push(JsonLdSyntaxError::msg_and_code(
405                        "@direction is only supported in JSON-LD 1.1",
406                        JsonLdErrorCode::InvalidContextEntry,
407                    ));
408                }
409                match value {
410                    // 5.10.3)
411                    JsonNode::Null => {
412                        result.default_direction = None;
413                    }
414                    // 5.10.4)
415                    JsonNode::String(value) => match value.as_str() {
416                        "ltr" => result.default_direction = Some("ltr"),
417                        "rtl" => result.default_direction = Some("rtl"),
418                        _ => errors.push(JsonLdSyntaxError::msg_and_code(
419                            format!("@direction value must be 'ltr' or 'rtl', found '{value}'"),
420                            JsonLdErrorCode::InvalidBaseDirection,
421                        )),
422                    },
423                    _ => errors.push(JsonLdSyntaxError::msg_and_code(
424                        "@direction value must be a string or null",
425                        JsonLdErrorCode::InvalidBaseDirection,
426                    )),
427                }
428            }
429            // 5.11)
430            if let Some(value) = context.remove("@propagate") {
431                // 5.11.1)
432                if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
433                    errors.push(JsonLdSyntaxError::msg_and_code(
434                        "@propagate is only supported in JSON-LD 1.1",
435                        JsonLdErrorCode::InvalidContextEntry,
436                    ));
437                }
438                // 5.11.2)
439                if !matches!(value, JsonNode::Boolean(_)) {
440                    errors.push(JsonLdSyntaxError::msg_and_code(
441                        "@propagate value must be a boolean",
442                        JsonLdErrorCode::InvalidPropagateValue,
443                    ));
444                    continue;
445                };
446            }
447            // 5.13)
448            if let Some(value) = context.remove("@protected") {
449                if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
450                    errors.push(JsonLdSyntaxError::msg_and_code(
451                        "@protected is only supported in JSON-LD 1.1",
452                        JsonLdErrorCode::InvalidContextEntry,
453                    ));
454                }
455                if let JsonNode::Boolean(value) = value {
456                    protected = value
457                } else {
458                    errors.push(JsonLdSyntaxError::msg_and_code(
459                        "@protected value must be a boolean",
460                        JsonLdErrorCode::InvalidProtectedValue,
461                    ))
462                }
463            }
464            let mut defined = HashMap::new();
465            for term in context.keys() {
466                self.create_term_definition(
467                    &mut result,
468                    &context,
469                    term,
470                    &mut defined,
471                    base_url,
472                    protected,
473                    override_protected,
474                    remote_contexts,
475                    errors,
476                )
477            }
478        }
479        // 6)
480        result
481    }
482
483    /// [Create Term Definition](https://www.w3.org/TR/json-ld-api/#create-term-definition)
484    #[allow(clippy::too_many_arguments)]
485    fn create_term_definition(
486        &self,
487        active_context: &mut JsonLdContext,
488        local_context: &HashMap<String, JsonNode>,
489        term: &str,
490        defined: &mut HashMap<String, bool>,
491        base_url: Option<&Iri<String>>,
492        protected: bool,
493        override_protected: bool,
494        remote_contexts: &mut Vec<String>,
495        errors: &mut Vec<JsonLdSyntaxError>,
496    ) {
497        // 1)
498        if let Some(defined_value) = defined.get(term) {
499            if !defined_value {
500                errors.push(JsonLdSyntaxError::msg_and_code(
501                    "Cyclic IRI mapping",
502                    JsonLdErrorCode::CyclicIriMapping,
503                ))
504            }
505            return;
506        }
507        // 2)
508        if term.is_empty() {
509            errors.push(JsonLdSyntaxError::msg_and_code(
510                "@context terms must not be the empty strings",
511                JsonLdErrorCode::InvalidTermDefinition,
512            ));
513            return;
514        }
515        defined.insert(term.into(), false);
516        // 3)
517        let Some(value) = local_context.get(term) else {
518            unreachable!();
519        };
520        // 4)
521        if term == "@type" {
522            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
523                errors.push(JsonLdSyntaxError::msg_and_code(
524                    "@type keyword can't be redefined in JSON-LD 1.0 @context",
525                    JsonLdErrorCode::KeywordRedefinition,
526                ));
527            }
528            if let JsonNode::Object(value) = value {
529                if value.is_empty() {
530                    errors.push(JsonLdSyntaxError::msg_and_code(
531                        "@type keyword definition can't be empty",
532                        JsonLdErrorCode::KeywordRedefinition,
533                    ));
534                    return;
535                }
536                for (key, key_value) in value {
537                    match key.as_str() {
538                        "@protected" => (),
539                        "@container" => match key_value {
540                            JsonNode::String(s) if s == "@set" => (),
541                            JsonNode::Array(s)
542                                if s.iter().all(|v| {
543                                    if let JsonNode::String(s) = v {
544                                        s == "@set"
545                                    } else {
546                                        false
547                                    }
548                                }) => {}
549                            _ => {
550                                errors.push(JsonLdSyntaxError::msg_and_code(
551                                    "@type definition only allowed @container is @set",
552                                    JsonLdErrorCode::KeywordRedefinition,
553                                ));
554                                return;
555                            }
556                        },
557                        _ => {
558                            errors.push(JsonLdSyntaxError::msg_and_code(
559                                format!("@type definition can only contain @protected and @container keywords, {key} found"),
560                                JsonLdErrorCode::KeywordRedefinition,
561                            ));
562                            return;
563                        }
564                    }
565                }
566            } else {
567                errors.push(JsonLdSyntaxError::msg_and_code(
568                    "@type definition must be an object",
569                    JsonLdErrorCode::KeywordRedefinition,
570                ));
571                return;
572            }
573        } else if has_keyword_form(term) {
574            // 5)
575            if is_keyword(term) {
576                errors.push(JsonLdSyntaxError::msg_and_code(
577                    format!("{term} keyword can't be redefined in context"),
578                    JsonLdErrorCode::KeywordRedefinition,
579                ));
580            }
581            return;
582        }
583        // 6)
584        let previous_definition = active_context.term_definitions.remove(term);
585        let value = match value {
586            // 7)
587            JsonNode::Null => Cow::Owned([("@id".to_owned(), JsonNode::Null)].into()),
588            // 8)
589            JsonNode::String(id) => {
590                Cow::Owned([("@id".to_owned(), JsonNode::String(id.clone()))].into())
591            }
592            // 9)
593            JsonNode::Object(map) => Cow::Borrowed(map),
594            _ => {
595                errors.push(JsonLdSyntaxError::msg_and_code(
596                    "Term definition value must be null, a string or a map",
597                    JsonLdErrorCode::InvalidTermDefinition,
598                ));
599                return;
600            }
601        };
602        // 10)
603        let mut definition = JsonLdTermDefinition {
604            iri_mapping: None,
605            prefix_flag: false,
606            protected,
607            reverse_property: false,
608            base_url: None,
609            context: None,
610            container_mapping: &[],
611            direction_mapping: None,
612            index_mapping: None,
613            language_mapping: None,
614            nest_value: None,
615            type_mapping: None,
616        };
617        // 11)
618        if let Some(key_value) = value.get("@protected") {
619            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
620                errors.push(JsonLdSyntaxError::msg_and_code(
621                    "@protected keyword can't be used in JSON-LD 1.0 @context",
622                    JsonLdErrorCode::InvalidTermDefinition,
623                ));
624            }
625            let JsonNode::Boolean(key_value) = key_value else {
626                errors.push(JsonLdSyntaxError::msg_and_code(
627                    "@protected value must be a boolean",
628                    JsonLdErrorCode::InvalidProtectedValue,
629                ));
630                return;
631            };
632            definition.protected = *key_value;
633        }
634        // 12)
635        if let Some(key_value) = value.get("@type") {
636            // 12.1)
637            let JsonNode::String(r#type) = key_value else {
638                errors.push(JsonLdSyntaxError::msg_and_code(
639                    "The value of @type in a context must be a string",
640                    JsonLdErrorCode::InvalidTypeMapping,
641                ));
642                return;
643            };
644            // 12.2)
645            let Some(r#type) = self.expand_iri(
646                active_context,
647                r#type.as_str().into(),
648                false,
649                true,
650                Some(local_context),
651                defined,
652                errors,
653            ) else {
654                errors.push(JsonLdSyntaxError::msg_and_code(
655                    format!("Invalid @type value in context: {type}"),
656                    JsonLdErrorCode::InvalidTypeMapping,
657                ));
658                return;
659            };
660            // 12.3)
661            if matches!(r#type.as_ref(), "@json" | "@none")
662                && self.processing_mode == JsonLdProcessingMode::JsonLd1_0
663            {
664                errors.push(JsonLdSyntaxError::msg_and_code(
665                    format!("@type value {type} in a context is only supported in JSON-LD 1.1"),
666                    JsonLdErrorCode::InvalidTypeMapping,
667                ));
668            }
669            // 12.4)
670            let is_keyword = has_keyword_form(&r#type);
671            if is_keyword && !matches!(r#type.as_ref(), "@id" | "@json" | "@none" | "@vocab")
672                || r#type.starts_with("_:")
673            {
674                errors.push(JsonLdSyntaxError::msg_and_code(
675                    format!("Invalid @type value in context: {type}"),
676                    JsonLdErrorCode::InvalidTypeMapping,
677                ));
678            }
679            if !self.lenient && !is_keyword {
680                if let Err(e) = Iri::parse(r#type.as_ref()) {
681                    errors.push(JsonLdSyntaxError::msg_and_code(
682                        format!("Invalid @type iri '{type}': {e}"),
683                        JsonLdErrorCode::InvalidTypeMapping,
684                    ));
685                }
686            }
687            // 12.5)
688            definition.type_mapping = Some(r#type.into());
689        }
690        // 13)
691        if let Some(key_value) = value.get("@reverse") {
692            // 13.1)
693            if value.contains_key("@id") {
694                errors.push(JsonLdSyntaxError::msg_and_code(
695                    "@reverse and @id cannot be used together in a context",
696                    JsonLdErrorCode::InvalidReverseProperty,
697                ));
698                return;
699            }
700            if value.contains_key("@nest") {
701                errors.push(JsonLdSyntaxError::msg_and_code(
702                    "@reverse and @nest cannot be used together in a context",
703                    JsonLdErrorCode::InvalidReverseProperty,
704                ));
705                return;
706            }
707            // 13.2)
708            let JsonNode::String(key_value) = key_value else {
709                errors.push(JsonLdSyntaxError::msg_and_code(
710                    "@reverse value must be a string in a context",
711                    JsonLdErrorCode::InvalidIriMapping,
712                ));
713                return;
714            };
715            // 13.4)
716            if let Some(iri) = self.expand_iri(
717                active_context,
718                key_value.into(),
719                false,
720                true,
721                Some(local_context),
722                defined,
723                errors,
724            ) {
725                if self.lenient && !has_keyword_form(&iri)
726                    || !self.lenient && (iri.starts_with("_:") || Iri::parse(iri.as_ref()).is_ok())
727                {
728                    definition.iri_mapping = Some(Some(iri.into()));
729                } else {
730                    errors.push(JsonLdSyntaxError::msg_and_code(
731                        format!("{iri} is not a valid IRI or blank node"),
732                        JsonLdErrorCode::InvalidIriMapping,
733                    ));
734                    definition.iri_mapping = Some(None);
735                }
736            } else {
737                definition.iri_mapping = Some(None);
738            }
739            definition.iri_mapping = Some(
740                self.expand_iri(
741                    active_context,
742                    key_value.into(),
743                    false,
744                    true,
745                    Some(local_context),
746                    defined,
747                    errors,
748                )
749                .map(Into::into),
750            );
751            // 13.5)
752            if let Some(container_entry) = value.get("@container") {
753                match container_entry {
754                    JsonNode::Null => (),
755                    JsonNode::String(container_entry) => {
756                        if !matches!(container_entry.as_str(), "@index" | "@set") {
757                            errors.push(JsonLdSyntaxError::msg_and_code(
758                                "@reverse is only compatible with @index and @set containers",
759                                JsonLdErrorCode::InvalidReverseProperty,
760                            ));
761                        }
762                    }
763                    _ => {
764                        errors.push(JsonLdSyntaxError::msg_and_code(
765                            "@container value must be a string or null",
766                            JsonLdErrorCode::InvalidReverseProperty,
767                        ));
768                    }
769                }
770            }
771            // 13.6)
772            definition.reverse_property = true;
773        } else if let Some(key_value) = value.get("@id").filter(|v| {
774            if let JsonNode::String(v) = v {
775                v != term
776            } else {
777                true
778            }
779        }) {
780            // 14)
781            match key_value {
782                // 14.1)
783                JsonNode::Null => {
784                    definition.iri_mapping = Some(None);
785                }
786                JsonNode::String(id) => {
787                    if id == term {
788                        return;
789                    }
790                    let Some(expanded) = self.expand_iri(
791                        active_context,
792                        id.into(),
793                        false,
794                        true,
795                        Some(local_context),
796                        defined,
797                        errors,
798                    ) else {
799                        // 14.2.2)
800                        definition.iri_mapping = Some(None);
801                        return;
802                    };
803                    // 14.2.3)
804                    if expanded == "@context" {
805                        errors.push(JsonLdSyntaxError::msg_and_code(
806                            "@context cannot be aliased with @id: @context",
807                            JsonLdErrorCode::InvalidKeywordAlias,
808                        ));
809                        return;
810                    }
811                    definition.iri_mapping = Some(Some(expanded.into()));
812                    // 14.2.4)
813                    if term
814                        .as_bytes()
815                        .get(1..term.len() - 1)
816                        .is_some_and(|t| t.contains(&b':'))
817                        || term.contains('/')
818                    {
819                        // 14.2.4.1)
820                        defined.insert(term.into(), true);
821                        // 14.2.4.2)
822                        let expended_term = self.expand_iri(
823                            active_context,
824                            term.into(),
825                            false,
826                            true,
827                            Some(local_context),
828                            defined,
829                            errors,
830                        );
831                        if expended_term.as_deref()
832                            != definition.iri_mapping.as_ref().and_then(|o| o.as_deref())
833                        {
834                            errors.push(JsonLdSyntaxError::msg_and_code(
835                                if let (Some(expended_term), Some(Some(iri_mapping))) = (&expended_term, &definition.iri_mapping) {
836                                    format!("Inconsistent expansion of {term} between {expended_term} and {iri_mapping}")
837                                } else {
838                                    format!("Inconsistent expansion of {term}")
839                                },
840                                JsonLdErrorCode::InvalidIriMapping,
841                            ))
842                        }
843                    }
844                    // 14.2.5)
845                    if !term.contains(':')
846                        && !term.contains('/')
847                        && definition.iri_mapping.as_ref().is_some_and(|iri| {
848                            iri.as_ref().is_some_and(|iri| {
849                                iri.ends_with(|c| {
850                                    matches!(c, ':' | '/' | '?' | '#' | '[' | ']' | '@')
851                                }) || iri.starts_with("_:")
852                            })
853                        })
854                    {
855                        definition.prefix_flag = true;
856                    }
857                }
858                // 14.2.1)
859                _ => {
860                    definition.iri_mapping = Some(None);
861                    errors.push(JsonLdSyntaxError::msg_and_code(
862                        "@id value must be a string",
863                        JsonLdErrorCode::InvalidIriMapping,
864                    ))
865                }
866            }
867        } else if let Some((prefix, suffix)) = term.split_once(':').and_then(|(prefix, suffix)| {
868            if prefix.is_empty() {
869                // We ignore the empty prefixes
870                suffix.split_once(':')
871            } else {
872                Some((prefix, suffix))
873            }
874        }) {
875            // 15)
876            if local_context.contains_key(prefix) {
877                // 15.1)
878                self.create_term_definition(
879                    active_context,
880                    local_context,
881                    prefix,
882                    defined,
883                    base_url,
884                    false,
885                    false,
886                    remote_contexts,
887                    errors,
888                )
889            }
890            if let Some(term_definition) = active_context.term_definitions.get(prefix) {
891                // 15.2)
892                if let Some(Some(iri_mapping)) = &term_definition.iri_mapping {
893                    definition.iri_mapping = Some(Some(format!("{iri_mapping}{suffix}")));
894                } else {
895                    errors.push(JsonLdSyntaxError::msg(format!(
896                        "The prefix '{prefix}' is not associated with an IRI in the context"
897                    )));
898                }
899            } else {
900                // 15.3)
901                definition.iri_mapping = Some(Some(term.into()));
902            }
903        } else if term.contains('/') {
904            // 16)
905            let iri = match if let Some(base_url) = base_url {
906                base_url.resolve(term)
907            } else {
908                Iri::parse(term.to_owned())
909            } {
910                Ok(iri) => iri.into_inner(),
911                Err(e) => {
912                    errors.push(JsonLdSyntaxError::msg_and_code(
913                        format!("Invalid term relative IRI '{term}': {e}"),
914                        JsonLdErrorCode::InvalidIriMapping,
915                    ));
916                    return;
917                }
918            };
919            definition.iri_mapping = Some(Some(iri));
920        } else if term == "@type" {
921            // 17)
922            definition.iri_mapping = Some(Some("@type".into()));
923        } else {
924            // 18)
925            if let Some(vocabulary_mapping) = &active_context.vocabulary_mapping {
926                definition.iri_mapping = Some(Some(format!("{vocabulary_mapping}{term}")));
927            } else {
928                errors.push(JsonLdSyntaxError::msg_and_code(
929                    format!("No @vocab key to build an IRI from context {term} term definition"),
930                    JsonLdErrorCode::InvalidIriMapping,
931                ))
932            }
933        }
934        // 19)
935        if let Some(key_value) = value.get("@container") {
936            const ALLOWED_CONTAINER_MAPPINGS: &[&[&str]] = &[
937                &["@index"],
938                &["@language"],
939                &["@list"],
940                &["@set"],
941                &["@index", "@set"],
942                &["@language", "@set"],
943                &["@graph"],
944                &["@graph", "@id"],
945                &["@graph", "@index"],
946                &["@graph", "@id", "@set"],
947                &["@graph", "@index", "@set"],
948                &["@id"],
949                &["@id", "@set"],
950                &["@type"],
951                &["@type", "@set"],
952            ];
953
954            // 19.1)
955            let mut container_mapping = Vec::new();
956            for value in if let JsonNode::Array(value) = key_value {
957                if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
958                    errors.push(JsonLdSyntaxError::msg_and_code(
959                                    "@container definition with multiple values is not supported in JSON-LD 1.0",
960                                    JsonLdErrorCode::InvalidContainerMapping,
961                                ));
962                }
963                value.as_slice()
964            } else {
965                slice::from_ref(key_value)
966            } {
967                if let JsonNode::String(container) = value {
968                    container_mapping.push(container.as_str());
969                } else {
970                    errors.push(JsonLdSyntaxError::msg_and_code(
971                        "@container value must be a string or an array of strings",
972                        JsonLdErrorCode::InvalidContainerMapping,
973                    ));
974                }
975            }
976            container_mapping.sort_unstable();
977            let Some(container_mapping) = ALLOWED_CONTAINER_MAPPINGS
978                .iter()
979                .find_map(|c| (*c == container_mapping).then_some(*c))
980            else {
981                errors.push(JsonLdSyntaxError::msg_and_code(
982                    "Not supported @container value combination",
983                    JsonLdErrorCode::InvalidContainerMapping,
984                ));
985                return;
986            };
987            // 19.2)
988            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
989                if let Some(bad) = ["@graph", "@id", "@type"]
990                    .into_iter()
991                    .find(|k| container_mapping.contains(k))
992                {
993                    errors.push(JsonLdSyntaxError::msg_and_code(
994                        format!("{bad} container is not supported in JSON-LD 1.0"),
995                        JsonLdErrorCode::InvalidContainerMapping,
996                    ));
997                }
998            }
999            // 19.3)
1000            definition.container_mapping = container_mapping;
1001            // 19.4)
1002            if container_mapping.contains(&"@type") {
1003                if let Some(type_mapping) = &definition.type_mapping {
1004                    if !["@id", "@vocab"].contains(&type_mapping.as_str()) {
1005                        errors.push(JsonLdSyntaxError::msg_and_code(
1006                                    format!("Type mapping must be @id or @vocab, not {type_mapping} when used with @type container"),
1007                                    JsonLdErrorCode::InvalidContainerMapping,
1008                                ));
1009                    }
1010                } else {
1011                    definition.type_mapping = Some("@id".into());
1012                }
1013            }
1014        }
1015        // 20)
1016        if let Some(key_value) = value.get("@index") {
1017            // 20.1)
1018            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
1019                errors.push(JsonLdSyntaxError::msg_and_code(
1020                    "@index inside of term definitions is only supported in JSON-LD 1.1",
1021                    JsonLdErrorCode::InvalidTermDefinition,
1022                ));
1023            }
1024            if !definition.container_mapping.contains(&"@index") {
1025                errors.push(JsonLdSyntaxError::msg_and_code(
1026                    "@index inside of term definitions is only allowed when @container is set to @index",
1027                    JsonLdErrorCode::InvalidTermDefinition,
1028                ));
1029            }
1030            // 20.2)
1031            let JsonNode::String(index) = key_value else {
1032                errors.push(JsonLdSyntaxError::msg_and_code(
1033                    "@index value must be a string",
1034                    JsonLdErrorCode::InvalidTermDefinition,
1035                ));
1036                return;
1037            };
1038            let Some(index) = self.expand_iri(
1039                active_context,
1040                index.into(),
1041                false,
1042                true,
1043                Some(local_context),
1044                defined,
1045                errors,
1046            ) else {
1047                errors.push(JsonLdSyntaxError::msg_and_code(
1048                    "@index value must be a valid IRI",
1049                    JsonLdErrorCode::InvalidTermDefinition,
1050                ));
1051                return;
1052            };
1053            if self.lenient && (has_keyword_form(&index) || index.starts_with("_:"))
1054                || !self.lenient && Iri::parse(index.as_ref()).is_err()
1055            {
1056                errors.push(JsonLdSyntaxError::msg_and_code(
1057                    "@index value must be a valid IRI",
1058                    JsonLdErrorCode::InvalidTermDefinition,
1059                ));
1060                return;
1061            }
1062            // 20.3)
1063            definition.index_mapping = Some(index.into());
1064        }
1065        // 21)
1066        if let Some(key_value) = value.get("@context") {
1067            // 21.1)
1068            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
1069                errors.push(JsonLdSyntaxError::msg_and_code(
1070                    "@context inside of term definitions is only supported in JSON-LD 1.1",
1071                    JsonLdErrorCode::InvalidTermDefinition,
1072                ));
1073            }
1074            // 21.2)
1075            let context = key_value;
1076            // 21.3)
1077            let error_count = errors.len();
1078            self.process_context(
1079                active_context,
1080                context.clone(),
1081                base_url,
1082                remote_contexts,
1083                true,
1084                true,
1085                false,
1086                errors,
1087            );
1088            for error in errors.drain(error_count..).collect::<Vec<_>>() {
1089                errors.push(JsonLdSyntaxError::msg_and_code(
1090                    format!("Invalid scoped context: {error}"),
1091                    JsonLdErrorCode::InvalidScopedContext,
1092                ));
1093            }
1094            // 21.4)
1095            definition.context = Some(context.clone());
1096            definition.base_url = base_url.cloned();
1097        }
1098        // 22)
1099        if let Some(key_value) = value.get("@language") {
1100            if value.contains_key("@type") {
1101                errors.push(JsonLdSyntaxError::msg_and_code(
1102                    "Both @language and @type can't be set at the same time",
1103                    JsonLdErrorCode::InvalidLanguageMapping,
1104                ));
1105            }
1106            definition.language_mapping = Some(match key_value {
1107                JsonNode::String(language) => Some(language.clone()),
1108                JsonNode::Null => None,
1109                _ => {
1110                    errors.push(JsonLdSyntaxError::msg_and_code(
1111                        "@language value must be a string or null",
1112                        JsonLdErrorCode::InvalidLanguageMapping,
1113                    ));
1114                    return;
1115                }
1116            })
1117        }
1118        // 23)
1119        if let Some(key_value) = value.get("@direction") {
1120            // 23.1)
1121            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
1122                errors.push(JsonLdSyntaxError::msg_and_code(
1123                    "@direction is only supported in JSON-LD 1.1",
1124                    JsonLdErrorCode::InvalidTermDefinition,
1125                ));
1126            }
1127            match key_value {
1128                // 5.10.3)
1129                JsonNode::Null => {
1130                    definition.direction_mapping = Some(None);
1131                }
1132                // 5.10.4)
1133                JsonNode::String(value) => match value.as_str() {
1134                    "ltr" => definition.direction_mapping = Some(Some("ltr")),
1135                    "rtl" => definition.direction_mapping = Some(Some("rtl")),
1136                    _ => errors.push(JsonLdSyntaxError::msg_and_code(
1137                        format!("@direction value must be 'ltr' or 'rtl', found '{value}'"),
1138                        JsonLdErrorCode::InvalidBaseDirection,
1139                    )),
1140                },
1141                _ => errors.push(JsonLdSyntaxError::msg_and_code(
1142                    "@direction value must be a string or null",
1143                    JsonLdErrorCode::InvalidBaseDirection,
1144                )),
1145            }
1146        }
1147        // 24)
1148        if let Some(key_value) = value.get("@nest") {
1149            // 24.1)
1150            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
1151                errors.push(JsonLdSyntaxError::msg_and_code(
1152                    "@nest is only supported in JSON-LD 1.1",
1153                    JsonLdErrorCode::InvalidTermDefinition,
1154                ));
1155            }
1156            // 24.2)
1157            let JsonNode::String(value) = key_value else {
1158                errors.push(JsonLdSyntaxError::msg_and_code(
1159                    "@nest value must be a string",
1160                    JsonLdErrorCode::InvalidNestValue,
1161                ));
1162                return;
1163            };
1164            if is_keyword(value) && value != "@nest" {
1165                errors.push(JsonLdSyntaxError::msg_and_code(
1166                    "@nest value must not be a keyword other than @nest",
1167                    JsonLdErrorCode::InvalidNestValue,
1168                ));
1169                return;
1170            }
1171            definition.nest_value = Some(value.into());
1172        }
1173        // 25)
1174        if let Some(key_value) = value.get("@prefix") {
1175            // 25.1)
1176            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
1177                errors.push(JsonLdSyntaxError::msg_and_code(
1178                    "@prefix is only supported in JSON-LD 1.1",
1179                    JsonLdErrorCode::InvalidTermDefinition,
1180                ));
1181            }
1182            if term.contains(':') {
1183                errors.push(JsonLdSyntaxError::msg_and_code(
1184                    format!("@prefix cannot be set on terms like {term} that contains a :"),
1185                    JsonLdErrorCode::InvalidTermDefinition,
1186                ));
1187                return;
1188            }
1189            if term.contains('/') {
1190                errors.push(JsonLdSyntaxError::msg_and_code(
1191                    format!("@prefix cannot be set on terms like {term} that contains a /"),
1192                    JsonLdErrorCode::InvalidTermDefinition,
1193                ));
1194                return;
1195            }
1196            // 25.2)
1197            let JsonNode::Boolean(value) = key_value else {
1198                errors.push(JsonLdSyntaxError::msg_and_code(
1199                    "@prefix value must be a boolean",
1200                    JsonLdErrorCode::InvalidPrefixValue,
1201                ));
1202                return;
1203            };
1204            definition.prefix_flag = *value;
1205            // 25.3)
1206            if definition.prefix_flag
1207                && definition
1208                    .iri_mapping
1209                    .as_ref()
1210                    .is_some_and(|d| d.as_ref().is_some_and(|d| is_keyword(d)))
1211            {
1212                errors.push(JsonLdSyntaxError::msg_and_code(
1213                    format!("@prefix cannot be set on terms like {term} that are keywords"),
1214                    JsonLdErrorCode::InvalidTermDefinition,
1215                ));
1216                return;
1217            }
1218        }
1219        // 26)
1220        if let Some(key) = value.keys().find(|k| {
1221            !matches!(
1222                k.as_str(),
1223                "@id"
1224                    | "@reverse"
1225                    | "@container"
1226                    | "@context"
1227                    | "@direction"
1228                    | "@index"
1229                    | "@language"
1230                    | "@nest"
1231                    | "@prefix"
1232                    | "@protected"
1233                    | "@type"
1234            )
1235        }) {
1236            errors.push(JsonLdSyntaxError::msg_and_code(
1237                format!("Unexpected key in term definition '{key}'"),
1238                JsonLdErrorCode::InvalidTermDefinition,
1239            ));
1240        }
1241        // 27)
1242        if !override_protected {
1243            if let Some(previous_definition) = previous_definition {
1244                if previous_definition.protected {
1245                    // 27.1)
1246                    if definition.iri_mapping != previous_definition.iri_mapping
1247                        || definition.prefix_flag != previous_definition.prefix_flag
1248                        || definition.reverse_property != previous_definition.reverse_property
1249                        || definition.base_url != previous_definition.base_url
1250                        || definition.context != previous_definition.context
1251                        || definition.container_mapping != previous_definition.container_mapping
1252                        || definition.direction_mapping != previous_definition.direction_mapping
1253                        || definition.index_mapping != previous_definition.index_mapping
1254                        || definition.language_mapping != previous_definition.language_mapping
1255                        || definition.nest_value != previous_definition.nest_value
1256                        || definition.type_mapping != previous_definition.type_mapping
1257                    {
1258                        // TODO: make sure it's full
1259                        errors.push(JsonLdSyntaxError::msg_and_code(
1260                            format!("Overriding the protected term {term}"),
1261                            JsonLdErrorCode::ProtectedTermRedefinition,
1262                        ));
1263                    }
1264                    // 27.2)
1265                    definition = previous_definition;
1266                }
1267            }
1268        }
1269        // 28)
1270        active_context
1271            .term_definitions
1272            .insert(term.into(), definition);
1273        defined.insert(term.into(), true);
1274    }
1275
1276    /// [IRI Expansion](https://www.w3.org/TR/json-ld-api/#iri-expansion)
1277    #[allow(clippy::too_many_arguments)]
1278    pub fn expand_iri<'a>(
1279        &self,
1280        active_context: &mut JsonLdContext,
1281        value: Cow<'a, str>,
1282        document_relative: bool,
1283        vocab: bool,
1284        local_context: Option<&HashMap<String, JsonNode>>,
1285        defined: &mut HashMap<String, bool>,
1286        errors: &mut Vec<JsonLdSyntaxError>,
1287    ) -> Option<Cow<'a, str>> {
1288        if has_keyword_form(&value) {
1289            // 1)
1290            return is_keyword(&value).then_some(value);
1291        }
1292        // 3)
1293        if let Some(local_context) = local_context {
1294            if local_context.contains_key(value.as_ref())
1295                && defined.get(value.as_ref()) != Some(&true)
1296            {
1297                self.create_term_definition(
1298                    active_context,
1299                    local_context,
1300                    &value,
1301                    defined,
1302                    None,
1303                    false,
1304                    false,
1305                    &mut Vec::new(),
1306                    errors,
1307                )
1308            }
1309        }
1310        if let Some(term_definition) = active_context.term_definitions.get(value.as_ref()) {
1311            if let Some(iri_mapping) = &term_definition.iri_mapping {
1312                let iri_mapping = iri_mapping.as_ref()?;
1313                // 4)
1314                if is_keyword(iri_mapping) {
1315                    return Some(iri_mapping.clone().into());
1316                }
1317                // 5)
1318                if vocab {
1319                    return Some(iri_mapping.clone().into());
1320                }
1321            }
1322        }
1323        // 6.1)
1324        if let Some((prefix, suffix)) = value.split_once(':') {
1325            // 6.2)
1326            if prefix == "_" || suffix.starts_with("//") {
1327                return Some(value);
1328            }
1329            // 6.3)
1330            if let Some(local_context) = local_context {
1331                if local_context.contains_key(prefix) && defined.get(prefix) != Some(&true) {
1332                    self.create_term_definition(
1333                        active_context,
1334                        local_context,
1335                        prefix,
1336                        defined,
1337                        None,
1338                        false,
1339                        false,
1340                        &mut Vec::new(),
1341                        errors,
1342                    )
1343                }
1344            }
1345            // 6.4)
1346            if let Some(term_definition) = active_context.term_definitions.get(prefix) {
1347                if let Some(Some(iri_mapping)) = &term_definition.iri_mapping {
1348                    if term_definition.prefix_flag {
1349                        return Some(format!("{iri_mapping}{suffix}").into());
1350                    }
1351                }
1352            }
1353            // 6.5)
1354            if Iri::parse(value.as_ref()).is_ok() {
1355                return Some(value);
1356            }
1357        }
1358        // 7)
1359        if vocab {
1360            if let Some(vocabulary_mapping) = &active_context.vocabulary_mapping {
1361                return Some(format!("{vocabulary_mapping}{value}").into());
1362            }
1363        }
1364        // 8)
1365        if document_relative {
1366            if let Some(base_iri) = &active_context.base_iri {
1367                if self.lenient {
1368                    return Some(base_iri.resolve_unchecked(&value).into_inner().into());
1369                } else if let Ok(value) = base_iri.resolve(&value) {
1370                    return Some(value.into_inner().into());
1371                }
1372            }
1373        }
1374
1375        Some(value)
1376    }
1377
1378    fn load_remote_context(
1379        &self,
1380        url: &str,
1381    ) -> Result<(Option<Iri<String>>, JsonNode), JsonLdSyntaxError> {
1382        let mut remote_context_cache = self
1383            .remote_context_cache
1384            .lock()
1385            .map_err(|_| JsonLdSyntaxError::msg("Poisoned mutex"))?;
1386        if let Some(loaded_context) = remote_context_cache.get(url) {
1387            // 5.2.4)
1388            return Ok(loaded_context.clone());
1389        }
1390
1391        // 5.2.5)
1392        let Some(load_document_callback) = &self.load_document_callback else {
1393            return Err(JsonLdSyntaxError::msg_and_code(
1394                "No LoadDocumentCallback has been set to load remote contexts",
1395                JsonLdErrorCode::LoadingRemoteContextFailed,
1396            ));
1397        };
1398        let context_document = match load_document_callback(
1399            url,
1400            &JsonLdLoadDocumentOptions {
1401                request_profile: JsonLdProfile::Context.into(),
1402            },
1403        ) {
1404            Ok(document) => document,
1405            Err(e) => {
1406                return Err(JsonLdSyntaxError::msg_and_code(
1407                    format!("Failed to load remote context {url}: {e}"),
1408                    JsonLdErrorCode::LoadingRemoteContextFailed,
1409                ));
1410            }
1411        };
1412        let parsed_document = match json_slice_to_node(&context_document.document) {
1413            Ok(d) => d,
1414            Err(e) => {
1415                return Err(JsonLdSyntaxError::msg_and_code(
1416                    format!("Failed to parse remote context {url}: {e}"),
1417                    JsonLdErrorCode::LoadingRemoteContextFailed,
1418                ));
1419            }
1420        };
1421        let JsonNode::Object(parsed_document) = parsed_document else {
1422            return Err(JsonLdSyntaxError::msg_and_code(
1423                format!("Remote context {url} must be a map"),
1424                JsonLdErrorCode::InvalidRemoteContext,
1425            ));
1426        };
1427        let Some(loaded_context) = parsed_document
1428            .into_iter()
1429            .find_map(|(k, v)| (k == "@context").then_some(v))
1430        else {
1431            return Err(JsonLdSyntaxError::msg_and_code(
1432                format!("Remote context {url} must be contain a @context key"),
1433                JsonLdErrorCode::InvalidRemoteContext,
1434            ));
1435        };
1436        let document_url = Iri::parse(context_document.document_url).ok();
1437        remote_context_cache.insert(url.into(), (document_url.clone(), loaded_context.clone()));
1438        Ok((document_url, loaded_context))
1439    }
1440}
1441
1442pub fn has_keyword_form(value: &str) -> bool {
1443    value
1444        .strip_prefix('@')
1445        .is_some_and(|suffix| !suffix.is_empty() && suffix.bytes().all(|b| b.is_ascii_alphabetic()))
1446}
1447
1448fn is_keyword(value: &str) -> bool {
1449    matches!(
1450        value,
1451        "@base"
1452            | "@container"
1453            | "@context"
1454            | "@direction"
1455            | "@graph"
1456            | "@id"
1457            | "@import"
1458            | "@included"
1459            | "@index"
1460            | "@json"
1461            | "@language"
1462            | "@list"
1463            | "@nest"
1464            | "@none"
1465            | "@prefix"
1466            | "@propagate"
1467            | "@protected"
1468            | "@reverse"
1469            | "@set"
1470            | "@type"
1471            | "@value"
1472            | "@version"
1473            | "@vocab"
1474    )
1475}
1476
1477fn json_slice_to_node(data: &[u8]) -> Result<JsonNode, JsonSyntaxError> {
1478    let mut parser = SliceJsonParser::new(data);
1479    json_node_from_events(std::iter::from_fn(|| match parser.parse_next() {
1480        Ok(JsonEvent::Eof) => None,
1481        Ok(event) => Some(Ok(event)),
1482        Err(e) => Some(Err(e)),
1483    }))
1484}
1485
1486enum BuildingObjectOrArrayNode {
1487    Object(HashMap<String, JsonNode>),
1488    ObjectWithPendingKey(HashMap<String, JsonNode>, String),
1489    Array(Vec<JsonNode>),
1490}
1491
1492pub fn json_node_from_events<'a>(
1493    events: impl IntoIterator<Item = Result<JsonEvent<'a>, JsonSyntaxError>>,
1494) -> Result<JsonNode, JsonSyntaxError> {
1495    let mut stack = Vec::new();
1496    for event in events {
1497        if let Some(result) = match event? {
1498            JsonEvent::String(value) => {
1499                after_to_node_event(&mut stack, JsonNode::String(value.into()))
1500            }
1501            JsonEvent::Number(value) => {
1502                after_to_node_event(&mut stack, JsonNode::Number(value.into()))
1503            }
1504            JsonEvent::Boolean(value) => after_to_node_event(&mut stack, JsonNode::Boolean(value)),
1505            JsonEvent::Null => after_to_node_event(&mut stack, JsonNode::Null),
1506            JsonEvent::EndArray | JsonEvent::EndObject => {
1507                let value = match stack.pop() {
1508                    Some(BuildingObjectOrArrayNode::Object(object)) => JsonNode::Object(object),
1509                    Some(BuildingObjectOrArrayNode::Array(array)) => JsonNode::Array(array),
1510                    _ => unreachable!(),
1511                };
1512                after_to_node_event(&mut stack, value)
1513            }
1514            JsonEvent::StartArray => {
1515                stack.push(BuildingObjectOrArrayNode::Array(Vec::new()));
1516                None
1517            }
1518            JsonEvent::StartObject => {
1519                stack.push(BuildingObjectOrArrayNode::Object(HashMap::new()));
1520                None
1521            }
1522            JsonEvent::ObjectKey(key) => {
1523                if let Some(BuildingObjectOrArrayNode::Object(object)) = stack.pop() {
1524                    stack.push(BuildingObjectOrArrayNode::ObjectWithPendingKey(
1525                        object,
1526                        key.into(),
1527                    ));
1528                }
1529                None
1530            }
1531            JsonEvent::Eof => unreachable!(),
1532        } {
1533            return Ok(result);
1534        }
1535    }
1536    unreachable!("The JSON emitted by the parser mut be valid")
1537}
1538
1539fn after_to_node_event(
1540    stack: &mut Vec<BuildingObjectOrArrayNode>,
1541    new_value: JsonNode,
1542) -> Option<JsonNode> {
1543    match stack.pop() {
1544        Some(BuildingObjectOrArrayNode::ObjectWithPendingKey(mut object, key)) => {
1545            object.insert(key, new_value);
1546            stack.push(BuildingObjectOrArrayNode::Object(object));
1547            None
1548        }
1549        Some(BuildingObjectOrArrayNode::Object(object)) => {
1550            stack.push(BuildingObjectOrArrayNode::Object(object));
1551            None
1552        }
1553        Some(BuildingObjectOrArrayNode::Array(mut array)) => {
1554            array.push(new_value);
1555            stack.push(BuildingObjectOrArrayNode::Array(array));
1556            None
1557        }
1558        None => Some(new_value),
1559    }
1560}