oxjsonld/
context.rs

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