Skip to main content

jsonld_compaction/
value_hint.rs

1use derive_where::derive_where;
2use jsonld_core::{Context, Direction, Id, Indexed, LenientLangTag, Nullable, Object, Type, Value, object};
3use std::fmt;
4
5/// Describes a value to [IRI compaction][1] without materializing it.
6///
7/// IRI compaction picks the term for a property by looking at the value that
8/// will be written under it: its kind (node, value, list or graph object),
9/// its `@type`, `@language` and `@direction`, its `@id` and whether it has an
10/// `@index`. [`compact_iri_with`][crate::compact_iri_with] reads all of that
11/// off an expanded [`Indexed`] object; [`compact_iri_with_hint`] reads it off
12/// this hint instead, so a caller walking its own data model can select terms
13/// without first building `jsonld-core` objects.
14///
15/// A hint selects exactly the term the described object would select: both
16/// entry points derive the container list and the type/language preference of
17/// [term selection][2] through the same code.
18///
19/// [1]: https://www.w3.org/TR/json-ld-api/#iri-compaction
20/// [2]: https://www.w3.org/TR/json-ld-api/#term-selection
21/// [`compact_iri_with_hint`]: crate::compact_iri_with_hint
22#[derive_where(Clone, Copy)]
23#[derive_where(Debug; T: fmt::Debug, B: fmt::Debug)]
24pub struct ValueHint<'a, T, B> {
25    /// Kind of the described value.
26    pub kind: ValueKind<'a, T, B>,
27
28    /// Whether the described value carries an `@index` entry.
29    pub indexed: bool,
30}
31
32impl<'a, T, B> ValueHint<'a, T, B> {
33    /// Creates a hint for a value without an `@index` entry.
34    #[must_use]
35    pub const fn new(kind: ValueKind<'a, T, B>) -> Self {
36        Self { kind, indexed: false }
37    }
38
39    /// Creates a hint for a value carrying an `@index` entry.
40    #[must_use]
41    pub const fn indexed(kind: ValueKind<'a, T, B>) -> Self {
42        Self { kind, indexed: true }
43    }
44}
45
46/// Kind of a value described by a [`ValueHint`].
47///
48/// Each variant stands for one shape of expanded JSON-LD and carries exactly
49/// the parts of it that IRI compaction reads.
50#[derive_where(Clone, Copy)]
51#[derive_where(Debug; T: fmt::Debug, B: fmt::Debug)]
52pub enum ValueKind<'a, T, B> {
53    /// A node object that is not a graph object, such as a node reference
54    /// (`{"@id": ...}`), together with its `@id`, if any.
55    ///
56    /// When the `@id` compacts to a term whose IRI mapping is that same
57    /// `@id`, term selection prefers `@vocab`-typed terms over `@id`-typed
58    /// ones.
59    Node {
60        /// The node's `@id`.
61        id: Option<&'a Id<T, B>>,
62    },
63
64    /// A graph object (a node object whose only entries are `@graph` and
65    /// optionally `@id` and `@index`), together with its `@id`, if any.
66    Graph {
67        /// The graph's `@id`.
68        id: Option<&'a Id<T, B>>,
69    },
70
71    /// A value object whose `@value` is a string, number, boolean or `null`,
72    /// with no `@language` and no `@direction`.
73    Literal {
74        /// The value's `@type` IRI, if it has one.
75        datatype: Option<&'a T>,
76    },
77
78    /// A value object holding a string with a `@language`, a `@direction`, or
79    /// both.
80    LangString {
81        /// The value's `@language`.
82        language: Option<&'a LenientLangTag>,
83
84        /// The value's `@direction`.
85        direction: Option<Direction>,
86    },
87
88    /// A value object of type `@json`.
89    Json,
90
91    /// A list object, with the kinds of its items in order.
92    ///
93    /// Only whether each item is a value object, and its type, language and
94    /// direction if it is, take part in term selection; `@index` entries of
95    /// the items do not.
96    List(&'a [ValueKind<'a, T, B>]),
97
98    /// The empty array an expanded property holds when it has no values.
99    ///
100    /// Selects the term an `@id`-less node object would.
101    EmptyArray,
102}
103
104impl<'a, T, B> ValueKind<'a, T, B> {
105    /// Returns the value object this kind describes, or `None` if it is not
106    /// a value object.
107    fn value_object(self) -> Option<ValueObjectFeatures<'a, T>> {
108        match self {
109            Self::Literal { datatype } => Some(ValueObjectFeatures::Literal(datatype)),
110            Self::LangString { language, direction } => Some(ValueObjectFeatures::LangString { language, direction }),
111            Self::Json => Some(ValueObjectFeatures::Json),
112            Self::Node { .. } | Self::Graph { .. } | Self::List(_) | Self::EmptyArray => None,
113        }
114    }
115}
116
117/// Everything IRI compaction reads off the value it compacts a property for.
118///
119/// Derived either from an expanded object or from a [`ValueHint`]; deriving
120/// both through this one type is what guarantees they select the same term.
121pub(crate) struct ValueFeatures<'a, T, B> {
122    /// Whether the value carries an `@index` entry.
123    pub(crate) has_index: bool,
124
125    /// Shape of the value.
126    pub(crate) shape: ValueShape<'a, T, B>,
127}
128
129impl<'a, T, B> ValueFeatures<'a, T, B> {
130    /// Returns the `@id` of the value, if it is a node or graph object with
131    /// one.
132    pub(crate) fn id(&self) -> Option<&'a Id<T, B>> {
133        match &self.shape {
134            ValueShape::Node { id } | ValueShape::Graph { id } => *id,
135            ValueShape::Value(_) | ValueShape::List { .. } => None,
136        }
137    }
138
139    /// Checks whether the value is a graph object.
140    pub(crate) fn is_graph(&self) -> bool {
141        matches!(self.shape, ValueShape::Graph { .. })
142    }
143
144    /// Checks whether the value is a list object without items.
145    pub(crate) fn is_empty_list(&self) -> bool {
146        matches!(self.shape, ValueShape::List { is_empty: true, .. })
147    }
148}
149
150/// Shape of a value as IRI compaction distinguishes it.
151pub(crate) enum ValueShape<'a, T, B> {
152    /// A node object that is not a graph object.
153    Node { id: Option<&'a Id<T, B>> },
154
155    /// A graph object.
156    Graph { id: Option<&'a Id<T, B>> },
157
158    /// A value object.
159    Value(ValueObjectFeatures<'a, T>),
160
161    /// A list object, reduced to the type or language and direction its items
162    /// have in common.
163    List {
164        common_type: Option<Type<T>>,
165        common_lang_dir: Nullable<(Option<&'a LenientLangTag>, Option<Direction>)>,
166        is_empty: bool,
167    },
168}
169
170/// The parts of a value object IRI compaction reads.
171pub(crate) enum ValueObjectFeatures<'a, T> {
172    /// A literal, with its datatype IRI if any.
173    Literal(Option<&'a T>),
174
175    /// A string with a language, a direction, or both.
176    LangString {
177        language: Option<&'a LenientLangTag>,
178        direction: Option<Direction>,
179    },
180
181    /// A `@json` literal.
182    Json,
183}
184
185impl<'a, T> ValueObjectFeatures<'a, T> {
186    /// Reads the features of an expanded value object.
187    fn of_value(value: &'a Value<T>) -> Self {
188        match value {
189            Value::Literal(_, datatype) => Self::Literal(datatype.as_ref()),
190            Value::LangString(s) => Self::LangString {
191                language: s.language(),
192                direction: s.direction(),
193            },
194            Value::Json(_) => Self::Json,
195        }
196    }
197
198    /// The value's `@language`.
199    pub(crate) fn language(&self) -> Option<&'a LenientLangTag> {
200        match self {
201            Self::LangString { language, .. } => *language,
202            Self::Literal(_) | Self::Json => None,
203        }
204    }
205
206    /// The value's `@direction`.
207    pub(crate) fn direction(&self) -> Option<Direction> {
208        match self {
209            Self::LangString { direction, .. } => *direction,
210            Self::Literal(_) | Self::Json => None,
211        }
212    }
213
214    /// The value's `@type`, with `@json` literals reporting [`Type::Json`].
215    pub(crate) fn typ(&self) -> Option<Type<&'a T>> {
216        match self {
217            Self::Literal(Some(datatype)) => Some(Type::Iri(*datatype)),
218            Self::Json => Some(Type::Json),
219            Self::Literal(None) | Self::LangString { .. } => None,
220        }
221    }
222}
223
224/// Values IRI compaction can read [`ValueFeatures`] from.
225pub(crate) trait IntoValueFeatures<'a, T, B> {
226    /// Derives the features of this value under `active_context`.
227    fn into_value_features(self, active_context: &'a Context<T, B>) -> ValueFeatures<'a, T, B>;
228}
229
230impl<'a, T: Clone + PartialEq, B, O: object::Any<T, B>> IntoValueFeatures<'a, T, B> for &'a Indexed<O> {
231    fn into_value_features(self, active_context: &'a Context<T, B>) -> ValueFeatures<'a, T, B> {
232        let shape = match self.inner().as_ref() {
233            object::Ref::List(list) => list_shape(
234                list.iter().map(|item| match item.inner() {
235                    Object::Value(value) => Some(ValueObjectFeatures::of_value(value)),
236                    Object::Node(_) | Object::List(_) => None,
237                }),
238                list.is_empty(),
239                active_context,
240            ),
241            object::Ref::Node(node) if node.is_graph() => ValueShape::Graph { id: node.id.as_ref() },
242            object::Ref::Node(node) => ValueShape::Node { id: node.id.as_ref() },
243            object::Ref::Value(value) => ValueShape::Value(ValueObjectFeatures::of_value(value)),
244        };
245
246        ValueFeatures {
247            has_index: self.index().is_some(),
248            shape,
249        }
250    }
251}
252
253impl<'a, T: Clone + PartialEq, B> IntoValueFeatures<'a, T, B> for ValueHint<'a, T, B> {
254    fn into_value_features(self, active_context: &'a Context<T, B>) -> ValueFeatures<'a, T, B> {
255        let shape = match self.kind {
256            ValueKind::Node { id } => ValueShape::Node { id },
257            ValueKind::EmptyArray => ValueShape::Node { id: None },
258            ValueKind::Graph { id } => ValueShape::Graph { id },
259            ValueKind::Literal { datatype } => ValueShape::Value(ValueObjectFeatures::Literal(datatype)),
260            ValueKind::LangString { language, direction } => ValueShape::Value(ValueObjectFeatures::LangString { language, direction }),
261            ValueKind::Json => ValueShape::Value(ValueObjectFeatures::Json),
262            ValueKind::List(items) => list_shape(items.iter().map(|item| item.value_object()), items.is_empty(), active_context),
263        };
264
265        ValueFeatures {
266            has_index: self.indexed,
267            shape,
268        }
269    }
270}
271
272/// Reduces the items of a list object to the type, or the language and
273/// direction, they have in common, following step 2.6 of the
274/// [IRI compaction algorithm][1].
275///
276/// `items` yields the features of each item that is a value object and `None`
277/// for every other item. An empty list takes the default language and base
278/// direction of `active_context`.
279///
280/// [1]: https://www.w3.org/TR/json-ld-api/#iri-compaction
281fn list_shape<'a, T: Clone + PartialEq, B>(
282    items: impl Iterator<Item = Option<ValueObjectFeatures<'a, T>>>,
283    is_empty: bool,
284    active_context: &'a Context<T, B>,
285) -> ValueShape<'a, T, B> {
286    let mut common_type = None;
287    let mut common_lang_dir = None;
288
289    if is_empty {
290        common_lang_dir = Some(Nullable::Some((active_context.default_language(), active_context.default_base_direction())));
291    } else {
292        for item in items {
293            let mut item_type = None;
294            let mut item_lang_dir = None;
295            let is_value = item.is_some();
296
297            match item {
298                Some(ValueObjectFeatures::LangString { language, direction }) => item_lang_dir = Some(Nullable::Some((language, direction))),
299                Some(ValueObjectFeatures::Literal(Some(datatype))) => item_type = Some(Type::Iri(datatype.clone())),
300                Some(ValueObjectFeatures::Literal(None)) => item_lang_dir = Some(Nullable::Null),
301                Some(ValueObjectFeatures::Json) => item_type = Some(Type::Json),
302                None => item_type = Some(Type::Id),
303            }
304
305            if common_lang_dir.is_none() {
306                common_lang_dir = item_lang_dir;
307            } else if is_value && common_lang_dir != item_lang_dir {
308                common_lang_dir = Some(Nullable::Some((None, None)));
309            }
310
311            if common_type.is_none() {
312                common_type = Some(item_type);
313            } else if common_type.as_ref().is_some_and(|t| *t != item_type) {
314                common_type = Some(None);
315            }
316
317            if common_lang_dir == Some(Nullable::Some((None, None))) && common_type == Some(None) {
318                break;
319            }
320        }
321    }
322
323    ValueShape::List {
324        common_type: common_type.unwrap_or(None),
325        common_lang_dir: common_lang_dir.unwrap_or(Nullable::Some((None, None))),
326        is_empty,
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    #![allow(clippy::unwrap_used)]
333    use super::*;
334    use crate::{
335        Options,
336        compact_iri_with,
337        compact_iri_with_hint,
338        test_fixtures::{TestContext, TestObject, expanded_object, iri, iri_term, processed_context},
339    };
340    use jsonld_core::{Node, ProcessingMode};
341    use rdfx::{BlankIdBuf, IriBuf, vocabulary::no_vocabulary};
342    use std::collections::BTreeSet;
343
344    /// Defines a term for `https://example.com/name` with every type, language
345    /// and direction mapping term selection distinguishes, and no container
346    /// mapping, so that the type and language preference decide the term.
347    const MAPPING_CONTEXT: &str = r#"{
348        "@vocab": "https://vocab.example/",
349        "ex": "https://example.com/",
350        "xsd": "http://www.w3.org/2001/XMLSchema#",
351        "Building": "https://example.com/Building",
352        "name": "https://example.com/name",
353        "nameEn": { "@id": "https://example.com/name", "@language": "en" },
354        "nameArRtl": { "@id": "https://example.com/name", "@language": "ar", "@direction": "rtl" },
355        "nameLtr": { "@id": "https://example.com/name", "@direction": "ltr" },
356        "nameCount": { "@id": "https://example.com/name", "@type": "xsd:integer" },
357        "nameRef": { "@id": "https://example.com/name", "@type": "@id" },
358        "nameVocab": { "@id": "https://example.com/name", "@type": "@vocab" },
359        "nameJson": { "@id": "https://example.com/name", "@type": "@json" },
360        "nameReverse": { "@reverse": "https://example.com/name" }
361    }"#;
362
363    /// [`MAPPING_CONTEXT`] plus a term for `https://example.com/name` with
364    /// every container mapping term selection distinguishes, so that the
365    /// container preference decides the term.
366    const SELECTION_CONTEXT: &str = r#"{
367        "@vocab": "https://vocab.example/",
368        "ex": "https://example.com/",
369        "xsd": "http://www.w3.org/2001/XMLSchema#",
370        "Building": "https://example.com/Building",
371        "name": "https://example.com/name",
372        "nameEn": { "@id": "https://example.com/name", "@language": "en" },
373        "nameArRtl": { "@id": "https://example.com/name", "@language": "ar", "@direction": "rtl" },
374        "nameLtr": { "@id": "https://example.com/name", "@direction": "ltr" },
375        "nameCount": { "@id": "https://example.com/name", "@type": "xsd:integer" },
376        "nameRef": { "@id": "https://example.com/name", "@type": "@id" },
377        "nameVocab": { "@id": "https://example.com/name", "@type": "@vocab" },
378        "nameJson": { "@id": "https://example.com/name", "@type": "@json" },
379        "nameList": { "@id": "https://example.com/name", "@container": "@list" },
380        "nameSet": { "@id": "https://example.com/name", "@container": "@set" },
381        "nameMap": { "@id": "https://example.com/name", "@container": "@language" },
382        "nameIndex": { "@id": "https://example.com/name", "@container": "@index" },
383        "nameById": { "@id": "https://example.com/name", "@container": "@id" },
384        "nameGraph": { "@id": "https://example.com/name", "@container": "@graph" },
385        "nameNamedGraphs": { "@id": "https://example.com/name", "@container": ["@graph", "@id"] },
386        "nameReverse": { "@reverse": "https://example.com/name" }
387    }"#;
388
389    /// An IRI with many candidate terms, one only reachable as a compact IRI,
390    /// and one only reachable through `@vocab`.
391    const PROPERTIES: [&str; 3] = ["https://example.com/name", "https://example.com/unlisted", "https://vocab.example/temperature"];
392
393    const XSD_INTEGER: &str = "http://www.w3.org/2001/XMLSchema#integer";
394
395    /// Asserts that `hint` selects the term `object` selects, for every
396    /// property of [`PROPERTIES`], forward and reverse, under both processing
397    /// modes, and returns the term selected for the forward
398    /// `https://example.com/name` property under JSON-LD 1.1.
399    fn assert_same_selection(context: &TestContext, object: &TestObject, hint: ValueHint<IriBuf, BlankIdBuf>) -> Option<String> {
400        let modes = [ProcessingMode::JsonLd1_1, ProcessingMode::JsonLd1_0];
401        for processing_mode in modes {
402            let options = Options {
403                processing_mode,
404                ..Options::default()
405            };
406            for property in PROPERTIES {
407                let var = iri_term(property);
408                for reverse in [false, true] {
409                    let from_object = compact_iri_with(no_vocabulary(), context, &var, object, true, reverse, options).map_err(|_| ());
410                    let from_hint = compact_iri_with_hint(no_vocabulary(), context, &var, hint, true, reverse, options).map_err(|_| ());
411                    assert_eq!(
412                        from_object, from_hint,
413                        "{object:?} and {hint:?} select different terms for {property} (reverse: {reverse}, {processing_mode:?})"
414                    );
415                }
416            }
417        }
418
419        compact_iri_with_hint(no_vocabulary(), context, &iri_term(PROPERTIES[0]), hint, true, false, Options::default())
420            .unwrap()
421            .map(|term| term.to_string())
422    }
423
424    #[tokio::test]
425    async fn hint_selects_the_term_its_object_selects() {
426        let contexts = [processed_context(MAPPING_CONTEXT).await, processed_context(SELECTION_CONTEXT).await];
427
428        let building = Id::iri(iri("https://example.com/Building"));
429        let thing = Id::iri(iri("urn:ngsi-ld:Thing:1"));
430        let blank = Id::blank(BlankIdBuf::new("_:b0".to_owned()).unwrap());
431        let graph = Id::iri(iri("urn:graph"));
432        let xsd_integer = iri(XSD_INTEGER);
433        let xsd_date = iri("http://www.w3.org/2001/XMLSchema#date");
434        let en = LenientLangTag::new("en").0;
435        let ar = LenientLangTag::new("ar").0;
436
437        let plain = ValueKind::Literal { datatype: None };
438        let integer = ValueKind::Literal { datatype: Some(&xsd_integer) };
439        let english = ValueKind::LangString {
440            language: Some(en),
441            direction: None,
442        };
443        let plain_items = [plain, plain];
444        let english_items = [english, english];
445        let mixed_items = [integer, english, plain];
446        let reference_items = [ValueKind::Node { id: Some(&thing) }, ValueKind::Node { id: Some(&building) }];
447        let integer_items = [integer, integer];
448        let json_items = [ValueKind::Json];
449
450        let cases: Vec<(&str, ValueHint<IriBuf, BlankIdBuf>)> = vec![
451            (
452                r#"{ "@id": "https://example.com/Building" }"#,
453                ValueHint::new(ValueKind::Node { id: Some(&building) }),
454            ),
455            (r#"{ "@id": "urn:ngsi-ld:Thing:1" }"#, ValueHint::new(ValueKind::Node { id: Some(&thing) })),
456            (r#"{ "@id": "_:b0" }"#, ValueHint::new(ValueKind::Node { id: Some(&blank) })),
457            (
458                r#"{ "@id": "urn:ngsi-ld:Thing:1", "@index": "i" }"#,
459                ValueHint::indexed(ValueKind::Node { id: Some(&thing) }),
460            ),
461            (
462                r#"{ "https://example.com/p": [{ "@value": "x" }] }"#,
463                ValueHint::new(ValueKind::Node { id: None }),
464            ),
465            (
466                r#"{ "@id": "urn:graph", "@graph": [{ "@id": "urn:ngsi-ld:Thing:1" }] }"#,
467                ValueHint::new(ValueKind::Graph { id: Some(&graph) }),
468            ),
469            (
470                r#"{ "@graph": [{ "@id": "urn:ngsi-ld:Thing:1" }], "@index": "i" }"#,
471                ValueHint::indexed(ValueKind::Graph { id: None }),
472            ),
473            (
474                r#"{ "@value": "5", "@type": "http://www.w3.org/2001/XMLSchema#integer" }"#,
475                ValueHint::new(integer),
476            ),
477            (
478                r#"{ "@value": "2024-01-01", "@type": "http://www.w3.org/2001/XMLSchema#date" }"#,
479                ValueHint::new(ValueKind::Literal { datatype: Some(&xsd_date) }),
480            ),
481            (r#"{ "@value": "x" }"#, ValueHint::new(plain)),
482            (r#"{ "@value": 5 }"#, ValueHint::new(plain)),
483            (r#"{ "@value": true }"#, ValueHint::new(plain)),
484            (r#"{ "@value": "x", "@index": "i" }"#, ValueHint::indexed(plain)),
485            (r#"{ "@value": "x", "@language": "en" }"#, ValueHint::new(english)),
486            (
487                r#"{ "@value": "x", "@language": "ar", "@direction": "rtl" }"#,
488                ValueHint::new(ValueKind::LangString {
489                    language: Some(ar),
490                    direction: Some(Direction::Rtl),
491                }),
492            ),
493            (
494                r#"{ "@value": "x", "@direction": "ltr" }"#,
495                ValueHint::new(ValueKind::LangString {
496                    language: None,
497                    direction: Some(Direction::Ltr),
498                }),
499            ),
500            (r#"{ "@value": "x", "@language": "en", "@index": "i" }"#, ValueHint::indexed(english)),
501            (r#"{ "@value": { "a": [1, 2.50] }, "@type": "@json" }"#, ValueHint::new(ValueKind::Json)),
502            (
503                r#"{ "@list": [{ "@value": "a" }, { "@value": "b" }] }"#,
504                ValueHint::new(ValueKind::List(&plain_items)),
505            ),
506            (
507                r#"{ "@list": [{ "@value": "a", "@language": "en" }, { "@value": "b", "@language": "en" }] }"#,
508                ValueHint::new(ValueKind::List(&english_items)),
509            ),
510            (
511                r#"{ "@list": [{ "@value": "5", "@type": "http://www.w3.org/2001/XMLSchema#integer" }, { "@value": "a", "@language": "en" }, { "@value": "b" }] }"#,
512                ValueHint::new(ValueKind::List(&mixed_items)),
513            ),
514            (
515                r#"{ "@list": [{ "@id": "urn:ngsi-ld:Thing:1" }, { "@id": "https://example.com/Building" }] }"#,
516                ValueHint::new(ValueKind::List(&reference_items)),
517            ),
518            (
519                r#"{ "@list": [{ "@value": "1", "@type": "http://www.w3.org/2001/XMLSchema#integer" }, { "@value": "2", "@type": "http://www.w3.org/2001/XMLSchema#integer" }] }"#,
520                ValueHint::new(ValueKind::List(&integer_items)),
521            ),
522            (
523                r#"{ "@list": [{ "@value": { "a": 1 }, "@type": "@json" }] }"#,
524                ValueHint::new(ValueKind::List(&json_items)),
525            ),
526            (r#"{ "@list": [] }"#, ValueHint::new(ValueKind::List(&[]))),
527            (
528                r#"{ "@list": [{ "@value": "a" }], "@index": "i" }"#,
529                ValueHint::indexed(ValueKind::List(&plain_items[..1])),
530            ),
531        ];
532
533        let mut selected = BTreeSet::new();
534        for context in &contexts {
535            for (source, hint) in &cases {
536                selected.extend(assert_same_selection(context, &expanded_object(source), *hint));
537            }
538        }
539
540        // The comparisons above are only meaningful if the values are told
541        // apart: most of the contexts' terms must have been selected.
542        assert!(selected.len() >= 14, "too few distinct terms selected: {selected:?}");
543    }
544
545    /// The empty array of a property without values selects what an `@id`-less
546    /// node object selects, which is how the compaction algorithm compacts it.
547    #[tokio::test]
548    async fn empty_array_hint_selects_the_term_of_an_empty_property() {
549        let context = processed_context(SELECTION_CONTEXT).await;
550        let empty_node = Indexed::new(Object::node(Node::new()), None);
551        let term = assert_same_selection(&context, &empty_node, ValueHint::new(ValueKind::EmptyArray));
552        assert_eq!(term.as_deref(), Some("nameById"));
553    }
554}