Skip to main content

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}