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