Skip to main content

jsonld_compaction/
document.rs

1use jsonld_context_processing::WarningHandler;
2use jsonld_core::{ExpandedDocument, FlattenedDocument, Loader, warning};
3use jsonld_syntax::{IntoJson, Keyword};
4use rdfx::vocabulary::{self, Vocabulary};
5use std::hash::Hash;
6
7use crate::{CompactFragment, iri::IriConfusedWithPrefix};
8
9/// Result of compacting a whole document.
10pub type CompactDocumentResult<E> = Result<jstrict::Value, crate::Error<E>>;
11
12/// Documents that a `@context` can be embedded into.
13///
14/// The final step of document compaction is to record, in the output, the
15/// context the output was compacted against, so that the result round-trips
16/// back to the same expanded document.
17pub trait EmbedContext {
18    /// Embeds `context` into this document as a `@context` entry.
19    ///
20    /// The context is written in its original, unprocessed form, as the first
21    /// entry of the document's top-level object. A document that compacted to an
22    /// array is first wrapped in an object under whatever `@graph` compacts to,
23    /// since only an object can carry `@context`. Nothing is embedded when the
24    /// document compacted to `null` or to nothing at all, or when the context
25    /// itself is null or empty.
26    ///
27    /// # Errors
28    ///
29    /// Returns an error when the context cannot be embedded in the compacted document.
30    fn embed_context<N>(
31        &mut self,
32        vocabulary: &N,
33        context: jsonld_context_processing::ProcessedRef<N::Iri, N::BlankId>,
34        options: crate::Options,
35    ) -> Result<(), IriConfusedWithPrefix>
36    where
37        N: Vocabulary,
38        N::Iri: Clone + Hash + Eq,
39        N::BlankId: Clone + Hash + Eq;
40}
41
42/// Whole documents that can be compacted against a processed `@context`.
43///
44/// Implemented for [`ExpandedDocument`] and [`FlattenedDocument`]. Unlike
45/// [`CompactFragment`], these methods take the context in both its processed and
46/// its original form: the processed form drives compaction, and the original form
47/// is embedded into the output as its `@context`.
48pub trait Compact<I, B> {
49    /// Compacts this document with the given [`Options`][crate::Options].
50    ///
51    /// `warnings` receives every warning raised while processing the scoped
52    /// contexts compaction meets.
53    async fn compact_full<'a, N, L, W>(
54        &'a self,
55        vocabulary: &'a mut N,
56        context: jsonld_context_processing::ProcessedRef<'a, 'a, I, B>,
57        loader: &'a L,
58        options: crate::Options,
59        warnings: W,
60    ) -> CompactDocumentResult<L::Error>
61    where
62        N: rdfx::vocabulary::VocabularyMut<Iri = I, BlankId = B>,
63        I: Clone + Hash + Eq,
64        B: Clone + Hash + Eq,
65        L: Loader,
66        W: WarningHandler<N>;
67
68    /// Compacts this document with the default
69    /// [`Options`][crate::Options], using `vocabulary` to resolve IRI and blank
70    /// node identifiers.
71    ///
72    /// Warnings are printed to standard error.
73    async fn compact_with<'a, N, L>(
74        &'a self,
75        vocabulary: &'a mut N,
76        context: jsonld_context_processing::ProcessedRef<'a, 'a, I, B>,
77        loader: &'a L,
78    ) -> CompactDocumentResult<L::Error>
79    where
80        N: rdfx::vocabulary::VocabularyMut<Iri = I, BlankId = B>,
81        I: Clone + Hash + Eq,
82        B: Clone + Hash + Eq,
83        L: Loader,
84    {
85        self.compact_full(vocabulary, context, loader, crate::Options::default(), warning::Print).await
86    }
87
88    /// Compacts this document with the default [`Options`][crate::Options], for
89    /// documents that store IRIs and blank node identifiers inline instead of
90    /// indexing them through a vocabulary.
91    ///
92    /// Warnings are printed to standard error.
93    async fn compact<'a, L>(&'a self, context: jsonld_context_processing::ProcessedRef<'a, 'a, I, B>, loader: &'a L) -> CompactDocumentResult<L::Error>
94    where
95        (): rdfx::vocabulary::VocabularyMut<Iri = I, BlankId = B>,
96        I: Clone + Hash + Eq,
97        B: Clone + Hash + Eq,
98        L: Loader,
99    {
100        self.compact_with(vocabulary::no_vocabulary_mut(), context, loader).await
101    }
102}
103
104impl<I, B> Compact<I, B> for ExpandedDocument<I, B> {
105    async fn compact_full<'a, N, L, W>(
106        &'a self,
107        vocabulary: &'a mut N,
108        context: jsonld_context_processing::ProcessedRef<'a, 'a, I, B>,
109        loader: &'a L,
110        options: crate::Options,
111        mut warnings: W,
112    ) -> CompactDocumentResult<L::Error>
113    where
114        N: rdfx::vocabulary::VocabularyMut<Iri = I, BlankId = B>,
115        I: Clone + Hash + Eq,
116        B: Clone + Hash + Eq,
117        L: Loader,
118        W: WarningHandler<N>,
119    {
120        let mut compacted_output = self
121            .objects()
122            .compact_fragment_full(vocabulary, context.processed(), context.processed(), None, loader, options, &mut warnings)
123            .await?;
124
125        compacted_output.embed_context(vocabulary, context, options)?;
126
127        Ok(compacted_output)
128    }
129}
130
131impl<I, B> Compact<I, B> for FlattenedDocument<I, B> {
132    async fn compact_full<'a, N, L, W>(
133        &'a self,
134        vocabulary: &'a mut N,
135        context: jsonld_context_processing::ProcessedRef<'a, 'a, I, B>,
136        loader: &'a L,
137        options: crate::Options,
138        mut warnings: W,
139    ) -> CompactDocumentResult<L::Error>
140    where
141        N: rdfx::vocabulary::VocabularyMut<Iri = I, BlankId = B>,
142        I: Clone + Hash + Eq,
143        B: Clone + Hash + Eq,
144        L: Loader,
145        W: WarningHandler<N>,
146    {
147        let mut compacted_output = self
148            .compact_fragment_full(vocabulary, context.processed(), context.processed(), None, loader, options, &mut warnings)
149            .await?;
150
151        compacted_output.embed_context(vocabulary, context, options)?;
152
153        Ok(compacted_output)
154    }
155}
156
157impl EmbedContext for jstrict::Value {
158    fn embed_context<N>(
159        &mut self,
160        vocabulary: &N,
161        context: jsonld_context_processing::ProcessedRef<N::Iri, N::BlankId>,
162        options: crate::Options,
163    ) -> Result<(), IriConfusedWithPrefix>
164    where
165        N: Vocabulary,
166        N::Iri: Clone + Hash + Eq,
167        N::BlankId: Clone + Hash + Eq,
168    {
169        let value = self.take();
170
171        let obj = match value {
172            jstrict::Value::Array(array) => {
173                let mut obj = jstrict::Object::new();
174
175                if !array.is_empty() {
176                    let key = crate::iri::keyword_alias(vocabulary, context.processed(), options, Keyword::Graph);
177
178                    obj.insert(key.into(), array.into());
179                }
180
181                Some(obj)
182            }
183            jstrict::Value::Object(obj) => Some(obj),
184            _null => None,
185        };
186
187        if let Some(mut obj) = obj {
188            let json_context = IntoJson::into_json(context.unprocessed().clone());
189
190            if !obj.is_empty() && !json_context.is_null() && !json_context.is_empty_array_or_object() {
191                obj.insert_front("@context".into(), json_context);
192            }
193
194            *self = obj.into();
195        }
196
197        Ok(())
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    #![allow(clippy::unwrap_used)]
204
205    use crate::{Compact, Options, test_fixtures::json};
206    use jsonld_context_processing::{Process, ProcessedRef, Warning};
207    use jsonld_core::{ExpandedDocument, NoLoader, TryFromJson, warning::WarningBuf};
208    use jsonld_syntax::{TryFromJson as _, context::Context as SyntaxContext};
209    use rdfx::{BlankIdBuf, IriBuf, vocabulary::no_vocabulary_mut};
210
211    /// A property-scoped context is processed again when compaction reaches a
212    /// node written under that property, and what that processing reports
213    /// reaches the handler given to `compact_full`.
214    #[tokio::test]
215    async fn scoped_context_warnings_reach_the_compaction_handler() {
216        let context = SyntaxContext::try_from_json(&json(
217            r#"{
218                "p": { "@id": "https://example.com/p", "@context": { "bad": { "@id": "@foo" } } },
219                "q": "https://example.com/q"
220            }"#,
221        ))
222        .unwrap();
223        let mut processing_warnings = WarningBuf::<Warning>::new();
224        let processed = context
225            .process_full(
226                no_vocabulary_mut(),
227                &jsonld_core::Context::new(None),
228                &NoLoader,
229                None,
230                jsonld_context_processing::Options::default(),
231                &mut processing_warnings,
232            )
233            .await
234            .unwrap()
235            .into_processed();
236        let document = ExpandedDocument::<IriBuf, BlankIdBuf>::try_from_json_in(
237            no_vocabulary_mut(),
238            json(r#"[{ "https://example.com/p": [{ "https://example.com/q": [{ "@value": 1 }] }] }]"#),
239        )
240        .unwrap();
241
242        let mut warnings = WarningBuf::<Warning>::new();
243        document
244            .compact_full(
245                no_vocabulary_mut(),
246                ProcessedRef::new(&context, &processed),
247                &NoLoader,
248                Options::default(),
249                &mut warnings,
250            )
251            .await
252            .unwrap();
253
254        assert_eq!(warnings.0, vec![Warning::KeywordLikeValue(String::from("@foo"))]);
255    }
256}