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