Skip to main content

jsonld_core/context/
mod.rs

1//! Context processing algorithm and related types.
2mod definition;
3/// Inverse context, used to pick terms during compaction.
4pub mod inverse;
5
6use crate::{Direction, LenientLangTag, LenientLangTagBuf, ProcessingMode, Term, ValidId as Id};
7use contextual::WithContext;
8use iri_rs::IriBuf;
9use jsonld_syntax::{Keyword, KeywordType, Nullable};
10use once_cell::sync::OnceCell;
11use parking_lot::Mutex;
12use rdfx::{BlankIdBuf, vocabulary::Vocabulary};
13use std::{borrow::Borrow, hash::Hash, sync::Arc};
14
15pub use jsonld_syntax::context::{
16    definition::{Key, KeyOrType, Type},
17    term_definition::Nest,
18};
19
20pub use definition::*;
21pub use inverse::InverseContext;
22
23/// Either a borrowed context or an owned one held behind a box.
24///
25/// Expansion and compaction repeatedly rebind an active context that may or may
26/// not have been replaced by a processed one. A `Cow`-like type is the natural
27/// fit, but [`Context`] is over 250 bytes and storing it inline makes every
28/// async state machine holding one that much larger — the value is memmoved on
29/// every recursion. Boxing the owned arm keeps this two words wide however
30/// [`Context`] grows, at the cost of one allocation on a path that has just
31/// built a whole context anyway.
32pub enum ContextRef<'a, T = IriBuf, B = BlankIdBuf> {
33    /// Borrowed from the caller.
34    Borrowed(&'a Context<T, B>),
35
36    /// Owned by this value.
37    Owned(Box<Context<T, B>>),
38}
39
40impl<T, B> ContextRef<'_, T, B> {
41    /// Takes ownership of `context`, boxing it.
42    #[inline(always)]
43    pub fn owned(context: Context<T, B>) -> Self {
44        Self::Owned(Box::new(context))
45    }
46
47    /// Borrows the context, whichever arm holds it.
48    //
49    // Deliberately an inherent method rather than an `AsRef` impl, mirroring
50    // `Option::as_ref`: callers reach for it on a concrete `ContextRef`, and a
51    // trait impl would force type annotations at every call site.
52    #[allow(clippy::should_implement_trait)]
53    #[inline(always)]
54    #[must_use]
55    pub fn as_ref(&self) -> &Context<T, B> {
56        match self {
57            Self::Borrowed(context) => context,
58            Self::Owned(context) => context,
59        }
60    }
61
62    /// Returns `true` if the context is owned.
63    ///
64    /// Expansion uses this to detect that scoped-context processing replaced
65    /// the active context, and that cached key expansions must be recomputed.
66    #[inline(always)]
67    #[must_use]
68    pub fn is_owned(&self) -> bool {
69        matches!(self, Self::Owned(_))
70    }
71}
72
73impl<T, B> std::ops::Deref for ContextRef<'_, T, B> {
74    type Target = Context<T, B>;
75
76    #[inline(always)]
77    fn deref(&self) -> &Self::Target {
78        self.as_ref()
79    }
80}
81
82/// Error returned when a context vocabulary is set to an invalid value.
83#[derive(Debug, Clone, Copy, thiserror::Error)]
84pub enum InvalidVocab {
85    /// The vocabulary is a JSON-LD keyword.
86    #[error("vocabulary cannot be a keyword")]
87    Keyword,
88}
89
90/// Error returned when a context key cannot be expressed in syntax form.
91#[derive(Debug, Clone, Copy, thiserror::Error)]
92#[error("invalid context key")]
93pub struct InvalidContextKey;
94
95/// Error returned by [`Context::into_syntax_definition`] and related
96/// conversions.
97#[derive(Debug, Clone, Copy, thiserror::Error)]
98pub enum InvalidContextError {
99    #[error(transparent)]
100    /// The `@vocab` entry is invalid.
101    InvalidVocab(#[from] InvalidVocab),
102
103    #[error(transparent)]
104    /// A context key is invalid.
105    InvalidContextKey(#[from] InvalidContextKey),
106
107    #[error("IRI handle does not resolve in the given vocabulary")]
108    /// An IRI handle of the context does not resolve in the vocabulary passed
109    /// to [`Context::into_syntax_definition`]; the context was built with a
110    /// different vocabulary.
111    UnresolvedIri,
112}
113
114/// Cache key for [`Context::compact_iri_cache`]:
115/// `(var, vocab, reverse, mode)`.
116///
117/// The processing mode is part of the key because compact-IRI selection is
118/// mode-dependent (JSON-LD 1.1 considers `@index`/`@language` containers
119/// that 1.0 may not): one processed context compacted under both modes must
120/// not serve either mode the other's terms.
121pub type CompactIriKey<T, B> = (Term<T, B>, bool, bool, ProcessingMode);
122
123/// Borrowed view over a [`CompactIriKey`] for cache lookups that don't need
124/// to allocate a fresh `Term`. Hashes byte-for-byte identically to the
125/// owned tuple, so `HashMap::get` finds the same bucket.
126pub struct CompactIriKeyRef<'a, T, B>(pub &'a Term<T, B>, pub bool, pub bool, pub ProcessingMode);
127
128impl<T: std::hash::Hash, B: std::hash::Hash> std::hash::Hash for CompactIriKeyRef<'_, T, B> {
129    #[inline]
130    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
131        self.0.hash(state);
132        self.1.hash(state);
133        self.2.hash(state);
134        self.3.hash(state);
135    }
136}
137
138impl<T: PartialEq, B: PartialEq> hashbrown::Equivalent<CompactIriKey<T, B>> for CompactIriKeyRef<'_, T, B> {
139    #[inline]
140    fn equivalent(&self, key: &CompactIriKey<T, B>) -> bool {
141        self.0 == &key.0 && self.1 == key.1 && self.2 == key.2 && self.3 == key.3
142    }
143}
144
145/// Cached fixed-keyword aliases for an active context.
146///
147/// 13 keywords appear repeatedly during compaction (`@id`, `@type`, `@value`,
148/// `@list`, `@set`, `@graph`, `@index`, `@language`, `@direction`, `@reverse`,
149/// `@none`, `@included`, `@json`). Their compact-IRI form is a pure function
150/// of the active context and the processing mode, so the result is computed
151/// once per `(context, mode)` and reused — saving per-element [`Mutex`]
152/// traffic + cache lookups + alias-selection work in `compact_iri_full`.
153pub struct KeywordAliases {
154    aliases: [Box<str>; 13],
155}
156
157impl KeywordAliases {
158    /// Creates a new `KeywordAliases`.
159    #[must_use]
160    pub fn new(aliases: [Box<str>; 13]) -> Self {
161        Self { aliases }
162    }
163
164    /// Returns the cached alias for the given keyword. For one of the 13
165    /// supported keywords, returns the alias selected during cache
166    /// initialization; for any other keyword returns its literal name
167    /// (e.g. `"@base"`).
168    #[inline]
169    #[must_use]
170    pub fn get(&self, k: Keyword) -> &str {
171        match keyword_alias_index(k) {
172            Some(i) => self.aliases[i].as_ref(),
173            None => k.into_str(),
174        }
175    }
176}
177
178/// 13 keywords supported by [`KeywordAliases`], in cache-array order.
179pub const CACHED_KEYWORDS: [Keyword; 13] = [
180    Keyword::Id,
181    Keyword::Type,
182    Keyword::Value,
183    Keyword::List,
184    Keyword::Set,
185    Keyword::Graph,
186    Keyword::Index,
187    Keyword::Language,
188    Keyword::Direction,
189    Keyword::Reverse,
190    Keyword::None,
191    Keyword::Included,
192    Keyword::Json,
193];
194
195#[inline]
196fn keyword_aliases_slot(mode: ProcessingMode) -> usize {
197    match mode {
198        ProcessingMode::JsonLd1_0 => 0,
199        ProcessingMode::JsonLd1_1 => 1,
200    }
201}
202
203#[inline]
204fn keyword_alias_index(k: Keyword) -> Option<usize> {
205    Some(match k {
206        Keyword::Id => 0,
207        Keyword::Type => 1,
208        Keyword::Value => 2,
209        Keyword::List => 3,
210        Keyword::Set => 4,
211        Keyword::Graph => 5,
212        Keyword::Index => 6,
213        Keyword::Language => 7,
214        Keyword::Direction => 8,
215        Keyword::Reverse => 9,
216        Keyword::None => 10,
217        Keyword::Included => 11,
218        Keyword::Json => 12,
219        _ => return None,
220    })
221}
222
223type CompactIriCache<T, B> = Mutex<crate::HashMap<CompactIriKey<T, B>, Option<Arc<str>>>>;
224type TermResolutionCache<T, B> = Mutex<crate::HashMap<Box<str>, Arc<Term<T, B>>>>;
225
226/// A prefix-eligible term paired with the term it maps to.
227pub type PrefixTerm<T, B> = (Key, Arc<Term<T, B>>);
228
229/// Active context: everything the algorithms need to expand or compact
230/// against the current scope.
231pub struct Context<T = IriBuf, B = BlankIdBuf> {
232    original_base_url: Option<T>,
233    base_iri: Option<T>,
234    vocabulary: Option<Term<T, B>>,
235    default_language: Option<LenientLangTagBuf>,
236    default_base_direction: Option<Direction>,
237    previous_context: Option<Arc<Self>>,
238    definitions: Arc<Definitions<T, B>>,
239    /// Mode this context was processed under.
240    ///
241    /// A handful of algorithm steps behave differently in JSON-LD 1.0 — most
242    /// notably that any term definition may serve as a compact-IRI prefix
243    /// during IRI expansion, the `@prefix` flag being a 1.1 addition. Carrying
244    /// the mode here keeps those checks out of every IRI-expansion signature.
245    processing_mode: ProcessingMode,
246    // Caches are wrapped in `Arc` so that `Clone` shares them across
247    // copies of a processed context. This is critical for the
248    // `ProcessingCache` hit path, which clones a stored `Arc<Context>`
249    // for each entity expansion — without sharing, every entity would
250    // start with empty caches and the per-context memoization would
251    // never see cross-entity hits.
252    //
253    // Invalidation (via [`Self::invalidate_iri_caches`]) replaces the
254    // `Arc` with a fresh one, so other holders of the original `Arc`
255    // keep their (still-valid) cached entries.
256    inverse: Arc<OnceCell<InverseContext<T, B>>>,
257    prefix_terms: Arc<OnceCell<Vec<PrefixTerm<T, B>>>>,
258    compact_iri_cache: Arc<OnceCell<CompactIriCache<T, B>>>,
259    term_resolution_cache: Arc<OnceCell<TermResolutionCache<T, B>>>,
260    // One slot per processing mode: alias selection is mode-dependent.
261    keyword_aliases: Arc<[OnceCell<KeywordAliases>; 2]>,
262}
263
264impl<T, B> Default for Context<T, B> {
265    fn default() -> Self {
266        Self {
267            original_base_url: None,
268            base_iri: None,
269            vocabulary: None,
270            default_language: None,
271            default_base_direction: None,
272            previous_context: None,
273            definitions: Arc::new(Definitions::default()),
274            processing_mode: ProcessingMode::default(),
275            inverse: Arc::new(OnceCell::new()),
276            prefix_terms: Arc::new(OnceCell::new()),
277            compact_iri_cache: Arc::new(OnceCell::new()),
278            term_resolution_cache: Arc::new(OnceCell::new()),
279            keyword_aliases: Arc::new([OnceCell::new(), OnceCell::new()]),
280        }
281    }
282}
283
284/// Binding of a context definition, key and term definition together.
285pub type DefinitionEntryRef<'a, T = IriBuf, B = BlankIdBuf> = (&'a Key, &'a TermDefinition<T, B>);
286
287impl<T, B> Context<T, B> {
288    /// Create a new context with the given base IRI.
289    pub fn new(base_iri: Option<T>) -> Self
290    where
291        T: Clone,
292    {
293        Self {
294            original_base_url: base_iri.clone(),
295            base_iri,
296            vocabulary: None,
297            default_language: None,
298            default_base_direction: None,
299            previous_context: None,
300            definitions: Arc::new(Definitions::default()),
301            processing_mode: ProcessingMode::default(),
302            inverse: Arc::new(OnceCell::new()),
303            prefix_terms: Arc::new(OnceCell::new()),
304            compact_iri_cache: Arc::new(OnceCell::new()),
305            term_resolution_cache: Arc::new(OnceCell::new()),
306            keyword_aliases: Arc::new([OnceCell::new(), OnceCell::new()]),
307        }
308    }
309
310    /// Returns a copy of this context whose IRI memoisation caches are private
311    /// to the copy.
312    ///
313    /// [`Self::term_resolution_cache`][c] maps a term to an already-resolved
314    /// [`Term`], which carries `T`-typed identifiers. Sharing it across
315    /// vocabulary forks is unsound: a term that one fork resolved by interning
316    /// a *new* IRI would be served to another fork, where that identifier
317    /// denotes something else entirely — or nothing. Concurrent expansion
318    /// therefore gives each task a context of its own.
319    ///
320    /// Only the caches are fresh. Definitions, prefix terms and keyword
321    /// aliases are shared through their `Arc`s: they are derived from the
322    /// context's own definitions, whose identifiers every fork inherits, so
323    /// they stay valid. The same reasoning covers the inverse context.
324    ///
325    /// [c]: Self::term_resolution_cache
326    #[must_use]
327    pub fn with_private_caches(&self) -> Context<T, B>
328    where
329        T: Clone,
330        B: Clone,
331    {
332        Context {
333            original_base_url: self.original_base_url.clone(),
334            base_iri: self.base_iri.clone(),
335            vocabulary: self.vocabulary.clone(),
336            default_language: self.default_language.clone(),
337            default_base_direction: self.default_base_direction,
338            // A previous context is consulted on the same code paths, so its
339            // caches have to be private too.
340            previous_context: self.previous_context.as_ref().map(|previous| Arc::new(previous.with_private_caches())),
341            definitions: Arc::clone(&self.definitions),
342            processing_mode: self.processing_mode,
343            inverse: Arc::clone(&self.inverse),
344            prefix_terms: Arc::clone(&self.prefix_terms),
345            compact_iri_cache: Arc::new(OnceCell::new()),
346            term_resolution_cache: Arc::new(OnceCell::new()),
347            keyword_aliases: Arc::clone(&self.keyword_aliases),
348        }
349    }
350
351    /// Returns the processing mode this context was processed under.
352    #[inline(always)]
353    pub fn processing_mode(&self) -> ProcessingMode {
354        self.processing_mode
355    }
356
357    /// Sets the processing mode of this context.
358    #[inline(always)]
359    pub fn set_processing_mode(&mut self, mode: ProcessingMode) {
360        self.processing_mode = mode;
361    }
362
363    /// Returns a reference to the given `term` definition, if any.
364    pub fn get<Q>(&self, term: &Q) -> Option<TermDefinitionRef<'_, T, B>>
365    where
366        Key: Borrow<Q>,
367        KeywordType: Borrow<Q>,
368        Q: ?Sized + Hash + Eq,
369    {
370        self.definitions.get(term)
371    }
372
373    /// Returns a reference to the given `term` normal definition, if any.
374    pub fn get_normal<Q>(&self, term: &Q) -> Option<&NormalTermDefinition<T, B>>
375    where
376        Key: Borrow<Q>,
377        Q: ?Sized + Hash + Eq,
378    {
379        self.definitions.get_normal(term)
380    }
381
382    /// Returns a reference to the `@type` definition, if any.
383    pub fn get_type(&self) -> Option<&TypeTermDefinition> {
384        self.definitions.get_type()
385    }
386
387    /// Checks if the given `term` is defined.
388    pub fn contains_term<Q>(&self, term: &Q) -> bool
389    where
390        Key: Borrow<Q>,
391        KeywordType: Borrow<Q>,
392        Q: ?Sized + Hash + Eq,
393    {
394        self.definitions.contains_term(term)
395    }
396
397    /// Returns the original base URL of the context.
398    pub fn original_base_url(&self) -> Option<&T> {
399        self.original_base_url.as_ref()
400    }
401
402    /// Returns the base IRI of the context.
403    pub fn base_iri(&self) -> Option<&T> {
404        self.base_iri.as_ref()
405    }
406
407    /// Returns the `@vocab` value, if any.
408    pub fn vocabulary(&self) -> Option<&Term<T, B>> {
409        match &self.vocabulary {
410            Some(v) => Some(v),
411            None => None,
412        }
413    }
414
415    /// Returns the default `@language` value.
416    pub fn default_language(&self) -> Option<&LenientLangTag> {
417        self.default_language.as_ref().map(jsonld_syntax::LenientLangTagBuf::as_lenient_lang_tag_ref)
418    }
419
420    /// Returns the default `@direction` value.
421    pub fn default_base_direction(&self) -> Option<Direction> {
422        self.default_base_direction
423    }
424
425    /// Returns a reference to the previous context.
426    pub fn previous_context(&self) -> Option<&Self> {
427        match &self.previous_context {
428            Some(c) => Some(c),
429            None => None,
430        }
431    }
432
433    /// Returns the address of the `Arc` backing the term definitions.
434    ///
435    /// Two contexts that share this pointer share the exact same definitions
436    /// (copy-on-write via [`Arc::make_mut`]). Useful as a cheap fingerprint
437    /// for memoization keys.
438    pub fn definitions_arc_ptr(&self) -> *const Definitions<T, B> {
439        Arc::as_ptr(&self.definitions)
440    }
441
442    /// Returns the address of the `Arc` backing the previous context, or null
443    /// when there is none. See [`Self::definitions_arc_ptr`].
444    pub fn previous_context_arc_ptr(&self) -> *const Self {
445        self.previous_context.as_ref().map_or(std::ptr::null(), Arc::as_ptr)
446    }
447
448    /// Returns the number of terms defined.
449    pub fn len(&self) -> usize {
450        self.definitions.len()
451    }
452
453    /// Checks if no terms are defined.
454    pub fn is_empty(&self) -> bool {
455        self.definitions.is_empty()
456    }
457
458    /// Returns a handle to the term definitions.
459    pub fn definitions(&self) -> &Definitions<T, B> {
460        &self.definitions
461    }
462
463    /// Checks if the context has a protected definition.
464    pub fn has_protected_items(&self) -> bool {
465        for binding in self.definitions() {
466            if binding.definition().protected() {
467                return true;
468            }
469        }
470
471        false
472    }
473
474    /// Returns the inverse context, computing and caching it on first access.
475    pub fn inverse(&self) -> &InverseContext<T, B>
476    where
477        T: Clone + Hash + Eq,
478        B: Clone + Hash + Eq,
479    {
480        self.inverse.get_or_init(|| self.into())
481    }
482
483    /// Returns the term definitions whose `prefix` flag is `true`, each paired
484    /// with its IRI mapping.
485    ///
486    /// The mapping is carried alongside the key so that compaction's
487    /// candidate loop does not have to look the definition back up — which
488    /// would re-hash the very key this list just handed it. Definitions with no
489    /// IRI mapping cannot form a compact IRI and are left out.
490    ///
491    /// Lazily computed and cached; invalidated whenever a definition is added,
492    /// removed, or replaced.
493    pub fn prefix_terms(&self) -> &[PrefixTerm<T, B>] {
494        self.prefix_terms.get_or_init(|| {
495            self.definitions
496                .iter()
497                .filter_map(|binding| match binding {
498                    BindingRef::Normal(key, def) if def.prefix => def.value_arc().map(|value| (*key, value.clone())),
499                    _ => None,
500                })
501                .collect()
502        })
503    }
504
505    /// Returns the per-context memoization map for the compact-IRI algorithm.
506    ///
507    /// The cache stores results keyed on `(var, vocab, reverse, mode)`. It is
508    /// invalidated whenever the context's term definitions, base IRI,
509    /// vocabulary, language, or direction change, since those affect
510    /// compaction output.
511    pub fn compact_iri_cache(&self) -> &CompactIriCache<T, B> {
512        self.compact_iri_cache.get_or_init(|| Mutex::new(crate::HashMap::default()))
513    }
514
515    /// Returns the per-context memoization map for IRI expansion of term keys.
516    ///
517    /// Maps the input string to the resolved [`Arc<Term>`]. Populated only by
518    /// the dominant key-expansion call shape (`vocab=Some(Keep)`,
519    /// `document_relative=false`); other shapes bypass this cache. Invalidated
520    /// alongside the compact-IRI / inverse caches whenever the context's term
521    /// definitions, base IRI, vocabulary, language, or direction change.
522    pub fn term_resolution_cache(&self) -> &TermResolutionCache<T, B> {
523        self.term_resolution_cache.get_or_init(|| Mutex::new(crate::HashMap::default()))
524    }
525
526    /// Returns the cached keyword aliases of this context for the given
527    /// processing mode, computing them via `init` on first access. The
528    /// closure runs once per `(context, mode)`; subsequent calls return the
529    /// cached value. Aliases are cached per mode because compact-IRI
530    /// selection is mode-dependent.
531    pub fn keyword_aliases_or_init<F: FnOnce() -> KeywordAliases>(&self, mode: ProcessingMode, init: F) -> &KeywordAliases {
532        self.keyword_aliases[keyword_aliases_slot(mode)].get_or_init(init)
533    }
534
535    /// Drops the inverse-context and compact-IRI caches if they are populated.
536    ///
537    /// Replaces each cache `Arc` with a fresh empty one.
538    ///
539    /// We can't `take()` from a shared `Arc<OnceCell<_>>` (it requires `&mut`
540    /// access to the `OnceCell`, which the Arc doesn't grant). Replacing the
541    /// `Arc` itself diverges this context's caches from any sharers — which
542    /// is exactly what we want: other holders had a context with the
543    /// pre-mutation state and their caches are still valid for that state.
544    #[inline]
545    fn invalidate_iri_caches(&mut self) {
546        if self.inverse.get().is_some() {
547            self.inverse = Arc::new(OnceCell::new());
548        }
549        if self.compact_iri_cache.get().is_some() {
550            self.compact_iri_cache = Arc::new(OnceCell::new());
551        }
552        if self.term_resolution_cache.get().is_some() {
553            self.term_resolution_cache = Arc::new(OnceCell::new());
554        }
555        if self.keyword_aliases.iter().any(|slot| slot.get().is_some()) {
556            self.keyword_aliases = Arc::new([OnceCell::new(), OnceCell::new()]);
557        }
558    }
559
560    /// Sets the normal definition for the given term `key`.
561    pub fn set_normal(&mut self, key: Key, definition: Option<NormalTermDefinition<T, B>>) -> Option<NormalTermDefinition<T, B>>
562    where
563        T: Clone,
564        B: Clone,
565    {
566        self.invalidate_iri_caches();
567        if self.prefix_terms.get().is_some() {
568            self.prefix_terms = Arc::new(OnceCell::new());
569        }
570        Arc::make_mut(&mut self.definitions).set_normal(key, definition)
571    }
572
573    /// Sets the `@type` definition.
574    pub fn set_type(&mut self, type_: Option<TypeTermDefinition>) -> Option<TypeTermDefinition>
575    where
576        T: Clone,
577        B: Clone,
578    {
579        if self.compact_iri_cache.get().is_some() {
580            self.compact_iri_cache = Arc::new(OnceCell::new());
581        }
582        if self.term_resolution_cache.get().is_some() {
583            self.term_resolution_cache = Arc::new(OnceCell::new());
584        }
585        if self.keyword_aliases.iter().any(|slot| slot.get().is_some()) {
586            self.keyword_aliases = Arc::new([OnceCell::new(), OnceCell::new()]);
587        }
588        Arc::make_mut(&mut self.definitions).set_type(type_)
589    }
590
591    /// Sets the base IRI.
592    pub fn set_base_iri(&mut self, iri: Option<T>) {
593        self.invalidate_iri_caches();
594        self.base_iri = iri;
595    }
596
597    /// Sets the `@vocab` value.
598    pub fn set_vocabulary(&mut self, vocab: Option<Term<T, B>>) {
599        self.invalidate_iri_caches();
600        self.vocabulary = vocab;
601    }
602
603    /// Sets the default `@language` value.
604    pub fn set_default_language(&mut self, lang: Option<LenientLangTagBuf>) {
605        self.invalidate_iri_caches();
606        self.default_language = lang;
607    }
608
609    /// Sets the default `@direction` value.
610    pub fn set_default_base_direction(&mut self, dir: Option<Direction>) {
611        self.invalidate_iri_caches();
612        self.default_base_direction = dir;
613    }
614
615    /// Sets the previous context.
616    pub fn set_previous_context(&mut self, previous: Self) {
617        self.invalidate_iri_caches();
618        self.previous_context = Some(Arc::new(previous));
619    }
620
621    /// Converts this context into its syntactic definition.
622    ///
623    /// # Errors
624    ///
625    /// Returns an error when a term in the context cannot be represented in syntax form, which happens if the vocabulary has no IRI for an identifier it holds.
626    pub fn into_syntax_definition(self, vocabulary: &impl Vocabulary<Iri = T, BlankId = B>) -> Result<jsonld_syntax::context::Definition, InvalidContextError>
627    where
628        T: Clone,
629        B: Clone,
630    {
631        let (bindings, type_) = Arc::unwrap_or_clone(self.definitions).into_parts();
632
633        let vocab = match self.vocabulary {
634            Some(Term::Null) => Some(Nullable::Null),
635            Some(Term::Id(r)) => Some(Nullable::Some(r.with(vocabulary).to_string().into())),
636            Some(Term::Keyword(_)) => return Err(InvalidContextError::InvalidVocab(InvalidVocab::Keyword)),
637            None => None,
638        };
639
640        let base = match self.base_iri {
641            Some(i) => {
642                // Fails only when `vocabulary` is not the vocabulary this
643                // context was built with.
644                let iri: iri_rs::IriBuf = vocabulary.iri(&i).ok_or(InvalidContextError::UnresolvedIri)?.into();
645                Some(Nullable::Some(iri.into()))
646            }
647            None => None,
648        };
649
650        Ok(jsonld_syntax::context::Definition {
651            base,
652            import: None,
653            language: self.default_language.map(Nullable::Some),
654            direction: self.default_base_direction.map(Nullable::Some),
655            propagate: None,
656            protected: None,
657            type_: type_.map(TypeTermDefinition::into_syntax_definition),
658            version: None,
659            vocab,
660            bindings: {
661                // `HashMap` iteration order is nondeterministic; sort by term
662                // so the serialized definition is stable across runs.
663                let mut entries: Vec<_> = bindings.into_iter().collect();
664                entries.sort_unstable_by(|(a, _), (b, _)| a.as_str().cmp(b.as_str()));
665                entries
666                    .into_iter()
667                    .map(|(key, definition)| definition.into_syntax_definition(vocabulary).map(|d| (key, d)))
668                    .collect::<Result<_, _>>()?
669            },
670        })
671    }
672
673    /// Rewrites every IRI and identifier of this context with the given
674    /// functions.
675    pub fn map_ids<U, C>(self, mut map_iri: impl FnMut(T) -> U, mut map_id: impl FnMut(Id<T, B>) -> Id<U, C>) -> Context<U, C>
676    where
677        T: Clone,
678        B: Clone,
679    {
680        self.map_ids_with(&mut map_iri, &mut map_id)
681    }
682
683    fn map_ids_with<U, C>(self, map_iri: &mut impl FnMut(T) -> U, map_id: &mut impl FnMut(Id<T, B>) -> Id<U, C>) -> Context<U, C>
684    where
685        T: Clone,
686        B: Clone,
687    {
688        Context {
689            original_base_url: self.original_base_url.map(&mut *map_iri),
690            base_iri: self.base_iri.map(&mut *map_iri),
691            vocabulary: self.vocabulary.map(|v| v.map_id(&mut *map_id)),
692            default_language: self.default_language,
693            default_base_direction: self.default_base_direction,
694            previous_context: self.previous_context.map(|c| Arc::new(Arc::unwrap_or_clone(c).map_ids_with(map_iri, map_id))),
695            definitions: Arc::new(Arc::unwrap_or_clone(self.definitions).map_ids(map_iri, map_id)),
696            processing_mode: self.processing_mode,
697            inverse: Arc::new(OnceCell::new()),
698            prefix_terms: Arc::new(OnceCell::new()),
699            compact_iri_cache: Arc::new(OnceCell::new()),
700            term_resolution_cache: Arc::new(OnceCell::new()),
701            keyword_aliases: Arc::new([OnceCell::new(), OnceCell::new()]),
702        }
703    }
704}
705
706/// Conversion of a processed context back into its syntactic form.
707pub trait IntoSyntax<T = IriBuf, B = BlankIdBuf> {
708    /// Consumes this value, returning the equivalent syntactic context. The
709    /// given vocabulary must be the one the context was built with, so its
710    /// IRI handles can be resolved back to actual IRIs.
711    ///
712    /// # Errors
713    ///
714    /// Returns an error when a term in the context cannot be represented in syntax form.
715    fn into_syntax(self, vocabulary: &impl Vocabulary<Iri = T, BlankId = B>) -> Result<jsonld_syntax::context::Context, InvalidContextError>;
716}
717
718impl<T, B> IntoSyntax<T, B> for jsonld_syntax::context::Context {
719    fn into_syntax(self, _namespace: &impl Vocabulary<Iri = T, BlankId = B>) -> Result<jsonld_syntax::context::Context, InvalidContextError> {
720        Ok(self)
721    }
722}
723
724impl<T: Clone, B: Clone> IntoSyntax<T, B> for Context<T, B> {
725    fn into_syntax(self, vocabulary: &impl Vocabulary<Iri = T, BlankId = B>) -> Result<jsonld_syntax::context::Context, InvalidContextError> {
726        Ok(jsonld_syntax::context::Context::One(jsonld_syntax::ContextEntry::Definition(
727            self.into_syntax_definition(vocabulary)?,
728        )))
729    }
730}
731
732impl<T: Clone, B: Clone> Clone for Context<T, B> {
733    fn clone(&self) -> Self {
734        // Share caches across clones via `Arc::clone`. Sound because processed
735        // contexts are not mutated post-processing — the only mutating APIs
736        // (`set_normal`, `set_type`, `set_base_iri`, etc.) call
737        // `invalidate_iri_caches`, which replaces the `Arc` with a fresh one
738        // so the mutated copy diverges from any sharers.
739        Self {
740            original_base_url: self.original_base_url.clone(),
741            base_iri: self.base_iri.clone(),
742            vocabulary: self.vocabulary.clone(),
743            default_language: self.default_language.clone(),
744            default_base_direction: self.default_base_direction,
745            previous_context: self.previous_context.clone(),
746            definitions: Arc::clone(&self.definitions),
747            processing_mode: self.processing_mode,
748            inverse: Arc::clone(&self.inverse),
749            prefix_terms: Arc::clone(&self.prefix_terms),
750            compact_iri_cache: Arc::clone(&self.compact_iri_cache),
751            term_resolution_cache: Arc::clone(&self.term_resolution_cache),
752            keyword_aliases: Arc::clone(&self.keyword_aliases),
753        }
754    }
755}
756
757impl<T: PartialEq, B: PartialEq> PartialEq for Context<T, B> {
758    fn eq(&self, other: &Self) -> bool {
759        self.original_base_url == other.original_base_url
760            && self.base_iri == other.base_iri
761            && self.vocabulary == other.vocabulary
762            && self.default_language == other.default_language
763            && self.default_base_direction == other.default_base_direction
764            && self.processing_mode == other.processing_mode
765            // `ptr_eq` first: clones share their `Arc<Definitions>`, making
766            // the common case free.
767            && (Arc::ptr_eq(&self.definitions, &other.definitions) || self.definitions == other.definitions)
768            && self.previous_context == other.previous_context
769    }
770}