1use crate::{Error, IriConfusedWithPrefix, Options, add_value, compact_iri, compact_property, iri::keyword_alias};
2use contextual::WithContext;
3use jsonld_context_processing::{Options as ProcessingOptions, Process, ProcessingMode};
4use jsonld_core::{Container, ContainerKind, Context, ContextRef, Id, Loader, Node, Term, Type};
5use jsonld_syntax::Keyword;
6use rdfx::vocabulary::{Vocabulary, VocabularyMut};
7use std::hash::Hash;
8
9fn optional_string(s: Option<&str>) -> jstrict::Value {
14 s.map_or(jstrict::Value::Null, |s| jstrict::Value::String(s.into()))
15}
16
17pub async fn compact_indexed_node_with<N, L>(
32 vocabulary: &mut N,
33 node: &Node<N::Iri, N::BlankId>,
34 index: Option<&str>,
35 mut active_context: &Context<N::Iri, N::BlankId>,
36 type_scoped_context: &Context<N::Iri, N::BlankId>,
37 active_property: Option<&str>,
38 loader: &L,
39 options: Options,
40) -> Result<jstrict::Value, Error<L::Error>>
41where
42 N: VocabularyMut,
43 N::Iri: Clone + Hash + Eq,
44 N::BlankId: Clone + Hash + Eq,
45 L: Loader,
46{
47 if !(node.is_empty() && node.id.is_some()) {
52 if let Some(previous_context) = active_context.previous_context() {
54 active_context = previous_context;
55 }
56 }
57
58 let mut active_context = ContextRef::Borrowed(active_context);
66 if let Some(active_property) = active_property
67 && let Some(active_property_definition) = type_scoped_context.get(active_property)
68 && let Some(local_context) = active_property_definition.context()
69 {
70 active_context = ContextRef::owned(
71 local_context
72 .process_with(
73 vocabulary,
74 active_context.as_ref(),
75 loader,
76 active_property_definition.base_url().cloned(),
77 ProcessingOptions::from(options).with_override(),
78 )
79 .await?
80 .into_processed(),
81 );
82 }
83
84 if node.is_empty()
85 && let Some(id) = &node.id
86 {
87 return Ok(compact_node_reference(
88 vocabulary,
89 id,
90 index,
91 active_context.as_ref(),
92 active_property,
93 options,
94 )?);
95 }
96
97 let mut result = jstrict::Object::default();
98
99 if !node.types().is_empty() {
100 let mut compacted_types = Vec::new();
105 for ty in node.types() {
106 let compacted_ty = compact_iri(vocabulary, type_scoped_context, &ty.clone().into_term(), true, false, options)?;
107 compacted_types.push(compacted_ty);
108 }
109
110 compacted_types.sort_by(|a, b| a.as_deref().cmp(&b.as_deref()));
111
112 for term in compacted_types.iter().flatten() {
113 if let Some(term_definition) = type_scoped_context.get(&**term)
114 && let Some(local_context) = term_definition.context()
115 {
116 let processing_options = ProcessingOptions::from(options).without_propagation();
117 active_context = ContextRef::owned(
118 local_context
119 .process_with(
120 vocabulary,
121 active_context.as_ref(),
122 loader,
123 term_definition.base_url().cloned(),
124 processing_options,
125 )
126 .await?
127 .into_processed(),
128 );
129 }
130 }
131 }
132
133 let expanded_entries: Vec<_> = if options.ordered {
136 let vocabulary: &N = vocabulary;
142 let mut decorated: Vec<(&str, _)> = node.properties().iter().map(|entry| (entry.0.with(vocabulary).as_str(), entry)).collect();
143 decorated.sort_by(|a, b| a.0.cmp(b.0));
144 decorated.into_iter().map(|(_, entry)| entry).collect()
145 } else {
146 node.properties().iter().collect()
147 };
148
149 if let Some(id_entry) = &node.id {
151 insert_id(vocabulary, &mut result, id_entry, active_context.as_ref(), options)?;
152 }
153
154 compact_types(
155 vocabulary,
156 &mut result,
157 node.types.as_deref(),
158 active_context.as_ref(),
159 type_scoped_context,
160 options,
161 )?;
162
163 if let Some(reverse_properties) = node.reverse_properties_entry()
165 && !reverse_properties.is_empty()
166 {
167 let active_property = "@reverse";
171 if let Some(active_property_definition) = active_context.get(active_property)
172 && let Some(local_context) = active_property_definition.context()
173 {
174 active_context = ContextRef::owned(
175 local_context
176 .process_with(
177 vocabulary,
178 active_context.as_ref(),
179 loader,
180 active_property_definition.base_url().cloned(),
181 ProcessingOptions::from(options).with_override(),
182 )
183 .await?
184 .into_processed(),
185 );
186 }
187
188 let mut reverse_result = jstrict::Object::default();
200 for (expanded_property, expanded_value) in reverse_properties {
201 compact_property(
202 vocabulary,
203 &mut reverse_result,
204 expanded_property.clone().into(),
205 expanded_value.iter(),
206 active_context.as_ref(),
207 loader,
208 true,
209 options,
210 )
211 .await?;
212 }
213
214 let mut reverse_map = jstrict::Object::default();
216 for (property, mapped_value) in &mut reverse_result {
217 let mut value = jstrict::Value::Null;
218 std::mem::swap(&mut value, &mut *mapped_value);
219
220 if let Some(term_definition) = active_context.get(property.as_str())
223 && term_definition.reverse_property()
224 {
225 let as_array = term_definition.container().contains(ContainerKind::Set) || !options.compact_arrays;
228
229 add_value(&mut result, property, value, as_array);
231 continue;
232 }
233
234 reverse_map.insert(property.clone(), value);
235 }
236
237 if !reverse_map.is_empty() {
238 let alias = keyword_alias(vocabulary, active_context.as_ref(), options, Keyword::Reverse);
240
241 result.insert(alias.into(), reverse_map.into());
243 }
244 }
245
246 insert_index(vocabulary, &mut result, index, active_context.as_ref(), active_property, options);
247
248 if let Some(graph_entry) = node.graph_entry() {
249 compact_property(
250 vocabulary,
251 &mut result,
252 Term::Keyword(Keyword::Graph),
253 graph_entry.iter(),
254 active_context.as_ref(),
255 loader,
256 false,
257 options,
258 )
259 .await?;
260 }
261
262 for (expanded_property, expanded_value) in expanded_entries {
263 compact_property(
264 vocabulary,
265 &mut result,
266 expanded_property.clone().into(),
267 expanded_value.iter(),
268 active_context.as_ref(),
269 loader,
270 false,
271 options,
272 )
273 .await?;
274 }
275
276 if let Some(included_entry) = node.included_entry() {
277 compact_property(
278 vocabulary,
279 &mut result,
280 Term::Keyword(Keyword::Included),
281 included_entry.iter(),
282 active_context.as_ref(),
283 loader,
284 false,
285 options,
286 )
287 .await?;
288 }
289
290 Ok(result.into())
291}
292
293pub fn compact_node_reference<N>(
321 vocabulary: &N,
322 id: &Id<N::Iri, N::BlankId>,
323 index: Option<&str>,
324 active_context: &Context<N::Iri, N::BlankId>,
325 active_property: Option<&str>,
326 options: Options,
327) -> Result<jstrict::Value, IriConfusedWithPrefix>
328where
329 N: Vocabulary,
330 N::Iri: Clone + Hash + Eq,
331 N::BlankId: Clone + Hash + Eq,
332{
333 let type_mapping = match active_property {
346 Some(prop) => match active_context.get(prop) {
347 Some(def) => def.typ(),
348 None => None,
349 },
350 None => None,
351 };
352
353 if type_mapping == Some(&Type::Id) {
354 let compacted_value = compact_iri(vocabulary, active_context, &id.clone().into_term(), false, false, options)?;
355 return Ok(optional_string(compacted_value.as_deref()));
356 }
357
358 if type_mapping == Some(&Type::Vocab) {
361 let compacted_value = compact_iri(vocabulary, active_context, &id.clone().into_term(), true, false, options)?;
362 return Ok(optional_string(compacted_value.as_deref()));
363 }
364
365 let mut result = jstrict::Object::default();
366 insert_id(vocabulary, &mut result, id, active_context, options)?;
367 insert_index(vocabulary, &mut result, index, active_context, active_property, options);
368 Ok(result.into())
369}
370
371fn insert_id<N>(
374 vocabulary: &N,
375 result: &mut jstrict::Object,
376 id: &Id<N::Iri, N::BlankId>,
377 active_context: &Context<N::Iri, N::BlankId>,
378 options: Options,
379) -> Result<(), IriConfusedWithPrefix>
380where
381 N: Vocabulary,
382 N::Iri: Clone + Hash + Eq,
383 N::BlankId: Clone + Hash + Eq,
384{
385 let compacted_value = compact_iri(vocabulary, active_context, &id.clone().into_term(), false, false, options)?;
388
389 let alias = keyword_alias(vocabulary, active_context, options, Keyword::Id);
391 result.insert(alias.into(), optional_string(compacted_value.as_deref()));
392 Ok(())
393}
394
395fn insert_index<N>(
398 vocabulary: &N,
399 result: &mut jstrict::Object,
400 index: Option<&str>,
401 active_context: &Context<N::Iri, N::BlankId>,
402 active_property: Option<&str>,
403 options: Options,
404) where
405 N: Vocabulary,
406 N::Iri: Clone + Hash + Eq,
407 N::BlankId: Clone + Hash + Eq,
408{
409 if let Some(index_entry) = index {
412 let mut index_container = false;
413 if let Some(active_property) = active_property
414 && let Some(active_property_definition) = active_context.get(active_property)
415 && active_property_definition.container().contains(ContainerKind::Index)
416 {
417 index_container = true;
420 }
421
422 if !index_container {
423 let alias = keyword_alias(vocabulary, active_context, options, Keyword::Index);
425
426 result.insert(alias.into(), index_entry.into());
428 }
429 }
430}
431
432#[must_use]
444pub fn types_as_array<I, B>(active_context: &Context<I, B>, type_alias: &str, options: Options) -> bool {
445 let container_mapping = match active_context.get(type_alias) {
449 Some(def) => def.container(),
450 None => Container::None,
451 };
452 (options.processing_mode == ProcessingMode::JsonLd1_1 && container_mapping.contains(ContainerKind::Set)) || !options.compact_arrays
453}
454
455fn compact_types<N, E>(
463 vocabulary: &mut N,
464 result: &mut jstrict::Object,
465 types: Option<&[Id<N::Iri, N::BlankId>]>,
466 active_context: &Context<N::Iri, N::BlankId>,
467 type_scoped_context: &Context<N::Iri, N::BlankId>,
468 options: Options,
469) -> Result<(), Error<E>>
470where
471 N: VocabularyMut,
472 N::Iri: Clone + Hash + Eq,
473 N::BlankId: Clone + Hash + Eq,
474{
475 if let Some(types) = types
477 && !types.is_empty()
478 {
479 let compacted_value = if types.len() == 1 {
483 let arc = compact_iri(vocabulary, type_scoped_context, &types[0].clone().into_term(), true, false, options)?;
484 optional_string(arc.as_deref())
485 } else {
486 let mut compacted_value = Vec::with_capacity(types.len());
489
490 for ty in types {
492 let ty = ty.clone().into_term();
493
494 let compacted_ty = compact_iri(vocabulary, type_scoped_context, &ty, true, false, options)?;
496
497 compacted_value.push(optional_string(compacted_ty.as_deref()));
499 }
500
501 jstrict::Value::Array(compacted_value.into_iter().collect())
502 };
503
504 let alias = keyword_alias(vocabulary, active_context, options, Keyword::Type);
506
507 let as_array = types_as_array(active_context, alias, options);
508
509 add_value(result, alias, compacted_value, as_array);
511 }
512
513 Ok(())
514}
515
516#[cfg(test)]
517mod tests {
518 #![allow(clippy::unwrap_used)]
519 use super::*;
520 use crate::{
521 CompactFragment,
522 test_fixtures::{VALUE_CONTEXT, compact_one_value_document, expanded_object, json, processed_context},
523 };
524 use jsonld_core::NoLoader;
525 use rdfx::vocabulary::{no_vocabulary, no_vocabulary_mut};
526
527 #[tokio::test]
531 async fn compact_node_reference_matches_the_compaction_of_a_one_value_document() {
532 let context = processed_context(VALUE_CONTEXT).await;
533
534 let cases = [
536 (
537 "https://example.com/object",
538 r#"{ "@id": "https://example.com/Building" }"#,
539 "object",
540 r#""ex:Building""#,
541 ),
542 (
543 "https://example.com/objectVocab",
544 r#"{ "@id": "https://example.com/Building" }"#,
545 "objectVocab",
546 r#""Building""#,
547 ),
548 (
549 "https://example.com/objectVocab",
550 r#"{ "@id": "https://vocab.example/Thing" }"#,
551 "objectVocab",
552 r#""Thing""#,
553 ),
554 (
555 "https://example.com/text",
556 r#"{ "@id": "urn:ngsi-ld:Thing:1" }"#,
557 "text",
558 r#"{ "id": "urn:ngsi-ld:Thing:1" }"#,
559 ),
560 ("https://example.com/text", r#"{ "@id": "_:b0" }"#, "text", r#"{ "id": "_:b0" }"#),
561 ("https://example.com/object", r#"{ "@id": "urn:x", "@index": "i" }"#, "object", r#""urn:x""#),
562 (
563 "https://example.com/text",
564 r#"{ "@id": "urn:x", "@index": "i" }"#,
565 "text",
566 r#"{ "id": "urn:x", "@index": "i" }"#,
567 ),
568 ];
569
570 for (property, reference, expected_key, expected) in cases {
571 let (key, written) = compact_one_value_document(&context, property, reference).await;
572 assert_eq!(key, expected_key, "key of {reference}");
573
574 let object = expanded_object(reference);
575 let compacted = compact_node_reference(no_vocabulary(), object.id().unwrap(), object.index(), &context, Some(&key), Options::default()).unwrap();
576 assert_eq!(compacted, json(expected), "compaction of {reference} under {key}");
577 assert_eq!(compacted, written, "{reference} compacted alone and in a document");
578 }
579 }
580
581 #[tokio::test]
584 async fn types_as_array_follows_the_container_of_the_type_alias() {
585 let node = expanded_object(r#"{ "@id": "urn:x", "@type": ["https://example.com/Building"] }"#);
586 let cases = [
587 (
588 r#"{ "type": "@type", "ex": "https://example.com/" }"#,
589 false,
590 r#"{ "@id": "urn:x", "type": "ex:Building" }"#,
591 ),
592 (
593 r#"{ "type": { "@id": "@type", "@container": "@set" }, "ex": "https://example.com/" }"#,
594 true,
595 r#"{ "@id": "urn:x", "type": ["ex:Building"] }"#,
596 ),
597 ];
598
599 for (source, as_array, expected) in cases {
600 let context = processed_context(source).await;
601 let alias = keyword_alias(no_vocabulary(), &context, Options::default(), Keyword::Type);
602 assert_eq!(types_as_array(&context, alias, Options::default()), as_array, "{source}");
603
604 let compacted = node
605 .compact_fragment_full(no_vocabulary_mut(), &context, &context, None, &NoLoader, Options::default())
606 .await
607 .unwrap();
608 assert_eq!(compacted, json(expected), "{source}");
609 }
610 }
611}