jsonld_compaction/property.rs
1use crate::{
2 CompactFragment,
3 CompactIndexedFragment,
4 Error,
5 Options,
6 add_value,
7 compact_collection_with,
8 compact_iri,
9 compact_iri_with,
10 compact_iri_with_memo,
11 iri::{CompactIriMemo, keyword_alias},
12 value_value,
13};
14
15use jsonld_context_processing::WarningHandler;
16use jsonld_core::{
17 Container,
18 ContainerKind,
19 Context,
20 Id,
21 Indexed,
22 Loader,
23 Node,
24 Object,
25 Term,
26 context::Nest,
27 object::{self, List},
28};
29use jsonld_syntax::{Keyword, native_stack::Recursion};
30use rdfx::vocabulary::VocabularyMut;
31use std::hash::Hash;
32
33/// Compacts a list object and adds it to `nest_result` under
34/// `item_active_property`.
35///
36/// When the term's container mapping includes `@list` the list becomes a plain
37/// array assigned directly to the key. Otherwise it is wrapped back into a list
38/// object — a map holding whatever `@list` compacts to, plus `@index` when the
39/// list carried one — and added with [`add_value`].
40async fn compact_property_list<N, L, W>(
41 vocabulary: &mut N,
42 list: &List<N::Iri, N::BlankId>,
43 expanded_index: Option<&str>,
44 nest_result: &mut jstrict::Object,
45 container: Container,
46 as_array: bool,
47 item_active_property: &str,
48 active_context: &Context<N::Iri, N::BlankId>,
49 loader: &L,
50 options: Options,
51 warnings: &mut W,
52) -> Result<(), Error<L::Error>>
53where
54 N: VocabularyMut,
55 N::Iri: Clone + Hash + Eq,
56 N::BlankId: Clone + Hash + Eq,
57 L: Loader,
58 W: WarningHandler<N>,
59{
60 // If expanded item is a list object:
61 let mut compacted_item = Recursion::new(compact_collection_with(
62 vocabulary,
63 list.iter(),
64 active_context,
65 active_context,
66 Some(item_active_property),
67 loader,
68 options,
69 warnings,
70 ))
71 .await?;
72
73 // If compacted item is not an array,
74 // then set `compacted_item` to an array containing only `compacted_item`.
75 if !compacted_item.is_array() {
76 let array = vec![compacted_item];
77 compacted_item = jstrict::Value::Array(array.into());
78 }
79
80 // If container does not include @list:
81 if container.contains(ContainerKind::List) {
82 // Otherwise, set the value of the item active property entry in nest result to compacted item.
83 nest_result.insert(item_active_property.into(), compacted_item);
84 } else {
85 // Convert `compacted_item` to a list object by setting it to
86 // a map containing an entry where the key is the result of
87 // IRI compacting @list and the value is the original
88 // compacted item.
89 let key = keyword_alias(vocabulary, active_context, options, Keyword::List);
90 let mut compacted_item_list_object = jstrict::Object::default();
91 compacted_item_list_object.insert(key.into(), compacted_item);
92
93 // If `expanded_item` contains the entry @index-value,
94 // then add an entry to compacted item where the key is
95 // the result of IRI compacting @index and value is value.
96 if let Some(index) = expanded_index {
97 let key = keyword_alias(vocabulary, active_context, options, Keyword::Index);
98
99 compacted_item_list_object.insert(key.into(), jstrict::Value::String(index.into()));
100 }
101
102 compacted_item = jstrict::Value::Object(compacted_item_list_object);
103
104 // Use add value to add `compacted_item` to
105 // the `item_active_property` entry in `nest_result` using `as_array`.
106 add_value(nest_result, item_active_property, compacted_item, as_array);
107 }
108
109 Ok(())
110}
111
112/// Compacts a graph object and adds it to `nest_result` under
113/// `item_active_property`.
114///
115/// The shape depends on the term's container mapping: `@graph`+`@id` and
116/// `@graph`+`@index` turn the key into a map keyed by the graph's `@id` or
117/// `@index`; a plain `@graph` container drops the wrapper entirely (collapsing
118/// several graph entries into `@included` when they cannot be told apart);
119/// anything else rebuilds an explicit graph object with `@graph`, `@id` and
120/// `@index` entries.
121///
122/// Returns [`Error::CollidingEntry`] when a map-shaped container has to be
123/// written where an earlier item of the same property already stored a non-map
124/// value — a mix the specification defines no output for.
125async fn compact_property_graph<N, L, W>(
126 vocabulary: &mut N,
127 node: &Node<N::Iri, N::BlankId>,
128 expanded_index: Option<&str>,
129 nest_result: &mut jstrict::Object,
130 container: Container,
131 as_array: bool,
132 item_active_property: &str,
133 active_context: &Context<N::Iri, N::BlankId>,
134 loader: &L,
135 options: Options,
136 warnings: &mut W,
137) -> Result<(), Error<L::Error>>
138where
139 N: VocabularyMut,
140 N::Iri: Clone + Hash + Eq,
141 N::BlankId: Clone + Hash + Eq,
142 L: Loader,
143 W: WarningHandler<N>,
144{
145 // If expanded item is a graph object.
146 // `compact_property_graph` is only invoked when `node.is_graph()`, so
147 // this never actually returns early.
148 let Some(graph) = node.graph() else { return Ok(()) };
149 let mut compacted_item = Recursion::new(graph.compact_fragment_full(
150 vocabulary,
151 active_context,
152 active_context,
153 Some(item_active_property),
154 loader,
155 options,
156 warnings,
157 ))
158 .await?;
159
160 // If `container` includes @graph and @id:
161 if container.contains(ContainerKind::Graph) && container.contains(ContainerKind::Id) {
162 // Initialize `map_object` to the value of `item_active_property`
163 // in `nest_result`, initializing it to a new empty map,
164 // if necessary.
165 if nest_result.get_unique(item_active_property).is_ok_and(|o| o.is_none()) {
166 nest_result.insert(item_active_property.into(), jsonld_syntax::Object::default().into());
167 }
168
169 // The entry exists (just inserted above if absent), but may hold a
170 // non-map value if an earlier expanded item was routed through the
171 // fall-through branch below (e.g. a named graph mixed with simple
172 // graphs under the same property); the spec defines no output for
173 // that state.
174 let Some(map_object) = nest_result
175 .get_unique_mut(item_active_property)
176 .ok()
177 .flatten()
178 .and_then(jstrict::Value::as_object_mut)
179 else {
180 return Err(Error::CollidingEntry);
181 };
182
183 // Initialize `map_key` by IRI compacting the value of @id in
184 // `expanded_item` or @none if no such value exists
185 // with `vocab` set to false if there is an @id entry in
186 // `expanded_item`.
187 let (id_value, vocab) = match &node.id {
188 Some(entry) => (entry.clone().into_term(), false),
189 None => (Term::Keyword(Keyword::None), true),
190 };
191
192 let Some(map_key) = compact_iri(vocabulary, active_context, &id_value, vocab, false, options)? else {
193 return Err(Error::IriConfusedWithPrefix);
194 };
195
196 // Use `add_value` to add `compacted_item` to
197 // the `map_key` entry in `map_object` using `as_array`.
198 add_value(map_object, &map_key, compacted_item, as_array);
199 } else if container.contains(ContainerKind::Graph) && container.contains(ContainerKind::Index) && node.is_simple_graph() {
200 // Initialize `map_object` to the value of `item_active_property`
201 // in `nest_result`, initializing it to a new empty map,
202 // if necessary.
203 if nest_result.get_unique(item_active_property).is_ok_and(|o| o.is_none()) {
204 nest_result.insert(item_active_property.into(), jsonld_syntax::Object::default().into());
205 }
206
207 // Same collision case as the `@graph`+`@id` branch above: an earlier
208 // named graph routed through the fall-through branch may have stored
209 // a non-map value under this key.
210 let Some(map_object) = nest_result
211 .get_unique_mut(item_active_property)
212 .ok()
213 .flatten()
214 .and_then(jstrict::Value::as_object_mut)
215 else {
216 return Err(Error::CollidingEntry);
217 };
218
219 // Initialize `map_key` the value of @index in `expanded_item`
220 // or @none, if no such value exists.
221 let map_key = expanded_index.unwrap_or("@none");
222
223 // Use `add_value` to add `compacted_item` to
224 // the `map_key` entry in `map_object` using `as_array`.
225 add_value(map_object, map_key, compacted_item, as_array);
226 } else if container.contains(ContainerKind::Graph) && node.is_simple_graph() {
227 // Otherwise, if `container` includes @graph and
228 // `expanded_item` is a simple graph object
229 // the value cannot be represented as a map object.
230
231 // If `compacted_item` is an array with more than one value,
232 // it cannot be directly represented,
233 // as multiple objects would be interpreted as different named graphs.
234 // Set `compacted_item` to a new map,
235 // containing the key from IRI compacting @included and
236 // the original `compacted_item` as the value.
237 compacted_item = match compacted_item {
238 jstrict::Value::Array(items) if items.len() > 1 => {
239 let key = keyword_alias(vocabulary, active_context, options, Keyword::Included);
240 let mut map = jstrict::Object::default();
241 map.insert(key.into(), jstrict::Value::Array(items));
242 jstrict::Value::Object(map)
243 }
244 item => item,
245 };
246
247 // Use `add_value` to add `compacted_item` to the
248 // `item_active_property` entry in `nest_result` using `as_array`.
249 add_value(nest_result, item_active_property, compacted_item, as_array);
250 } else {
251 // Otherwise, `container` does not include @graph or
252 // otherwise does not match one of the previous cases.
253
254 // Set `compacted_item` to a new map containing the key from
255 // IRI compacting @graph using the original `compacted_item` as a value.
256 let key = keyword_alias(vocabulary, active_context, options, Keyword::Graph);
257 let mut map = jstrict::Object::default();
258 map.insert(key.into(), compacted_item);
259
260 // If `expanded_item` contains an @id entry,
261 // add an entry in `compacted_item` using the key from
262 // IRI compacting @id using the value of
263 // IRI compacting the value of @id in `expanded_item` using
264 // false for vocab.
265 if let Some(id_entry) = &node.id {
266 // The key is the literal `"@id"` rather than a `keyword_alias`
267 // lookup: this step compacts `@id` with `vocab` set to false, which
268 // bypasses the inverse context and so can only ever return `"@id"`.
269 let id: Term<N::Iri, N::BlankId> = id_entry.clone().into();
270 let value = compact_iri(vocabulary, active_context, &id, false, false, options)?;
271 map.insert(
272 Keyword::Id.into_str().into(),
273 match value {
274 Some(s) => jstrict::Value::String((&*s).into()),
275 None => jstrict::Value::Null,
276 },
277 );
278 }
279
280 // If `expanded_item` contains an @index entry,
281 // add an entry in `compacted_item` using the key from
282 // IRI compacting @index and the value of @index in `expanded_item`.
283 if let Some(index_entry) = expanded_index {
284 let key = keyword_alias(vocabulary, active_context, options, Keyword::Index);
285 map.insert(key.into(), index_entry.into());
286 }
287
288 // Use `add_value` to add `compacted_item` to the
289 // `item_active_property` entry in `nest_result` using `as_array`.
290 let compacted_item = jstrict::Value::Object(map);
291 add_value(nest_result, item_active_property, compacted_item, as_array);
292 }
293
294 Ok(())
295}
296
297/// Picks the object a compacted property should be written into, and returns it
298/// together with the term's container mapping and whether its values must be
299/// wrapped in an array.
300///
301/// A term with a `@nest` entry is written into a nested sub-object of `result`
302/// keyed by the nest term, created on demand; every other term is written into
303/// `result` itself. `as_array` is true when the container mapping includes
304/// `@set`, when the key is `@graph` or `@list`, or when `compact_arrays` is off.
305///
306/// Returns [`Error::InvalidNestValue`] if `@nest` names something that is not
307/// `@nest` or an alias for it, and [`Error::CollidingEntry`] if the nest key
308/// already holds a non-map value.
309fn select_nest_result<'a, I, B, E>(
310 result: &'a mut jstrict::Object,
311 active_context: &Context<I, B>,
312 item_active_property: &str,
313 compact_arrays: bool,
314) -> Result<(&'a mut jstrict::Object, Container, bool), Error<E>>
315where
316 I: Clone + Hash + Eq,
317 B: Clone + Hash + Eq,
318{
319 let (nest_result, container) = match active_context.get(item_active_property) {
320 Some(term_definition) => {
321 let nest_result = match term_definition.nest() {
322 Some(nest_term) => {
323 // If nest term is not @nest,
324 // or a term in the active context that expands to @nest,
325 // an invalid @nest value error has been detected,
326 // and processing is aborted.
327 if *nest_term != Nest::Nest {
328 match active_context.get(nest_term.as_str()) {
329 Some(term_def) if term_def.value() == Some(&Term::Keyword(Keyword::Nest)) => {}
330 _ => return Err(Error::InvalidNestValue),
331 }
332 }
333
334 // If result does not have a nest_term entry,
335 // initialize it to an empty map.
336 if result.get_unique(nest_term.as_str()).is_ok_and(|o| o.is_none()) {
337 result.insert(nest_term.as_str().into(), jstrict::Object::default().into());
338 }
339
340 // Initialize `nest_result` to the value of `nest_term` in result.
341 // The entry exists (just inserted above if absent), but may hold
342 // a non-map value if another property already compacted to the
343 // same key; the spec defines no output for that state.
344 match result.get_unique_mut(nest_term.as_str()).ok().flatten().and_then(jstrict::Value::as_object_mut) {
345 Some(object) => object,
346 None => return Err(Error::CollidingEntry),
347 }
348 }
349 None => {
350 // Otherwise, initialize `nest_result` to result.
351 result
352 }
353 };
354
355 (nest_result, term_definition.container())
356 }
357 None => (result, Container::None),
358 };
359
360 // Initialize container to container mapping for item active property
361 // in active context, or to a new empty array,
362 // if there is no such container mapping.
363 // DONE.
364
365 let as_array = values_as_array(container, item_active_property, compact_arrays);
366
367 Ok((nest_result, container, as_array))
368}
369
370/// Checks whether the values of a property are written as an array even when
371/// there is only one.
372///
373/// `item_active_property` is the term the property compacted to and
374/// `container` that term's container mapping in the active context, or
375/// [`Container::None`] when the term has no definition. Follows the
376/// *as array* rule the [compaction algorithm][1] applies to each value of a
377/// property: `true` when the container mapping
378/// includes `@set`, when the term is `@graph` or `@list`, or when
379/// `compact_arrays` is off.
380///
381/// A term whose container mapping includes `@list` writes a list object's
382/// items as an array regardless, and terms with an `@language`, `@index`,
383/// `@id` or `@type` container write a map whose entries this flag applies to.
384///
385/// [1]: https://www.w3.org/TR/json-ld-api/#compaction-algorithm
386#[must_use]
387pub fn values_as_array(container: Container, item_active_property: &str, compact_arrays: bool) -> bool {
388 // Initialize `as_array` to true if `container` includes @set,
389 // or if `item_active_property` is @graph or @list,
390 // otherwise the negation of `options.compact_arrays`.
391 if container.contains(ContainerKind::Set) || item_active_property == "@graph" || item_active_property == "@list" {
392 true
393 } else {
394 !compact_arrays
395 }
396}
397
398/// Compacts one expanded property and all of its values into `result`.
399///
400/// Each value is compacted independently, and may select a different term for
401/// the same expanded property — a language-tagged string and a node reference
402/// can end up under different keys. Values are then routed by the selected
403/// term's container mapping: list and graph objects to
404/// [`compact_property_list`] and [`compact_property_graph`], and `@language`,
405/// `@index`, `@id` and `@type` containers into a map keyed by the corresponding
406/// part of the value.
407///
408/// `inside_reverse` marks the property as coming from an `@reverse` entry, which
409/// restricts term selection to terms declared with `@reverse`.
410///
411/// An empty value array still produces an entry: the property is compacted
412/// against an empty node object and assigned an empty array.
413pub async fn compact_property<'a, N, L, O, T, W>(
414 vocabulary: &mut N,
415 result: &mut jstrict::Object,
416 expanded_property: Term<N::Iri, N::BlankId>,
417 expanded_value: O,
418 active_context: &Context<N::Iri, N::BlankId>,
419 loader: &L,
420 inside_reverse: bool,
421 options: Options,
422 warnings: &mut W,
423) -> Result<(), Error<L::Error>>
424where
425 N: VocabularyMut,
426 N::Iri: Clone + Hash + Eq,
427 N::BlankId: Clone + Hash + Eq,
428 O: IntoIterator<Item = &'a Indexed<T>>,
429 T: 'a + object::Any<N::Iri, N::BlankId>,
430 L: Loader,
431 W: WarningHandler<N>,
432{
433 let mut is_empty = true;
434
435 // A property's values are near-always alike — an array of value objects, or
436 // of node references — and IRI compaction reads the value only through a
437 // handful of features. Remembering the last one lets a run of them share a
438 // single term selection. Valid only because every other argument below is
439 // fixed for this call.
440 let mut memo = CompactIriMemo::new();
441
442 // For each item `expanded_item` in `expanded value`
443 for expanded_item in expanded_value {
444 is_empty = false;
445 // Initialize `item_active_property` by IRI compacting `expanded_property`
446 // using `expanded_item` for value and `inside_reverse` for `reverse`.
447 let item_active_property = compact_iri_with_memo(
448 vocabulary,
449 active_context,
450 &expanded_property,
451 expanded_item,
452 true,
453 inside_reverse,
454 options,
455 &mut memo,
456 )?;
457
458 // If the term definition for `item_active_property` in the active context
459 // has a nest value entry (nest term)
460 if let Some(item_active_property) = item_active_property {
461 let (nest_result, container, as_array) = select_nest_result(result, active_context, &item_active_property, options.compact_arrays)?;
462
463 // Initialize `compacted_item` to the result of using this algorithm
464 // recursively, passing `active_context`, `item_active_property` for
465 // `active_property`, `expanded_item` for `element`, along with the
466 // `compact_arrays` and `ordered_flags`.
467 // If `expanded_item` is a list object or a graph object,
468 // use the value of the @list or @graph entries, respectively,
469 // for `element` instead of `expanded_item`.
470 match expanded_item.inner().as_ref() {
471 object::Ref::List(list) => {
472 compact_property_list(
473 vocabulary,
474 list,
475 expanded_item.index(),
476 nest_result,
477 container,
478 as_array,
479 &item_active_property,
480 active_context,
481 loader,
482 options,
483 warnings,
484 )
485 .await?;
486 }
487 object::Ref::Node(node) if node.is_graph() => {
488 compact_property_graph(
489 vocabulary,
490 node,
491 expanded_item.index(),
492 nest_result,
493 container,
494 as_array,
495 &item_active_property,
496 active_context,
497 loader,
498 options,
499 warnings,
500 )
501 .await?;
502 }
503 _ => {
504 let mut compacted_item = Recursion::new(expanded_item.compact_fragment_full(
505 vocabulary,
506 active_context,
507 active_context,
508 Some(&item_active_property),
509 loader,
510 options,
511 warnings,
512 ))
513 .await?;
514
515 // if container includes @language, @index, @id,
516 // or @type and container does not include @graph:
517 if !container.contains(ContainerKind::Graph)
518 && (container.contains(ContainerKind::Language)
519 || container.contains(ContainerKind::Index)
520 || container.contains(ContainerKind::Id)
521 || container.contains(ContainerKind::Type))
522 {
523 // Initialize `map_object` to the value of
524 // `item_active_property` in `nest_result`,
525 // initializing it to a new empty map, if necessary.
526 if nest_result.get_unique(&*item_active_property).is_ok_and(|o| o.is_none()) {
527 nest_result.insert((&*item_active_property).into(), jstrict::Object::default().into());
528 }
529
530 // The entry exists (just inserted above if absent), but may
531 // hold a non-map value if another property already compacted
532 // to the same key with a different container shape; the spec
533 // defines no output for that state.
534 let Some(map_object) = nest_result
535 .get_unique_mut(&*item_active_property)
536 .ok()
537 .flatten()
538 .and_then(jstrict::Value::as_object_mut)
539 else {
540 return Err(Error::CollidingEntry);
541 };
542
543 // Initialize container key by IRI compacting either
544 // @language, @index, @id, or @type based on the contents of container.
545 let container_type = if container.contains(ContainerKind::Language) {
546 ContainerKind::Language
547 } else if container.contains(ContainerKind::Index) {
548 ContainerKind::Index
549 } else if container.contains(ContainerKind::Id) {
550 ContainerKind::Id
551 } else {
552 ContainerKind::Type
553 };
554
555 let mut container_key: String = keyword_alias(vocabulary, active_context, options, container_type.into()).to_string();
556
557 // Initialize `index_key` to the value of index mapping in
558 // the term definition associated with `item_active_property`
559 // in active context, or @index (None in our case), if no such value exists.
560 let index_key = match active_context.get(&*item_active_property) {
561 Some(def) if def.index().is_some() => def.index(),
562 _ => None,
563 };
564
565 // If `container` includes @language and `expanded_item`
566 // contains a @value entry, then set `compacted_item` to
567 // the value associated with its @value entry.
568 // Set `map_key` to the value of @language in `expanded_item`,
569 // if any.
570 let map_key = if container_type == ContainerKind::Language && expanded_item.is_value() {
571 if let object::Ref::Value(value) = expanded_item.inner().as_ref() {
572 compacted_item = value_value(value);
573 }
574
575 expanded_item.language().map(std::string::ToString::to_string)
576 } else if container_type == ContainerKind::Index {
577 match index_key {
578 Some(index_key) => {
579 // Otherwise, if `container` includes @index and
580 // `index_key` is not @index:
581
582 // Reinitialize `container_key` by
583 // IRI compacting `index_key`.
584 container_key =
585 match compact_iri(vocabulary, active_context, &Term::Id(Id::Invalid(index_key.to_string())), true, false, options)? {
586 Some(arc) => arc.to_string(),
587 None => return Err(Error::IriConfusedWithPrefix),
588 };
589
590 // Set `map_key` to the first value of
591 // `container_key` in `compacted_item`, if any.
592 let (map_key, remaining_values) = match &mut compacted_item {
593 jstrict::Value::Object(map) => match map.remove_unique(container_key.as_str()).ok().flatten() {
594 Some(entry) => match entry.value {
595 jstrict::Value::String(s) => (Some(s.to_string()), Vec::new()),
596 jstrict::Value::Array(values) => {
597 let mut values = values.into_iter();
598 match values.next() {
599 Some(first_value) => (first_value.as_str().map(std::string::ToString::to_string), values.collect()),
600 None => (None, values.collect()),
601 }
602 }
603 other => (None, vec![other]),
604 },
605 None => (None, Vec::new()),
606 },
607 _ => (None, Vec::new()),
608 };
609
610 // If there are remaining values in `compacted_item`
611 // for container key, use `add_value` to add
612 // those remaining values to the `container_key`
613 // in `compacted_item`.
614 // Otherwise, remove that entry from compacted item.
615 if !remaining_values.is_empty()
616 && let Some(map) = compacted_item.as_object_mut()
617 {
618 for value in remaining_values {
619 add_value(map, container_key.as_str(), value, false);
620 }
621 }
622
623 map_key
624 }
625 None => {
626 // Otherwise, if `container` includes @index and
627 // `index_key` is @index, set `map_key` to the value of
628 // @index in `expanded_item`, if any.
629 expanded_item.index().map(ToOwned::to_owned)
630 }
631 }
632 } else if container_type == ContainerKind::Id {
633 // Otherwise, if `container` includes @id,
634 // set `map_key` to the value of `container_key` in
635 // `compacted_item` and remove `container_key` from
636 // `compacted_item`.
637 compacted_item
638 .as_object_mut()
639 .and_then(|map| {
640 map.remove_unique(container_key.as_str())
641 .ok()
642 .flatten()
643 .map(|entry| entry.value.as_str().map(ToOwned::to_owned))
644 })
645 .flatten()
646 } else {
647 // Otherwise, if container includes @type:
648
649 // Set `map_key` to the first value of `container_key` in
650 // `compacted_item`, if any.
651 let (map_key, remaining_values) = match compacted_item.as_object_mut() {
652 Some(map) => match map.remove_unique(container_key.as_str()).ok().flatten() {
653 Some(entry) => match entry.value {
654 jstrict::Value::String(s) => (Some((*s).to_string()), Vec::new()),
655 jstrict::Value::Array(values) => {
656 let mut values = values.into_iter();
657 match values.next() {
658 Some(first_value) => (first_value.as_str().map(ToOwned::to_owned), values.collect()),
659 None => (None, values.collect()),
660 }
661 }
662 other => (None, vec![other]),
663 },
664 None => (None, Vec::new()),
665 },
666 _ => (None, Vec::new()),
667 };
668
669 // If there are remaining values in `compacted_item` for
670 // `container_key`, use `add_value` to add those
671 // remaining values to the `container_key` in
672 // `compacted_item`.
673 // Otherwise, remove that entry from compacted item.
674 if !remaining_values.is_empty()
675 && let Some(map) = compacted_item.as_object_mut()
676 {
677 for value in remaining_values {
678 add_value(map, container_key.as_str(), value, false);
679 }
680 }
681
682 // If `compacted_item` contains a single entry with a key
683 // expanding to @id, set `compacted_item` to the result of
684 // using this algorithm recursively,
685 // passing `active_context`, `item_active_property` for
686 // `active_property`, and a map composed of the single
687 // entry for @id from `expanded_item` for `element`.
688 if let Some(map) = compacted_item.as_object()
689 && map.len() == 1
690 && map.get_unique("@id").ok().flatten().is_some()
691 // An `@id`-only map implies the expanded item has an
692 // `id`, so this last condition always holds here.
693 && let Some(id) = expanded_item.id()
694 {
695 let obj = Object::node(Node::with_id(id.clone()));
696 compacted_item = Recursion::new(obj.compact_indexed_fragment(
697 vocabulary,
698 None,
699 active_context,
700 active_context,
701 Some(&item_active_property),
702 loader,
703 options,
704 warnings,
705 ))
706 .await?;
707 }
708
709 map_key
710 };
711
712 // If `map_key` is null, set it to the result of
713 // IRI compacting @none.
714 let map_key = match map_key {
715 Some(key) => key,
716 None => keyword_alias(vocabulary, active_context, options, Keyword::None).to_string(),
717 };
718
719 // Use `add_value` to add `compacted_item` to
720 // the `map_key` entry in `map_object` using `as_array`.
721 add_value(map_object, &map_key, compacted_item, as_array);
722 } else {
723 // Otherwise, use `add_value` to add `compacted_item` to the
724 // `item_active_property` entry in `nest_result` using `as_array`.
725 add_value(nest_result, &item_active_property, compacted_item, as_array);
726 }
727 }
728 }
729 }
730 }
731
732 // If expanded value is an empty array:
733 if is_empty {
734 // Initialize `item_active_property` by IRI compacting
735 // `expanded_property` using `expanded_value` for `value` and
736 // `inside_reverse` for `reverse`.
737 let item_active_property = compact_iri_with(
738 vocabulary,
739 active_context,
740 &expanded_property,
741 &Indexed::new(Object::node(Node::new()), None),
742 true,
743 inside_reverse,
744 options,
745 )?;
746
747 // If the term definition for `item_active_property` in the active context
748 // has a nest value entry (nest term):
749 if let Some(item_active_property) = item_active_property {
750 let (nest_result, _, _) = select_nest_result(result, active_context, &item_active_property, options.compact_arrays)?;
751
752 // Use `add_value` to add an empty array to the `item_active_property` entry in
753 // `nest_result` using true for `as_array`.
754 add_value(nest_result, &item_active_property, Vec::new().into(), true);
755 }
756 }
757
758 Ok(())
759}