Skip to main content

jsonld_compaction/
value.rs

1use crate::{Error, IriConfusedWithPrefix, Options, compact_iri, iri::keyword_alias};
2use jsonld_context_processing::{Options as ProcessingOptions, Process, WarningHandler};
3use jsonld_core::{Container, ContainerKind, Context, ContextRef, Id, Loader, Term, Type, Value, object};
4use jsonld_syntax::Keyword;
5use rdfx::vocabulary::{Vocabulary, VocabularyMut};
6use std::hash::Hash;
7
8/// Compacts a value object, following the [value compaction algorithm][1].
9///
10/// Reduces the value object to a bare JSON scalar whenever the active property's
11/// type, language and direction mappings already say everything the value object
12/// spells out; otherwise rebuilds it as an object with `@value` and whichever of
13/// `@type`, `@language`, `@direction` and `@index` still carry information.
14///
15/// `index` is the `@index` the value was reached through. It is dropped when the
16/// active property's container mapping includes `@index`, because it has already
17/// become the enclosing map's key.
18///
19/// The active property's scoped context, if it has one, is processed first;
20/// everything after that is [`compact_value`].
21///
22/// [1]: https://www.w3.org/TR/json-ld-api/#value-compaction
23pub async fn compact_indexed_value_with<N, L, W>(
24    vocabulary: &mut N,
25    value: &Value<N::Iri>,
26    index: Option<&str>,
27    active_context: &Context<N::Iri, N::BlankId>,
28    active_property: Option<&str>,
29    loader: &L,
30    options: Options,
31    warnings: &mut W,
32) -> Result<jstrict::Value, Error<L::Error>>
33where
34    N: VocabularyMut,
35    N::Iri: Clone + Hash + Eq,
36    N::BlankId: Clone + Hash + Eq,
37    L: Loader,
38    W: WarningHandler<N>,
39{
40    // If the term definition for active property in active context has a local context:
41    let mut active_context = ContextRef::Borrowed(active_context);
42    if let Some(active_property) = active_property
43        && let Some(active_property_definition) = active_context.get(active_property)
44        && let Some(local_context) = active_property_definition.context()
45    {
46        active_context = ContextRef::owned(
47            local_context
48                .process_full(
49                    vocabulary,
50                    active_context.as_ref(),
51                    loader,
52                    active_property_definition.base_url().cloned(),
53                    ProcessingOptions::from(options).with_override(),
54                    &mut *warnings,
55                )
56                .await?
57                .into_processed(),
58        );
59    }
60
61    Ok(compact_value(vocabulary, value, index, active_context.as_ref(), active_property, options)?)
62}
63
64/// Compacts a value object against an active context in which the active
65/// property's scoped context is already in force, following the
66/// [value compaction algorithm][1].
67///
68/// Reduces the value object to a bare JSON scalar whenever the active
69/// property's type, language and direction mappings already say everything
70/// the value object spells out — a typed literal under a term with that type
71/// mapping, a language-tagged string under a term with that language, a
72/// `@json` literal under a `@json`-typed term. Otherwise rebuilds it as an
73/// object keyed by the active context's aliases of `@value`, `@type`,
74/// `@language`, `@direction` and `@index`, compacting a datatype IRI with
75/// `vocab` set to `true`.
76///
77/// `active_property` is the compacted term the value is written under, as
78/// [`compact_iri_with`][crate::compact_iri_with] or
79/// [`compact_iri_with_hint`][crate::compact_iri_with_hint] selected it, or
80/// `None` at the top level. When that term defines a scoped `@context`,
81/// `active_context` must be the result of processing it on top of the
82/// context the term was selected in, as the compaction algorithm does before
83/// calling this; when it does not, `active_context` is that context itself.
84///
85/// `index` is the `@index` the value was reached through. It is dropped when
86/// the active property's container mapping includes `@index`, because it has
87/// already become the enclosing map's key.
88///
89/// This is synchronous and needs no loader. Only `options.processing_mode`
90/// is read.
91///
92/// [1]: https://www.w3.org/TR/json-ld-api/#value-compaction
93///
94/// # Errors
95///
96/// Returns [`IriConfusedWithPrefix`] when the value's datatype IRI can only
97/// be written out in full and would then be read back as a compact IRI.
98pub fn compact_value<N>(
99    vocabulary: &N,
100    value: &Value<N::Iri>,
101    index: Option<&str>,
102    active_context: &Context<N::Iri, N::BlankId>,
103    active_property: Option<&str>,
104    options: Options,
105) -> Result<jstrict::Value, IriConfusedWithPrefix>
106where
107    N: Vocabulary,
108    N::Iri: Clone + Hash + Eq,
109    N::BlankId: Clone + Hash + Eq,
110{
111    // If element has an @value or @id entry and the result of using the Value Compaction algorithm,
112    // passing active context, active property, and element as value is a scalar,
113    // or the term definition for active property has a type mapping of @json,
114    // return that result.
115
116    // Here starts the Value Compaction Algorithm.
117
118    // Initialize result to a copy of value.
119    let mut result = jstrict::Object::default();
120
121    // If the active context has a null inverse context,
122    // set inverse context in active context to the result of calling the
123    // Inverse Context Creation algorithm using active context.
124    //
125    // Initialize inverse context to the value of inverse context in active context.
126    //
127    // These two steps have no counterpart here: `Context::inverse` builds the
128    // inverse context on first access and caches it on the context, so it is
129    // never observably null and nothing has to bind it to a local.
130
131    let active_property_definition = match active_property {
132        Some(active_property) => active_context.get(active_property),
133        None => None,
134    };
135
136    // Initialize language to the language mapping for active property in active context,
137    // if any, otherwise to the default language of active context.
138    let language = match active_property_definition {
139        Some(def) => match def.language() {
140            Some(lang) => lang.as_ref().map(|l| l.as_lenient_lang_tag_ref()).option(),
141            None => active_context.default_language(),
142        },
143        None => active_context.default_language(),
144    };
145
146    // Initialize direction to the direction mapping for active property in active context,
147    // if any, otherwise to the default base direction of active context.
148    let direction = match active_property_definition {
149        Some(def) => match def.direction() {
150            Some(dir) => dir.option(),
151            None => active_context.default_base_direction(),
152        },
153        None => active_context.default_base_direction(),
154    };
155
156    // If value has an @id entry and has no other entries other than @index:
157    //
158    // Unreachable here: this function only ever receives a value object, which
159    // by construction has an `@value` entry and no `@id`. The `@id`-only case is
160    // handled where node objects are compacted, in `node.rs`.
161
162    // Otherwise, if value has an @type entry whose value matches the type mapping of
163    // active property, set result to the value associated with the @value entry of value.
164    let type_mapping: Option<Type<N::Iri>> = match active_property_definition {
165        Some(def) => def.typ().cloned(),
166        None => None,
167    };
168
169    let container_mapping = match active_property_definition {
170        Some(def) => def.container(),
171        None => Container::None,
172    };
173
174    let remove_index = (index.is_some() && container_mapping.contains(ContainerKind::Index)) || index.is_none();
175
176    match value {
177        Value::Literal(lit, ty) => {
178            use object::value::Literal;
179            if ty.clone().map(Type::Iri) == type_mapping && remove_index {
180                match lit {
181                    Literal::Null => return Ok(jstrict::Value::Null),
182                    Literal::Boolean(b) => return Ok(jstrict::Value::Boolean(*b)),
183                    Literal::Number(n) => return Ok(jstrict::Value::Number(n.clone())),
184                    Literal::String(s) => {
185                        if ty.is_some() || (language.is_none() && direction.is_none()) {
186                            return Ok(jstrict::Value::String(s.as_str().into()));
187                        }
188                        let compact_key = keyword_alias(vocabulary, active_context, options, Keyword::Value);
189                        result.insert(compact_key.into(), jstrict::Value::String(s.as_str().into()));
190                    }
191                }
192            } else {
193                let value_key = keyword_alias(vocabulary, active_context, options, Keyword::Value);
194                match lit {
195                    Literal::Null => {
196                        result.insert(value_key.into(), jstrict::Value::Null);
197                    }
198                    Literal::Boolean(b) => {
199                        result.insert(value_key.into(), jstrict::Value::Boolean(*b));
200                    }
201                    Literal::Number(n) => {
202                        result.insert(value_key.into(), jstrict::Value::Number(n.clone()));
203                    }
204                    Literal::String(s) => {
205                        result.insert(value_key.into(), jstrict::Value::String(s.as_str().into()));
206                    }
207                }
208
209                if let Some(ty) = ty {
210                    let type_key = keyword_alias(vocabulary, active_context, options, Keyword::Type);
211                    let compact_ty = compact_iri(vocabulary, active_context, &Term::Id(Id::iri(ty.clone())), true, false, options)?;
212                    result.insert(
213                        type_key.into(),
214                        match compact_ty {
215                            Some(s) => jstrict::Value::String((&*s).into()),
216                            None => jstrict::Value::Null,
217                        },
218                    );
219                }
220            }
221        }
222        Value::LangString(ls) => {
223            let ls_language = ls.language();
224            let ls_direction = ls.direction();
225
226            // The string can be written bare only if the active property's
227            // language and direction mappings already imply the ones carried by
228            // the value.
229            if remove_index && (ls_language.is_none() || language == ls_language) && (ls_direction.is_none() || direction == ls_direction) {
230                return Ok(jstrict::Value::String(ls.as_str().into()));
231            }
232            let value_key = keyword_alias(vocabulary, active_context, options, Keyword::Value);
233            result.insert(value_key.into(), jstrict::Value::String(ls.as_str().into()));
234
235            if let Some(language) = ls.language() {
236                let lang_key = keyword_alias(vocabulary, active_context, options, Keyword::Language);
237                result.insert(lang_key.into(), jstrict::Value::String(language.as_str().into()));
238            }
239
240            if let Some(direction) = ls.direction() {
241                let dir_key = keyword_alias(vocabulary, active_context, options, Keyword::Direction);
242                result.insert(dir_key.into(), jstrict::Value::String(direction.as_str().into()));
243            }
244        }
245        Value::Json(value) => {
246            if type_mapping == Some(Type::Json) && remove_index {
247                return Ok(value.clone());
248            }
249            let value_key = keyword_alias(vocabulary, active_context, options, Keyword::Value);
250            result.insert(value_key.into(), value.clone());
251
252            let type_key = keyword_alias(vocabulary, active_context, options, Keyword::Type);
253            let json_alias = keyword_alias(vocabulary, active_context, options, Keyword::Json);
254            result.insert(type_key.into(), jstrict::Value::String(json_alias.into()));
255        }
256    }
257
258    if !remove_index && let Some(index) = index {
259        let index_key = keyword_alias(vocabulary, active_context, options, Keyword::Index);
260        result.insert(index_key.into(), jstrict::Value::String(index.into()));
261    }
262
263    Ok(jstrict::Value::Object(result))
264}
265
266#[cfg(test)]
267mod tests {
268    #![allow(clippy::unwrap_used)]
269    use super::*;
270    use crate::test_fixtures::{VALUE_CONTEXT, as_written_under, compact_one_value_document, expanded_object, json, processed_context};
271    use rdfx::vocabulary::no_vocabulary;
272
273    /// Compacting one value object on its own gives what the full compaction
274    /// algorithm writes for it as the only value of a property, both when the
275    /// value reduces to a scalar and when it stays a value object.
276    #[tokio::test]
277    async fn compact_value_matches_the_compaction_of_a_one_value_document() {
278        let context = processed_context(VALUE_CONTEXT).await;
279
280        // (property, expanded value, expected key, expected compacted value)
281        let cases = [
282            (
283                "https://example.com/count",
284                r#"{ "@value": 5, "@type": "http://www.w3.org/2001/XMLSchema#integer" }"#,
285                "count",
286                "5",
287            ),
288            (
289                "https://example.com/count",
290                r#"{ "@value": "5", "@type": "http://www.w3.org/2001/XMLSchema#string" }"#,
291                "ex:count",
292                r#"{ "value": "5", "type": "xsd:string" }"#,
293            ),
294            (
295                "https://example.com/count",
296                r#"{ "@value": "x", "@type": "https://vocab.example/MyType" }"#,
297                "ex:count",
298                r#"{ "value": "x", "type": "MyType" }"#,
299            ),
300            (
301                "https://example.com/label",
302                r#"{ "@value": "hello", "@language": "en" }"#,
303                "label",
304                r#""hello""#,
305            ),
306            (
307                "https://example.com/label",
308                r#"{ "@value": "hola", "@language": "es" }"#,
309                "ex:label",
310                r#"{ "value": "hola", "lang": "es" }"#,
311            ),
312            ("https://example.com/text", r#"{ "@value": "plain" }"#, "text", r#""plain""#),
313            ("https://example.com/text", r#"{ "@value": true }"#, "text", "true"),
314            (
315                "https://example.com/text",
316                r#"{ "@value": 12345678901234567890.1230 }"#,
317                "text",
318                "12345678901234567890.1230",
319            ),
320            (
321                "https://example.com/text",
322                r#"{ "@value": "x", "@language": "fr", "@direction": "ltr" }"#,
323                "text",
324                r#"{ "value": "x", "lang": "fr", "@direction": "ltr" }"#,
325            ),
326            (
327                "https://example.com/text",
328                r#"{ "@value": "x", "@index": "i1" }"#,
329                "text",
330                r#"{ "value": "x", "@index": "i1" }"#,
331            ),
332            (
333                "https://example.com/json",
334                r#"{ "@value": { "a": [1, 2.50] }, "@type": "@json" }"#,
335                "json",
336                r#"{ "a": [1, 2.50] }"#,
337            ),
338            (
339                "https://example.com/text",
340                r#"{ "@value": { "a": 1 }, "@type": "@json" }"#,
341                "text",
342                r#"{ "value": { "a": 1 }, "type": "@json" }"#,
343            ),
344            ("https://example.com/tags", r#"{ "@value": "t" }"#, "tags", r#""t""#),
345        ];
346
347        for (property, value, expected_key, expected) in cases {
348            let (key, written) = compact_one_value_document(&context, property, value).await;
349            assert_eq!(key, expected_key, "key of {value}");
350
351            let object = expanded_object(value);
352            let compacted = compact_value(
353                no_vocabulary(),
354                object.as_value().unwrap(),
355                object.index(),
356                &context,
357                Some(&key),
358                Options::default(),
359            )
360            .unwrap();
361            assert_eq!(compacted, json(expected), "compaction of {value} under {key}");
362            assert_eq!(
363                as_written_under(&context, &key, compacted),
364                written,
365                "{value} compacted alone and in a document"
366            );
367        }
368    }
369}