Skip to main content

jsonld_compaction/
lib.rs

1//! Implementation of the [JSON-LD compaction algorithms][1].
2//!
3//! Compaction turns an expanded JSON-LD document back into the terse,
4//! human-friendly shape described by a `@context`: IRIs become terms or
5//! compact IRIs, single-element arrays collapse, and value objects reduce to
6//! plain JSON scalars wherever the context makes that lossless.
7//!
8//! [`Compact`] compacts a whole document (an
9//! [`ExpandedDocument`][jsonld_core::ExpandedDocument] or a
10//! [`FlattenedDocument`][jsonld_core::FlattenedDocument]) and embeds the
11//! `@context` used into the output. [`CompactFragment`] compacts a single
12//! object, node, list or collection against an already-processed active
13//! context, and is what the document-level algorithm recurses through.
14//!
15//! This crate is the compaction half of the `jsonld` family; the `jsonld`
16//! crate re-exports it behind a processor API that also handles expansion,
17//! flattening and RDF serialization.
18//!
19//! [1]: https://www.w3.org/TR/json-ld-api/#compaction-algorithms
20use jsonld_context_processing::{Options as ProcessingOptions, Process};
21use jsonld_core::{
22    Context,
23    ContextRef,
24    IndexSet,
25    Indexed,
26    Loader,
27    ProcessingMode,
28    Term,
29    Value,
30    context::inverse::{LangSelection, TypeSelection},
31    object::Any,
32};
33use jsonld_syntax::{ContainerKind, ErrorCode, Keyword, native_stack::Recursion};
34use rdfx::vocabulary::{self, VocabularyMut};
35use std::hash::Hash;
36
37mod document;
38mod iri;
39mod node;
40mod property;
41mod value;
42mod value_hint;
43
44#[cfg(test)]
45mod test_fixtures;
46
47pub use document::*;
48pub(crate) use iri::compact_iri_with_memo;
49pub use iri::{IriConfusedWithPrefix, compact_iri, compact_iri_with, compact_iri_with_hint, keyword_alias};
50use node::compact_indexed_node_with;
51pub use node::{compact_node_reference, types_as_array};
52use property::compact_property;
53pub use property::values_as_array;
54use value::compact_indexed_value_with;
55pub use value::compact_value;
56pub use value_hint::{ValueHint, ValueKind};
57
58#[derive(Debug, thiserror::Error)]
59/// Error raised while compacting a document.
60pub enum Error<E = std::convert::Infallible> {
61    #[error("IRI confused with prefix")]
62    /// An IRI cannot be written out because it would be read back as a
63    /// compact IRI.
64    ///
65    /// See [`IriConfusedWithPrefix`] for the exact condition.
66    IriConfusedWithPrefix,
67
68    #[error("Invalid `@nest` value")]
69    /// A term definition's `@nest` value is neither `@nest` itself nor a term
70    /// of the active context that expands to `@nest`.
71    InvalidNestValue,
72
73    #[error("Colliding compacted entry")]
74    /// The compaction target of a map-based container or `@nest` entry
75    /// already holds a non-map value: two terms compacted to the same key
76    /// with incompatible shapes. The JSON-LD specification does not define
77    /// an output for this state.
78    CollidingEntry,
79
80    #[error("Context processing failed: {0}")]
81    /// A `@context` encountered during compaction could not be processed.
82    ///
83    /// Compaction processes scoped contexts (`@context` entries inside term
84    /// definitions) as it descends, so any context-processing error can
85    /// surface here.
86    ContextProcessing(jsonld_context_processing::Error<E>),
87}
88
89impl<E> Error<E> {
90    /// Returns the JSON-LD error code this error is reported under.
91    pub fn code(&self) -> ErrorCode {
92        match self {
93            Self::IriConfusedWithPrefix => ErrorCode::IriConfusedWithPrefix,
94            Self::InvalidNestValue => ErrorCode::InvalidNestValue,
95            Self::CollidingEntry => ErrorCode::ConflictingIndexes,
96            Self::ContextProcessing(e) => e.code(),
97        }
98    }
99}
100
101impl<E> From<jsonld_context_processing::Error<E>> for Error<E> {
102    fn from(e: jsonld_context_processing::Error<E>) -> Self {
103        Self::ContextProcessing(e)
104    }
105}
106
107impl<E> From<IriConfusedWithPrefix> for Error<E> {
108    fn from(_: IriConfusedWithPrefix) -> Self {
109        Self::IriConfusedWithPrefix
110    }
111}
112
113/// Result of compacting a document fragment.
114pub type CompactFragmentResult<E> = Result<jstrict::Value, Error<E>>;
115
116/// Compaction options.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub struct Options {
119    /// JSON-LD processing mode.
120    pub processing_mode: ProcessingMode,
121
122    /// Whether IRIs may be compacted into references relative to the
123    /// document's location.
124    ///
125    /// The compaction algorithm itself does not read this flag: it relativizes
126    /// IRIs whenever the active context has a base IRI. The flag is what
127    /// decides whether that base IRI gets set in the first place — the
128    /// `jsonld` processor seeds the active context with the input document's
129    /// URL only when this is `true` and no explicit base IRI was given.
130    pub compact_to_relative: bool,
131
132    /// Whether a single-element array is replaced by that element.
133    ///
134    /// When `false`, arrays stay arrays even with one element. Terms whose
135    /// container mapping includes `@set`, and the `@graph` and `@list` keys,
136    /// keep their arrays regardless.
137    pub compact_arrays: bool,
138
139    /// Whether the entries of each node object are processed in lexicographic
140    /// order of their expanded property IRI.
141    ///
142    /// Only affects the order of keys in the output, not which keys appear.
143    pub ordered: bool,
144}
145
146impl From<Options> for jsonld_context_processing::Options {
147    fn from(options: Options) -> jsonld_context_processing::Options {
148        jsonld_context_processing::Options {
149            processing_mode: options.processing_mode,
150            ..Default::default()
151        }
152    }
153}
154
155impl From<jsonld_expansion::Options> for Options {
156    fn from(options: jsonld_expansion::Options) -> Options {
157        Options {
158            processing_mode: options.processing_mode,
159            ordered: options.ordered,
160            ..Options::default()
161        }
162    }
163}
164
165impl Default for Options {
166    fn default() -> Options {
167        Options {
168            processing_mode: ProcessingMode::default(),
169            compact_to_relative: true,
170            compact_arrays: true,
171            ordered: false,
172        }
173    }
174}
175
176/// Document fragments that can be compacted against an active context.
177pub trait CompactFragment<I, B> {
178    /// Compacts this fragment, taking every parameter of the algorithm
179    /// explicitly.
180    ///
181    /// `active_context` is the context in force for this fragment.
182    /// `type_scoped_context` is the context as it stood *before* this
183    /// fragment's own type-scoped contexts were applied; term definitions for
184    /// `active_property` and for `@type` values are looked up there, as the
185    /// specification requires. `active_property` is the term this fragment was
186    /// reached through, and drives container, language and index handling.
187    /// `loader` resolves any remote context referenced by a scoped `@context`.
188    async fn compact_fragment_full<'a, N, L>(
189        &'a self,
190        vocabulary: &'a mut N,
191        active_context: &'a Context<I, B>,
192        type_scoped_context: &'a Context<I, B>,
193        active_property: Option<&'a str>,
194        loader: &'a L,
195        options: Options,
196    ) -> CompactFragmentResult<L::Error>
197    where
198        N: VocabularyMut<Iri = I, BlankId = B>,
199        I: Clone + Hash + Eq,
200        B: Clone + Hash + Eq,
201        L: Loader;
202
203    #[inline(always)]
204    /// Compacts this fragment at the top level of a document, with the default
205    /// [`Options`], using `vocabulary` to resolve IRI and blank node
206    /// identifiers.
207    async fn compact_fragment_with<'a, N, L>(
208        &'a self,
209        vocabulary: &'a mut N,
210        active_context: &'a Context<I, B>,
211        loader: &'a mut L,
212    ) -> CompactFragmentResult<L::Error>
213    where
214        N: VocabularyMut<Iri = I, BlankId = B>,
215        I: Clone + Hash + Eq,
216        B: Clone + Hash + Eq,
217        L: Loader,
218    {
219        self.compact_fragment_full(vocabulary, active_context, active_context, None, loader, Options::default())
220            .await
221    }
222
223    #[inline(always)]
224    /// Compacts this fragment at the top level of a document, with the default
225    /// [`Options`], for documents that store IRIs and blank node identifiers
226    /// inline instead of indexing them through a vocabulary.
227    async fn compact_fragment<'a, L>(&'a self, active_context: &'a Context<I, B>, loader: &'a mut L) -> CompactFragmentResult<L::Error>
228    where
229        (): VocabularyMut<Iri = I, BlankId = B>,
230        I: Clone + Hash + Eq,
231        B: Clone + Hash + Eq,
232        L: Loader,
233    {
234        self.compact_fragment_full(
235            vocabulary::no_vocabulary_mut(),
236            active_context,
237            active_context,
238            None,
239            loader,
240            Options::default(),
241        )
242        .await
243    }
244}
245
246#[derive(PartialEq)]
247enum TypeLangValue<'a, I> {
248    Type(TypeSelection<I>),
249    Lang(LangSelection<'a>),
250}
251
252/// Document fragments that carry an `@index` separately from their own
253/// contents.
254///
255/// [`Indexed<T>`] pairs a fragment with the index it was reached through;
256/// implementing this trait for `T` is what gives `Indexed<T>` its
257/// [`CompactFragment`] implementation. The index has to be passed down rather
258/// than read off the fragment because whether it survives into the output
259/// depends on the active property's container mapping.
260pub trait CompactIndexedFragment<I, B> {
261    /// Compacts this fragment together with the `@index` value it was reached
262    /// through.
263    ///
264    /// The index is emitted as an `@index` entry unless the active property's
265    /// container mapping includes `@index`, in which case it has already become
266    /// the enclosing map's key and is dropped.
267    async fn compact_indexed_fragment<'a, N, L>(
268        &'a self,
269        vocabulary: &'a mut N,
270        index: Option<&'a str>,
271        active_context: &'a Context<I, B>,
272        type_scoped_context: &'a Context<I, B>,
273        active_property: Option<&'a str>,
274        loader: &'a L,
275        options: Options,
276    ) -> CompactFragmentResult<L::Error>
277    where
278        N: VocabularyMut<Iri = I, BlankId = B>,
279        I: Clone + Hash + Eq,
280        B: Clone + Hash + Eq,
281        L: Loader;
282}
283
284impl<I, B, T: CompactIndexedFragment<I, B>> CompactFragment<I, B> for Indexed<T> {
285    async fn compact_fragment_full<'a, N, L>(
286        &'a self,
287        vocabulary: &'a mut N,
288        active_context: &'a Context<I, B>,
289        type_scoped_context: &'a Context<I, B>,
290        active_property: Option<&'a str>,
291        loader: &'a L,
292        options: Options,
293    ) -> CompactFragmentResult<L::Error>
294    where
295        N: VocabularyMut<Iri = I, BlankId = B>,
296        I: Clone + Hash + Eq,
297        B: Clone + Hash + Eq,
298        L: Loader,
299    {
300        self.inner()
301            .compact_indexed_fragment(vocabulary, self.index(), active_context, type_scoped_context, active_property, loader, options)
302            .await
303    }
304}
305
306impl<I, B, T: Any<I, B>> CompactIndexedFragment<I, B> for T {
307    async fn compact_indexed_fragment<'a, N, L>(
308        &'a self,
309        vocabulary: &'a mut N,
310        index: Option<&'a str>,
311        active_context: &'a Context<I, B>,
312        type_scoped_context: &'a Context<I, B>,
313        active_property: Option<&'a str>,
314        loader: &'a L,
315        options: Options,
316    ) -> CompactFragmentResult<L::Error>
317    where
318        N: VocabularyMut<Iri = I, BlankId = B>,
319        I: Clone + Hash + Eq,
320        B: Clone + Hash + Eq,
321        L: Loader,
322    {
323        use jsonld_core::object::Ref;
324        match self.as_ref() {
325            Ref::Value(value) => compact_indexed_value_with(vocabulary, value, index, active_context, active_property, loader, options).await,
326            Ref::Node(node) => compact_indexed_node_with(vocabulary, node, index, active_context, type_scoped_context, active_property, loader, options).await,
327            Ref::List(list) => {
328                let mut active_context = active_context;
329                // If active context has a previous context, the active context is not propagated.
330                // If element does not contain an @value entry, and element does not consist of
331                // a single @id entry, set active context to previous context from active context,
332                // as the scope of a term-scoped context does not apply when processing new node objects.
333                if let Some(previous_context) = active_context.previous_context() {
334                    active_context = previous_context;
335                }
336
337                // If the term definition for active property in active context has a local context:
338                //
339                // Known deviation from the specification text, which says to look the
340                // term definition up in the active context. This looks it up in
341                // `type_scoped_context` instead, which is what makes the W3C
342                // compaction test suite pass; the spec text is believed to be in error.
343                // See https://github.com/w3c/json-ld-api/issues/502.
344                let mut active_context = ContextRef::Borrowed(active_context);
345                let mut list_container = false;
346                if let Some(active_property) = active_property
347                    && let Some(active_property_definition) = type_scoped_context.get(active_property)
348                {
349                    if let Some(local_context) = active_property_definition.context() {
350                        active_context = ContextRef::owned(
351                            local_context
352                                .process_with(
353                                    vocabulary,
354                                    active_context.as_ref(),
355                                    loader,
356                                    active_property_definition.base_url().cloned(),
357                                    ProcessingOptions::from(options).with_override(),
358                                )
359                                .await?
360                                .into_processed(),
361                        );
362                    }
363
364                    list_container = active_property_definition.container().contains(ContainerKind::List);
365                }
366
367                if list_container {
368                    compact_collection_with(
369                        vocabulary,
370                        list.iter(),
371                        active_context.as_ref(),
372                        active_context.as_ref(),
373                        active_property,
374                        loader,
375                        options,
376                    )
377                    .await
378                } else {
379                    let mut result = jstrict::Object::default();
380                    compact_property(
381                        vocabulary,
382                        &mut result,
383                        Term::Keyword(Keyword::List),
384                        list,
385                        active_context.as_ref(),
386                        loader,
387                        false,
388                        options,
389                    )
390                    .await?;
391
392                    // If expanded property is @index and active property has a container mapping in
393                    // active context that includes @index,
394                    if let Some(index) = index {
395                        let mut index_container = false;
396                        if let Some(active_property) = active_property
397                            && let Some(active_property_definition) = active_context.get(active_property)
398                            && active_property_definition.container().contains(ContainerKind::Index)
399                        {
400                            // then the compacted result will be inside of an @index container,
401                            // drop the @index entry by continuing to the next expanded property.
402                            index_container = true;
403                        }
404
405                        if !index_container {
406                            // Initialize alias by IRI compacting expanded property.
407                            let alias = crate::iri::keyword_alias(vocabulary, active_context.as_ref(), options, Keyword::Index);
408
409                            // Add an entry alias to result whose value is set to expanded value and continue with the next expanded property.
410                            result.insert(alias.into(), jstrict::Value::String(index.into()));
411                        }
412                    }
413
414                    Ok(jstrict::Value::Object(result))
415                }
416            }
417        }
418    }
419}
420
421/// Adds `value` under `key` in `map`, following the JSON-LD API's [add value][1]
422/// algorithm.
423///
424/// A key that already holds something becomes an array of both values. When
425/// `as_array` is true the value is wrapped in an array even if `key` was absent,
426/// so that a term with an `@set` container always compacts to an array. An
427/// array `value` is added element by element rather than nested.
428///
429/// [1]: https://www.w3.org/TR/json-ld-api/#add-value
430fn add_value(map: &mut jstrict::Object, key: &str, value: jstrict::Value, as_array: bool) {
431    match value {
432        jstrict::Value::Array(values) => {
433            // Establish the entry's shape before adding the elements: wrap an
434            // existing scalar into a single-element array, or insert an empty
435            // array when `as_array` is set and the entry is absent, so that an
436            // empty `values` still leaves an array behind.
437            // Compaction-built objects never have duplicate keys, so the `Err`
438            // (duplicate key) case of `get_unique_mut` is treated as absent.
439            match map.get_unique_mut(key).ok().flatten() {
440                Some(existing) if !existing.is_array() => {
441                    let prev = std::mem::replace(existing, jstrict::Value::Array(Vec::new().into()));
442                    if let jstrict::Value::Array(arr) = existing {
443                        arr.push(prev);
444                    }
445                }
446                None if as_array => {
447                    map.insert(key.into(), jstrict::Value::Array(Vec::new().into()));
448                }
449                _ => {}
450            }
451            for v in values {
452                add_value(map, key, v, false);
453            }
454        }
455        scalar => match map.get_unique_mut(key).ok().flatten() {
456            Some(jstrict::Value::Array(arr)) => {
457                arr.push(scalar);
458            }
459            Some(existing) => {
460                // Existing scalar — wrap into [prev, new] array.
461                let prev = std::mem::replace(existing, jstrict::Value::Array(Vec::new().into()));
462                if let jstrict::Value::Array(arr) = existing {
463                    arr.push(prev);
464                    arr.push(scalar);
465                }
466            }
467            None if as_array => {
468                map.insert(key.into(), jstrict::Value::Array(vec![scalar].into()));
469            }
470            None => {
471                map.insert(key.into(), scalar);
472            }
473        },
474    }
475}
476
477/// Returns the JSON value carried by the `@value` entry of a value object,
478/// discarding its `@type`, `@language` and `@direction`.
479fn value_value<I>(value: &Value<I>) -> jstrict::Value {
480    use jsonld_core::object::Literal;
481    match value {
482        Value::Literal(lit, _ty) => match lit {
483            Literal::Null => jstrict::Value::Null,
484            Literal::Boolean(b) => jstrict::Value::Boolean(*b),
485            Literal::Number(n) => jstrict::Value::Number(n.clone()),
486            Literal::String(s) => jstrict::Value::String(s.as_str().into()),
487        },
488        Value::LangString(s) => jstrict::Value::String(s.as_str().into()),
489        Value::Json(json) => json.clone(),
490    }
491}
492
493/// Compacts every item of a collection, then unwraps the result to a single
494/// value when the `compactArrays` option allows it.
495///
496/// Items compacting to `null` are dropped. A single-element array is kept as an
497/// array when `options.compact_arrays` is `false`, when `active_property` is
498/// `@graph` or `@set`, or when its container mapping includes `@list` or
499/// `@set`.
500async fn compact_collection_with<'a, N, L, O, T>(
501    vocabulary: &'a mut N,
502    items: O,
503    active_context: &'a Context<N::Iri, N::BlankId>,
504    type_scoped_context: &'a Context<N::Iri, N::BlankId>,
505    active_property: Option<&'a str>,
506    loader: &'a L,
507    options: Options,
508) -> CompactFragmentResult<L::Error>
509where
510    N: VocabularyMut,
511    N::Iri: Clone + Hash + Eq,
512    N::BlankId: Clone + Hash + Eq,
513    T: 'a + CompactFragment<N::Iri, N::BlankId>,
514    O: 'a + Iterator<Item = &'a T>,
515    L: Loader,
516{
517    let mut result = Vec::new();
518
519    for item in items {
520        let compacted_item =
521            Recursion::new(item.compact_fragment_full(vocabulary, active_context, type_scoped_context, active_property, loader, options)).await?;
522
523        if !compacted_item.is_null() {
524            result.push(compacted_item);
525        }
526    }
527
528    let mut list_or_set = false;
529    if let Some(active_property) = active_property
530        && let Some(active_property_definition) = active_context.get(active_property)
531    {
532        list_or_set =
533            active_property_definition.container().contains(ContainerKind::List) || active_property_definition.container().contains(ContainerKind::Set);
534    }
535
536    if result.len() != 1 || !options.compact_arrays || active_property == Some("@graph") || active_property == Some("@set") || list_or_set {
537        return Ok(jstrict::Value::Array(result.into_iter().collect()));
538    }
539
540    // `result.len() == 1` after the early return above, so the fallback
541    // is unreachable.
542    Ok(result.into_iter().next().unwrap_or(jstrict::Value::Null))
543}
544
545impl<T: CompactFragment<I, B>, I, B> CompactFragment<I, B> for IndexSet<T> {
546    async fn compact_fragment_full<'a, N, L>(
547        &'a self,
548        vocabulary: &'a mut N,
549        active_context: &'a Context<I, B>,
550        type_scoped_context: &'a Context<I, B>,
551        active_property: Option<&'a str>,
552        loader: &'a L,
553        options: Options,
554    ) -> CompactFragmentResult<L::Error>
555    where
556        N: VocabularyMut<Iri = I, BlankId = B>,
557        I: Clone + Hash + Eq,
558        B: Clone + Hash + Eq,
559        L: Loader,
560    {
561        compact_collection_with(vocabulary, self.iter(), active_context, type_scoped_context, active_property, loader, options).await
562    }
563}
564
565impl<T: CompactFragment<I, B>, I, B> CompactFragment<I, B> for Vec<T> {
566    async fn compact_fragment_full<'a, N, L>(
567        &'a self,
568        vocabulary: &'a mut N,
569        active_context: &'a Context<I, B>,
570        type_scoped_context: &'a Context<I, B>,
571        active_property: Option<&'a str>,
572        loader: &'a L,
573        options: Options,
574    ) -> CompactFragmentResult<L::Error>
575    where
576        N: VocabularyMut<Iri = I, BlankId = B>,
577        I: Clone + Hash + Eq,
578        B: Clone + Hash + Eq,
579        L: Loader,
580    {
581        compact_collection_with(vocabulary, self.iter(), active_context, type_scoped_context, active_property, loader, options).await
582    }
583}
584
585impl<T: CompactFragment<I, B>, I, B> CompactFragment<I, B> for [T] {
586    async fn compact_fragment_full<'a, N, L>(
587        &'a self,
588        vocabulary: &'a mut N,
589        active_context: &'a Context<I, B>,
590        type_scoped_context: &'a Context<I, B>,
591        active_property: Option<&'a str>,
592        loader: &'a L,
593        options: Options,
594    ) -> CompactFragmentResult<L::Error>
595    where
596        N: VocabularyMut<Iri = I, BlankId = B>,
597        I: Clone + Hash + Eq,
598        B: Clone + Hash + Eq,
599        L: Loader,
600    {
601        compact_collection_with(vocabulary, self.iter(), active_context, type_scoped_context, active_property, loader, options).await
602    }
603}