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_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}