1use crate::{Error, IriConfusedWithPrefix, Options, compact_iri, iri::keyword_alias};
2use jsonld_context_processing::{Options as ProcessingOptions, Process};
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>(
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) -> Result<jstrict::Value, Error<L::Error>>
32where
33 N: VocabularyMut,
34 N::Iri: Clone + Hash + Eq,
35 N::BlankId: Clone + Hash + Eq,
36 L: Loader,
37{
38 let mut active_context = ContextRef::Borrowed(active_context);
40 if let Some(active_property) = active_property
41 && let Some(active_property_definition) = active_context.get(active_property)
42 && let Some(local_context) = active_property_definition.context()
43 {
44 active_context = ContextRef::owned(
45 local_context
46 .process_with(
47 vocabulary,
48 active_context.as_ref(),
49 loader,
50 active_property_definition.base_url().cloned(),
51 ProcessingOptions::from(options).with_override(),
52 )
53 .await?
54 .into_processed(),
55 );
56 }
57
58 Ok(compact_value(vocabulary, value, index, active_context.as_ref(), active_property, options)?)
59}
60
61pub fn compact_value<N>(
96 vocabulary: &N,
97 value: &Value<N::Iri>,
98 index: Option<&str>,
99 active_context: &Context<N::Iri, N::BlankId>,
100 active_property: Option<&str>,
101 options: Options,
102) -> Result<jstrict::Value, IriConfusedWithPrefix>
103where
104 N: Vocabulary,
105 N::Iri: Clone + Hash + Eq,
106 N::BlankId: Clone + Hash + Eq,
107{
108 let mut result = jstrict::Object::default();
117
118 let active_property_definition = match active_property {
129 Some(active_property) => active_context.get(active_property),
130 None => None,
131 };
132
133 let language = match active_property_definition {
136 Some(def) => match def.language() {
137 Some(lang) => lang.as_ref().map(|l| l.as_lenient_lang_tag_ref()).option(),
138 None => active_context.default_language(),
139 },
140 None => active_context.default_language(),
141 };
142
143 let direction = match active_property_definition {
146 Some(def) => match def.direction() {
147 Some(dir) => dir.option(),
148 None => active_context.default_base_direction(),
149 },
150 None => active_context.default_base_direction(),
151 };
152
153 let type_mapping: Option<Type<N::Iri>> = match active_property_definition {
162 Some(def) => def.typ().cloned(),
163 None => None,
164 };
165
166 let container_mapping = match active_property_definition {
167 Some(def) => def.container(),
168 None => Container::None,
169 };
170
171 let remove_index = (index.is_some() && container_mapping.contains(ContainerKind::Index)) || index.is_none();
172
173 match value {
174 Value::Literal(lit, ty) => {
175 use object::value::Literal;
176 if ty.clone().map(Type::Iri) == type_mapping && remove_index {
177 match lit {
178 Literal::Null => return Ok(jstrict::Value::Null),
179 Literal::Boolean(b) => return Ok(jstrict::Value::Boolean(*b)),
180 Literal::Number(n) => return Ok(jstrict::Value::Number(n.clone())),
181 Literal::String(s) => {
182 if ty.is_some() || (language.is_none() && direction.is_none()) {
183 return Ok(jstrict::Value::String(s.as_str().into()));
184 }
185 let compact_key = keyword_alias(vocabulary, active_context, options, Keyword::Value);
186 result.insert(compact_key.into(), jstrict::Value::String(s.as_str().into()));
187 }
188 }
189 } else {
190 let value_key = keyword_alias(vocabulary, active_context, options, Keyword::Value);
191 match lit {
192 Literal::Null => {
193 result.insert(value_key.into(), jstrict::Value::Null);
194 }
195 Literal::Boolean(b) => {
196 result.insert(value_key.into(), jstrict::Value::Boolean(*b));
197 }
198 Literal::Number(n) => {
199 result.insert(value_key.into(), jstrict::Value::Number(n.clone()));
200 }
201 Literal::String(s) => {
202 result.insert(value_key.into(), jstrict::Value::String(s.as_str().into()));
203 }
204 }
205
206 if let Some(ty) = ty {
207 let type_key = keyword_alias(vocabulary, active_context, options, Keyword::Type);
208 let compact_ty = compact_iri(vocabulary, active_context, &Term::Id(Id::iri(ty.clone())), true, false, options)?;
209 result.insert(
210 type_key.into(),
211 match compact_ty {
212 Some(s) => jstrict::Value::String((&*s).into()),
213 None => jstrict::Value::Null,
214 },
215 );
216 }
217 }
218 }
219 Value::LangString(ls) => {
220 let ls_language = ls.language();
221 let ls_direction = ls.direction();
222
223 if remove_index && (ls_language.is_none() || language == ls_language) && (ls_direction.is_none() || direction == ls_direction) {
227 return Ok(jstrict::Value::String(ls.as_str().into()));
228 }
229 let value_key = keyword_alias(vocabulary, active_context, options, Keyword::Value);
230 result.insert(value_key.into(), jstrict::Value::String(ls.as_str().into()));
231
232 if let Some(language) = ls.language() {
233 let lang_key = keyword_alias(vocabulary, active_context, options, Keyword::Language);
234 result.insert(lang_key.into(), jstrict::Value::String(language.as_str().into()));
235 }
236
237 if let Some(direction) = ls.direction() {
238 let dir_key = keyword_alias(vocabulary, active_context, options, Keyword::Direction);
239 result.insert(dir_key.into(), jstrict::Value::String(direction.as_str().into()));
240 }
241 }
242 Value::Json(value) => {
243 if type_mapping == Some(Type::Json) && remove_index {
244 return Ok(value.clone());
245 }
246 let value_key = keyword_alias(vocabulary, active_context, options, Keyword::Value);
247 result.insert(value_key.into(), value.clone());
248
249 let type_key = keyword_alias(vocabulary, active_context, options, Keyword::Type);
250 let json_alias = keyword_alias(vocabulary, active_context, options, Keyword::Json);
251 result.insert(type_key.into(), jstrict::Value::String(json_alias.into()));
252 }
253 }
254
255 if !remove_index && let Some(index) = index {
256 let index_key = keyword_alias(vocabulary, active_context, options, Keyword::Index);
257 result.insert(index_key.into(), jstrict::Value::String(index.into()));
258 }
259
260 Ok(jstrict::Value::Object(result))
261}
262
263#[cfg(test)]
264mod tests {
265 #![allow(clippy::unwrap_used)]
266 use super::*;
267 use crate::test_fixtures::{VALUE_CONTEXT, as_written_under, compact_one_value_document, expanded_object, json, processed_context};
268 use rdfx::vocabulary::no_vocabulary;
269
270 #[tokio::test]
274 async fn compact_value_matches_the_compaction_of_a_one_value_document() {
275 let context = processed_context(VALUE_CONTEXT).await;
276
277 let cases = [
279 (
280 "https://example.com/count",
281 r#"{ "@value": 5, "@type": "http://www.w3.org/2001/XMLSchema#integer" }"#,
282 "count",
283 "5",
284 ),
285 (
286 "https://example.com/count",
287 r#"{ "@value": "5", "@type": "http://www.w3.org/2001/XMLSchema#string" }"#,
288 "ex:count",
289 r#"{ "value": "5", "type": "xsd:string" }"#,
290 ),
291 (
292 "https://example.com/count",
293 r#"{ "@value": "x", "@type": "https://vocab.example/MyType" }"#,
294 "ex:count",
295 r#"{ "value": "x", "type": "MyType" }"#,
296 ),
297 (
298 "https://example.com/label",
299 r#"{ "@value": "hello", "@language": "en" }"#,
300 "label",
301 r#""hello""#,
302 ),
303 (
304 "https://example.com/label",
305 r#"{ "@value": "hola", "@language": "es" }"#,
306 "ex:label",
307 r#"{ "value": "hola", "lang": "es" }"#,
308 ),
309 ("https://example.com/text", r#"{ "@value": "plain" }"#, "text", r#""plain""#),
310 ("https://example.com/text", r#"{ "@value": true }"#, "text", "true"),
311 (
312 "https://example.com/text",
313 r#"{ "@value": 12345678901234567890.1230 }"#,
314 "text",
315 "12345678901234567890.1230",
316 ),
317 (
318 "https://example.com/text",
319 r#"{ "@value": "x", "@language": "fr", "@direction": "ltr" }"#,
320 "text",
321 r#"{ "value": "x", "lang": "fr", "@direction": "ltr" }"#,
322 ),
323 (
324 "https://example.com/text",
325 r#"{ "@value": "x", "@index": "i1" }"#,
326 "text",
327 r#"{ "value": "x", "@index": "i1" }"#,
328 ),
329 (
330 "https://example.com/json",
331 r#"{ "@value": { "a": [1, 2.50] }, "@type": "@json" }"#,
332 "json",
333 r#"{ "a": [1, 2.50] }"#,
334 ),
335 (
336 "https://example.com/text",
337 r#"{ "@value": { "a": 1 }, "@type": "@json" }"#,
338 "text",
339 r#"{ "value": { "a": 1 }, "type": "@json" }"#,
340 ),
341 ("https://example.com/tags", r#"{ "@value": "t" }"#, "tags", r#""t""#),
342 ];
343
344 for (property, value, expected_key, expected) in cases {
345 let (key, written) = compact_one_value_document(&context, property, value).await;
346 assert_eq!(key, expected_key, "key of {value}");
347
348 let object = expanded_object(value);
349 let compacted = compact_value(
350 no_vocabulary(),
351 object.as_value().unwrap(),
352 object.index(),
353 &context,
354 Some(&key),
355 Options::default(),
356 )
357 .unwrap();
358 assert_eq!(compacted, json(expected), "compaction of {value} under {key}");
359 assert_eq!(
360 as_written_under(&context, &key, compacted),
361 written,
362 "{value} compacted alone and in a document"
363 );
364 }
365 }
366}