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