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
8pub 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 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
64pub 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 let mut result = jstrict::Object::default();
120
121 let active_property_definition = match active_property {
132 Some(active_property) => active_context.get(active_property),
133 None => None,
134 };
135
136 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 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 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 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 #[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 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}