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, WarningHandler};
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, W>(
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 warnings: &mut W,
41) -> Result<jstrict::Value, Error<L::Error>>
42where
43 N: VocabularyMut,
44 N::Iri: Clone + Hash + Eq,
45 N::BlankId: Clone + Hash + Eq,
46 L: Loader,
47 W: WarningHandler<N>,
48{
49 if !(node.is_empty() && node.id.is_some()) {
54 if let Some(previous_context) = active_context.previous_context() {
56 active_context = previous_context;
57 }
58 }
59
60 let mut active_context = ContextRef::Borrowed(active_context);
68 if let Some(active_property) = active_property
69 && let Some(active_property_definition) = type_scoped_context.get(active_property)
70 && let Some(local_context) = active_property_definition.context()
71 {
72 active_context = ContextRef::owned(
73 local_context
74 .process_full(
75 vocabulary,
76 active_context.as_ref(),
77 loader,
78 active_property_definition.base_url().cloned(),
79 ProcessingOptions::from(options).with_override(),
80 &mut *warnings,
81 )
82 .await?
83 .into_processed(),
84 );
85 }
86
87 if node.is_empty()
88 && let Some(id) = &node.id
89 {
90 return Ok(compact_node_reference(
91 vocabulary,
92 id,
93 index,
94 active_context.as_ref(),
95 active_property,
96 options,
97 )?);
98 }
99
100 let mut result = jstrict::Object::default();
101
102 if !node.types().is_empty() {
103 let mut compacted_types = Vec::new();
108 for ty in node.types() {
109 let compacted_ty = compact_iri(vocabulary, type_scoped_context, &ty.clone().into_term(), true, false, options)?;
110 compacted_types.push(compacted_ty);
111 }
112
113 compacted_types.sort_by(|a, b| a.as_deref().cmp(&b.as_deref()));
114
115 for term in compacted_types.iter().flatten() {
116 if let Some(term_definition) = type_scoped_context.get(&**term)
117 && let Some(local_context) = term_definition.context()
118 {
119 let processing_options = ProcessingOptions::from(options).without_propagation();
120 active_context = ContextRef::owned(
121 local_context
122 .process_full(
123 vocabulary,
124 active_context.as_ref(),
125 loader,
126 term_definition.base_url().cloned(),
127 processing_options,
128 &mut *warnings,
129 )
130 .await?
131 .into_processed(),
132 );
133 }
134 }
135 }
136
137 let expanded_entries: Vec<_> = if options.ordered {
140 let vocabulary: &N = vocabulary;
146 let mut decorated: Vec<(&str, _)> = node.properties().iter().map(|entry| (entry.0.with(vocabulary).as_str(), entry)).collect();
147 decorated.sort_by(|a, b| a.0.cmp(b.0));
148 decorated.into_iter().map(|(_, entry)| entry).collect()
149 } else {
150 node.properties().iter().collect()
151 };
152
153 if let Some(id_entry) = &node.id {
155 insert_id(vocabulary, &mut result, id_entry, active_context.as_ref(), options)?;
156 }
157
158 compact_types(
159 vocabulary,
160 &mut result,
161 node.types.as_deref(),
162 active_context.as_ref(),
163 type_scoped_context,
164 options,
165 )?;
166
167 if let Some(reverse_properties) = node.reverse_properties_entry()
169 && !reverse_properties.is_empty()
170 {
171 let active_property = "@reverse";
175 if let Some(active_property_definition) = active_context.get(active_property)
176 && let Some(local_context) = active_property_definition.context()
177 {
178 active_context = ContextRef::owned(
179 local_context
180 .process_full(
181 vocabulary,
182 active_context.as_ref(),
183 loader,
184 active_property_definition.base_url().cloned(),
185 ProcessingOptions::from(options).with_override(),
186 &mut *warnings,
187 )
188 .await?
189 .into_processed(),
190 );
191 }
192
193 let mut reverse_result = jstrict::Object::default();
205 for (expanded_property, expanded_value) in reverse_properties {
206 compact_property(
207 vocabulary,
208 &mut reverse_result,
209 expanded_property.clone().into(),
210 expanded_value.iter(),
211 active_context.as_ref(),
212 loader,
213 true,
214 options,
215 warnings,
216 )
217 .await?;
218 }
219
220 let mut reverse_map = jstrict::Object::default();
222 for (property, mapped_value) in &mut reverse_result {
223 let mut value = jstrict::Value::Null;
224 std::mem::swap(&mut value, &mut *mapped_value);
225
226 if let Some(term_definition) = active_context.get(property.as_str())
229 && term_definition.reverse_property()
230 {
231 let as_array = term_definition.container().contains(ContainerKind::Set) || !options.compact_arrays;
234
235 add_value(&mut result, property, value, as_array);
237 continue;
238 }
239
240 reverse_map.insert(property.clone(), value);
241 }
242
243 if !reverse_map.is_empty() {
244 let alias = keyword_alias(vocabulary, active_context.as_ref(), options, Keyword::Reverse);
246
247 result.insert(alias.into(), reverse_map.into());
249 }
250 }
251
252 insert_index(vocabulary, &mut result, index, active_context.as_ref(), active_property, options);
253
254 if let Some(graph_entry) = node.graph_entry() {
255 compact_property(
256 vocabulary,
257 &mut result,
258 Term::Keyword(Keyword::Graph),
259 graph_entry.iter(),
260 active_context.as_ref(),
261 loader,
262 false,
263 options,
264 warnings,
265 )
266 .await?;
267 }
268
269 for (expanded_property, expanded_value) in expanded_entries {
270 compact_property(
271 vocabulary,
272 &mut result,
273 expanded_property.clone().into(),
274 expanded_value.iter(),
275 active_context.as_ref(),
276 loader,
277 false,
278 options,
279 warnings,
280 )
281 .await?;
282 }
283
284 if let Some(included_entry) = node.included_entry() {
285 compact_property(
286 vocabulary,
287 &mut result,
288 Term::Keyword(Keyword::Included),
289 included_entry.iter(),
290 active_context.as_ref(),
291 loader,
292 false,
293 options,
294 warnings,
295 )
296 .await?;
297 }
298
299 Ok(result.into())
300}
301
302pub fn compact_node_reference<N>(
330 vocabulary: &N,
331 id: &Id<N::Iri, N::BlankId>,
332 index: Option<&str>,
333 active_context: &Context<N::Iri, N::BlankId>,
334 active_property: Option<&str>,
335 options: Options,
336) -> Result<jstrict::Value, IriConfusedWithPrefix>
337where
338 N: Vocabulary,
339 N::Iri: Clone + Hash + Eq,
340 N::BlankId: Clone + Hash + Eq,
341{
342 let type_mapping = match active_property {
355 Some(prop) => match active_context.get(prop) {
356 Some(def) => def.typ(),
357 None => None,
358 },
359 None => None,
360 };
361
362 if type_mapping == Some(&Type::Id) {
363 let compacted_value = compact_iri(vocabulary, active_context, &id.clone().into_term(), false, false, options)?;
364 return Ok(optional_string(compacted_value.as_deref()));
365 }
366
367 if type_mapping == Some(&Type::Vocab) {
370 let compacted_value = compact_iri(vocabulary, active_context, &id.clone().into_term(), true, false, options)?;
371 return Ok(optional_string(compacted_value.as_deref()));
372 }
373
374 let mut result = jstrict::Object::default();
375 insert_id(vocabulary, &mut result, id, active_context, options)?;
376 insert_index(vocabulary, &mut result, index, active_context, active_property, options);
377 Ok(result.into())
378}
379
380fn insert_id<N>(
383 vocabulary: &N,
384 result: &mut jstrict::Object,
385 id: &Id<N::Iri, N::BlankId>,
386 active_context: &Context<N::Iri, N::BlankId>,
387 options: Options,
388) -> Result<(), IriConfusedWithPrefix>
389where
390 N: Vocabulary,
391 N::Iri: Clone + Hash + Eq,
392 N::BlankId: Clone + Hash + Eq,
393{
394 let compacted_value = compact_iri(vocabulary, active_context, &id.clone().into_term(), false, false, options)?;
397
398 let alias = keyword_alias(vocabulary, active_context, options, Keyword::Id);
400 result.insert(alias.into(), optional_string(compacted_value.as_deref()));
401 Ok(())
402}
403
404fn insert_index<N>(
407 vocabulary: &N,
408 result: &mut jstrict::Object,
409 index: Option<&str>,
410 active_context: &Context<N::Iri, N::BlankId>,
411 active_property: Option<&str>,
412 options: Options,
413) where
414 N: Vocabulary,
415 N::Iri: Clone + Hash + Eq,
416 N::BlankId: Clone + Hash + Eq,
417{
418 if let Some(index_entry) = index {
421 let mut index_container = false;
422 if let Some(active_property) = active_property
423 && let Some(active_property_definition) = active_context.get(active_property)
424 && active_property_definition.container().contains(ContainerKind::Index)
425 {
426 index_container = true;
429 }
430
431 if !index_container {
432 let alias = keyword_alias(vocabulary, active_context, options, Keyword::Index);
434
435 result.insert(alias.into(), index_entry.into());
437 }
438 }
439}
440
441#[must_use]
453pub fn types_as_array<I, B>(active_context: &Context<I, B>, type_alias: &str, options: Options) -> bool {
454 let container_mapping = match active_context.get(type_alias) {
458 Some(def) => def.container(),
459 None => Container::None,
460 };
461 (options.processing_mode == ProcessingMode::JsonLd1_1 && container_mapping.contains(ContainerKind::Set)) || !options.compact_arrays
462}
463
464fn compact_types<N, E>(
472 vocabulary: &mut N,
473 result: &mut jstrict::Object,
474 types: Option<&[Id<N::Iri, N::BlankId>]>,
475 active_context: &Context<N::Iri, N::BlankId>,
476 type_scoped_context: &Context<N::Iri, N::BlankId>,
477 options: Options,
478) -> Result<(), Error<E>>
479where
480 N: VocabularyMut,
481 N::Iri: Clone + Hash + Eq,
482 N::BlankId: Clone + Hash + Eq,
483{
484 if let Some(types) = types
486 && !types.is_empty()
487 {
488 let compacted_value = if types.len() == 1 {
492 let arc = compact_iri(vocabulary, type_scoped_context, &types[0].clone().into_term(), true, false, options)?;
493 optional_string(arc.as_deref())
494 } else {
495 let mut compacted_value = Vec::with_capacity(types.len());
498
499 for ty in types {
501 let ty = ty.clone().into_term();
502
503 let compacted_ty = compact_iri(vocabulary, type_scoped_context, &ty, true, false, options)?;
505
506 compacted_value.push(optional_string(compacted_ty.as_deref()));
508 }
509
510 jstrict::Value::Array(compacted_value.into_iter().collect())
511 };
512
513 let alias = keyword_alias(vocabulary, active_context, options, Keyword::Type);
515
516 let as_array = types_as_array(active_context, alias, options);
517
518 add_value(result, alias, compacted_value, as_array);
520 }
521
522 Ok(())
523}
524
525#[cfg(test)]
526mod tests {
527 #![allow(clippy::unwrap_used)]
528 use super::*;
529 use crate::{
530 CompactFragment,
531 test_fixtures::{VALUE_CONTEXT, compact_one_value_document, expanded_object, json, processed_context},
532 };
533 use jsonld_core::NoLoader;
534 use rdfx::vocabulary::{no_vocabulary, no_vocabulary_mut};
535
536 #[tokio::test]
540 async fn compact_node_reference_matches_the_compaction_of_a_one_value_document() {
541 let context = processed_context(VALUE_CONTEXT).await;
542
543 let cases = [
545 (
546 "https://example.com/object",
547 r#"{ "@id": "https://example.com/Building" }"#,
548 "object",
549 r#""ex:Building""#,
550 ),
551 (
552 "https://example.com/objectVocab",
553 r#"{ "@id": "https://example.com/Building" }"#,
554 "objectVocab",
555 r#""Building""#,
556 ),
557 (
558 "https://example.com/objectVocab",
559 r#"{ "@id": "https://vocab.example/Thing" }"#,
560 "objectVocab",
561 r#""Thing""#,
562 ),
563 (
564 "https://example.com/text",
565 r#"{ "@id": "urn:ngsi-ld:Thing:1" }"#,
566 "text",
567 r#"{ "id": "urn:ngsi-ld:Thing:1" }"#,
568 ),
569 ("https://example.com/text", r#"{ "@id": "_:b0" }"#, "text", r#"{ "id": "_:b0" }"#),
570 ("https://example.com/object", r#"{ "@id": "urn:x", "@index": "i" }"#, "object", r#""urn:x""#),
571 (
572 "https://example.com/text",
573 r#"{ "@id": "urn:x", "@index": "i" }"#,
574 "text",
575 r#"{ "id": "urn:x", "@index": "i" }"#,
576 ),
577 ];
578
579 for (property, reference, expected_key, expected) in cases {
580 let (key, written) = compact_one_value_document(&context, property, reference).await;
581 assert_eq!(key, expected_key, "key of {reference}");
582
583 let object = expanded_object(reference);
584 let compacted = compact_node_reference(no_vocabulary(), object.id().unwrap(), object.index(), &context, Some(&key), Options::default()).unwrap();
585 assert_eq!(compacted, json(expected), "compaction of {reference} under {key}");
586 assert_eq!(compacted, written, "{reference} compacted alone and in a document");
587 }
588 }
589
590 #[tokio::test]
593 async fn types_as_array_follows_the_container_of_the_type_alias() {
594 let node = expanded_object(r#"{ "@id": "urn:x", "@type": ["https://example.com/Building"] }"#);
595 let cases = [
596 (
597 r#"{ "type": "@type", "ex": "https://example.com/" }"#,
598 false,
599 r#"{ "@id": "urn:x", "type": "ex:Building" }"#,
600 ),
601 (
602 r#"{ "type": { "@id": "@type", "@container": "@set" }, "ex": "https://example.com/" }"#,
603 true,
604 r#"{ "@id": "urn:x", "type": ["ex:Building"] }"#,
605 ),
606 ];
607
608 for (source, as_array, expected) in cases {
609 let context = processed_context(source).await;
610 let alias = keyword_alias(no_vocabulary(), &context, Options::default(), Keyword::Type);
611 assert_eq!(types_as_array(&context, alias, Options::default()), as_array, "{source}");
612
613 let compacted = node
614 .compact_fragment_full(no_vocabulary_mut(), &context, &context, None, &NoLoader, Options::default(), &mut ())
615 .await
616 .unwrap();
617 assert_eq!(compacted, json(expected), "{source}");
618 }
619 }
620}