Skip to main content

jsonld_expansion/
value.rs

1use crate::{Action, ExpandedEntry, Warning, WarningHandler, expand_iri};
2use jsonld_context_processing::algorithm::RejectVocab;
3use jsonld_core::{Context, Environment, Id, Indexed, IndexedObject, LangString, Object, ProcessingMode, Term, ValidId, Value, object::value::Literal};
4use jsonld_syntax::{Direction, ErrorCode, Keyword, LenientLangTagBuf, Nullable};
5use rdfx::vocabulary::VocabularyMut;
6
7/// Error raised while expanding a value object, a map with an `@value` entry.
8#[derive(Debug, thiserror::Error)]
9pub enum InvalidValue {
10    /// The `@language` entry of the value object is not a string.
11    #[error("Invalid language tagged string")]
12    LanguageTaggedString,
13
14    /// The `@direction` entry of the value object is neither `"ltr"` nor
15    /// `"rtl"`.
16    #[error("Invalid base `@direction`")]
17    BaseDirection,
18
19    /// The `@index` entry of the value object is not a string.
20    #[error("Invalid `@index` value")]
21    IndexValue,
22
23    /// The `@type` entry of the value object is not a string, or does not
24    /// expand to an IRI or to `@json`.
25    #[error("Invalid typed value")]
26    TypedValue,
27
28    /// The value object has an entry other than `@value`, `@type`,
29    /// `@language`, `@direction` and `@index`, or combines `@type` with
30    /// `@language` or `@direction`.
31    #[error("Invalid value object")]
32    ValueObject,
33
34    /// The `@value` entry holds an array or a map where only a scalar or
35    /// `null` is allowed. A `@type` of `@json` lifts that restriction.
36    #[error("Invalid value object value")]
37    ValueObjectValue,
38
39    /// The value object carries `@language` or `@direction` but its `@value`
40    /// is not a string, so it cannot be tagged.
41    #[error("Invalid language tagged value")]
42    LanguageTaggedValue,
43
44    /// The `@type` entry had to be expanded through the vocabulary mapping
45    /// (`@vocab`), which the [`Policy`](crate::Policy) in use forbids.
46    #[error("Forbidden use of `@vocab`")]
47    ForbiddenVocab,
48}
49
50impl InvalidValue {
51    /// Returns the JSON-LD error code this error is reported under.
52    #[must_use]
53    pub fn code(&self) -> ErrorCode {
54        match self {
55            Self::LanguageTaggedString => ErrorCode::InvalidLanguageTaggedString,
56            Self::BaseDirection => ErrorCode::InvalidBaseDirection,
57            Self::IndexValue => ErrorCode::InvalidIndexValue,
58            Self::TypedValue => ErrorCode::InvalidTypedValue,
59            Self::ValueObject => ErrorCode::InvalidValueObject,
60            Self::ValueObjectValue => ErrorCode::InvalidValueObjectValue,
61            Self::LanguageTaggedValue => ErrorCode::InvalidLanguageTaggedValue,
62            Self::ForbiddenVocab => ErrorCode::InvalidVocabMapping,
63        }
64    }
65}
66
67impl From<RejectVocab> for InvalidValue {
68    fn from(_value: RejectVocab) -> Self {
69        Self::ForbiddenVocab
70    }
71}
72
73/// Result of expanding a value object, `None` standing for a value object that
74/// is dropped because its `@value` is `null`.
75pub type ValueExpansionResult<T, B> = Result<Option<IndexedObject<T, B>>, InvalidValue>;
76
77/// Expands a value object, a map whose entries have already been expanded and
78/// one of which is `@value`.
79///
80/// Implements the value object cases of the [Expansion
81/// algorithm](https://www.w3.org/TR/json-ld11-api/#expansion-algorithm): the
82/// only entries a value object may have are `@value`, `@type`, `@language`,
83/// `@direction` and `@index`, and their combination decides whether the result
84/// is a plain literal, a typed literal, a language-tagged string or a JSON
85/// literal.
86pub(crate) fn expand_value<N, L, W>(
87    env: &mut Environment<N, L, W>,
88    vocab_policy: Action,
89    processing_mode: ProcessingMode,
90    input_type: Option<&Term<N::Iri, N::BlankId>>,
91    type_scoped_context: &Context<N::Iri, N::BlankId>,
92    expanded_entries: Vec<ExpandedEntry<N::Iri, N::BlankId>>,
93    value_entry: &jstrict::Value,
94) -> ValueExpansionResult<N::Iri, N::BlankId>
95where
96    N: VocabularyMut,
97    N::Iri: Clone + PartialEq,
98    N::BlankId: Clone + PartialEq,
99    W: WarningHandler<N>,
100{
101    let mut is_json = input_type.is_some_and(|t| *t == Term::Keyword(Keyword::Json));
102    let mut ty = None;
103    let mut index = None;
104    let mut language = None;
105    let mut direction = None;
106
107    for ExpandedEntry(_, expanded_key, value) in expanded_entries {
108        match expanded_key.as_ref() {
109            // If expanded property is @language:
110            Term::Keyword(Keyword::Language) => {
111                // If value is not a string, an invalid language-tagged string
112                // error has been detected and processing is aborted.
113                if let Some(value) = value.as_str() {
114                    // Otherwise, set expanded value to value. If value is not
115                    // well-formed according to section 2.2.9 of [BCP47],
116                    // processors SHOULD issue a warning. That warning is
117                    // raised further down, where the tag is turned into a
118                    // `LenientLangTagBuf`.
119
120                    if value != "@none" {
121                        language = Some(value.to_owned());
122                    }
123                } else {
124                    return Err(InvalidValue::LanguageTaggedString);
125                }
126            }
127            // If expanded property is @direction:
128            Term::Keyword(Keyword::Direction) => {
129                // If processing mode is json-ld-1.0, continue with the next key
130                // from element.
131                if processing_mode == ProcessingMode::JsonLd1_0 {
132                    continue;
133                }
134
135                // If value is neither "ltr" nor "rtl", an invalid base direction
136                // error has been detected and processing is aborted.
137                if let Some(value) = value.as_str() {
138                    if let Ok(value) = Direction::try_from(value) {
139                        direction = Some(value);
140                    } else {
141                        return Err(InvalidValue::BaseDirection);
142                    }
143                } else {
144                    return Err(InvalidValue::BaseDirection);
145                }
146            }
147            // If expanded property is @index:
148            Term::Keyword(Keyword::Index) => {
149                // If value is not a string, an invalid @index value error has
150                // been detected and processing is aborted.
151                if let Some(value) = value.as_str() {
152                    index = Some(value.to_string());
153                } else {
154                    return Err(InvalidValue::IndexValue);
155                }
156            }
157            // If expanded property is @type:
158            Term::Keyword(Keyword::Type) => {
159                if let Some(ty_value) = value.as_str() {
160                    let expanded_ty = expand_iri(env, type_scoped_context, Nullable::Some(ty_value.into()), true, Some(vocab_policy))?;
161
162                    match expanded_ty.as_deref() {
163                        Some(Term::Keyword(Keyword::Json)) => {
164                            is_json = true;
165                        }
166                        Some(Term::Id(Id::Valid(ValidId::Iri(expanded_ty)))) => {
167                            is_json = false;
168                            ty = Some(expanded_ty.clone());
169                        }
170                        _ => return Err(InvalidValue::TypedValue),
171                    }
172                } else {
173                    return Err(InvalidValue::TypedValue);
174                }
175            }
176            // The `@value` entry itself is handled after the loop, from
177            // `value_entry`.
178            Term::Keyword(Keyword::Value) => (),
179            // A value object must not have any other entry.
180            _ => {
181                return Err(InvalidValue::ValueObject);
182            }
183        }
184    }
185
186    // If input type is @json, set expanded value to value. The specification
187    // also raises an invalid value object value error here when the processing
188    // mode is json-ld-1.0, which this implementation does not: context
189    // processing already rejects a term definition with a `@json` type mapping
190    // under 1.0, but an explicit `"@type": "@json"` entry in a value object
191    // still goes through.
192    if is_json {
193        // A value object must not combine a type with `@language` or
194        // `@direction`, `@json` included.
195        if language.is_some() || direction.is_some() {
196            return Err(InvalidValue::ValueObject);
197        }
198        return Ok(Some(Indexed::new(Object::Value(Value::Json(value_entry.clone())), index)));
199    }
200
201    // Otherwise, if value is not a scalar or null, an invalid value object value
202    // error has been detected and processing is aborted.
203    let result = match value_entry {
204        jstrict::Value::Null => Literal::Null,
205        jstrict::Value::String(s) => Literal::String(s.clone()),
206        jstrict::Value::Number(n) => Literal::Number(n.clone()),
207        jstrict::Value::Boolean(b) => Literal::Boolean(*b),
208        _ => {
209            return Err(InvalidValue::ValueObjectValue);
210        }
211    };
212
213    // A `@type` of `@json` lets the `@value` entry hold any value, treated as a
214    // JSON literal. That case returned above, before the scalar check.
215
216    // Otherwise, if the value of result's @value entry is null, or an empty array,
217    // return null
218    if matches!(result, Literal::Null) {
219        return Ok(None);
220    }
221
222    // Otherwise, if the value of result's @value entry is not a string and result
223    // contains the entry @language, an invalid language-tagged value error has
224    // been detected (only strings can be language-tagged) and processing is
225    // aborted.
226    if language.is_some() || direction.is_some() {
227        // A value object must not have an `@type` entry alongside `@language`
228        // or `@direction`.
229        if ty.is_some() {
230            return Err(InvalidValue::ValueObject);
231        }
232
233        if let Literal::String(s) = result {
234            let lang = match language {
235                Some(language) => {
236                    let (language, error) = LenientLangTagBuf::new(language);
237
238                    if let Some(error) = error {
239                        env.warnings.handle(env.vocabulary, Warning::MalformedLanguageTag(language.to_string(), error));
240                    }
241
242                    Some(language)
243                }
244                None => None,
245            };
246
247            return match LangString::new(s, lang, direction) {
248                Ok(result) => Ok(Some(Indexed::new(Object::Value(Value::LangString(result)), index))),
249                Err(_) => Err(InvalidValue::LanguageTaggedValue),
250            };
251        }
252        return Err(InvalidValue::LanguageTaggedValue);
253    }
254
255    // The specification also drops free-floating value objects here, when the
256    // active property is null or `@graph`. This implementation drops them one
257    // level up instead, in `filter_top_level_item`, which filters both the top
258    // level of the document and the content of every `@graph` entry.
259
260    Ok(Some(Indexed::new(Object::Value(Value::Literal(result, ty)), index)))
261}