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}