Skip to main content

jsonld_core/document/
expanded.rs

1use crate::{
2    HashMap,
3    Id,
4    IndexSet,
5    Indexed,
6    IndexedObject,
7    Node,
8    Object,
9    Relabel,
10    TryFromJson,
11    object::{FragmentRef, InvalidExpandedJson, Traverse},
12};
13use iri_rs::IriBuf;
14use jsonld_syntax::IntoJsonWithContext;
15use rdfx::{
16    BlankIdBuf,
17    LocalGenerator,
18    vocabulary::{Vocabulary, VocabularyMut},
19};
20use smallvec::SmallVec;
21use std::{
22    collections::HashSet,
23    hash::{BuildHasher, Hash, Hasher},
24};
25
26/// Bucket key standing in for a full object hash.
27///
28/// Deduplicating the objects of a document needs a hash, and hashing an object
29/// walks its entire subtree — every property, every nested node. That is a
30/// steep price for what is almost always a miss.
31///
32/// This reads only the shallow fields: the kind of object, its `@index`, and,
33/// for a node, its `@id`. Equal objects necessarily agree on all three, so
34/// they always land in the same bucket, which is the property that matters.
35/// Unequal objects sharing a bucket cost nothing but the `Eq` comparison that
36/// follows.
37struct Discriminant<'a, T, B>(&'a IndexedObject<T, B>);
38
39impl<T: Hash, B: Hash> Hash for Discriminant<'_, T, B> {
40    #[inline]
41    fn hash<H: Hasher>(&self, state: &mut H) {
42        self.0.index().hash(state);
43
44        match self.0.inner() {
45            Object::Value(_) => 0u8.hash(state),
46            Object::List(_) => 1u8.hash(state),
47            Object::Node(node) => {
48                2u8.hash(state);
49                node.id.hash(state);
50            }
51        }
52    }
53}
54
55/// Result of the document expansion algorithm: a set of (indexed) objects.
56///
57/// Objects are kept in insertion order, and deduplicated through an index of
58/// discriminant buckets (shallow per-object keys) rather than by hashing
59/// each object in full.
60#[derive(Debug, Clone)]
61pub struct ExpandedDocument<T = IriBuf, B = BlankIdBuf> {
62    objects: Vec<IndexedObject<T, B>>,
63
64    /// Positions in `objects`, bucketed by discriminant. One entry inline:
65    /// collisions are the exception, not the rule.
66    buckets: HashMap<u64, SmallVec<[Entry; 1]>>,
67}
68
69/// One object's slot in a bucket.
70#[derive(Debug, Clone)]
71struct Entry {
72    /// Position in `ExpandedDocument::objects`.
73    index: usize,
74
75    /// Hash of the whole object, filled in only once this bucket holds more
76    /// than one entry.
77    ///
78    /// A bucket with a single occupant never needs it — the discriminant
79    /// already separated everything else — and computing it walks the object's
80    /// whole subtree. Documents whose objects carry distinct `@id`s therefore
81    /// never hash an object in full. Where the discriminant cannot separate
82    /// them (top-level nodes with no `@id`, say), this restores what a plain
83    /// hash set would have done: compare cheap hashes first, and reach for a
84    /// deep equality check only when they match.
85    full_hash: Option<u64>,
86}
87
88impl<T, B> Default for ExpandedDocument<T, B> {
89    #[inline(always)]
90    fn default() -> Self {
91        Self {
92            objects: Vec::new(),
93            buckets: HashMap::default(),
94        }
95    }
96}
97
98impl<T, B> ExpandedDocument<T, B> {
99    #[inline(always)]
100    /// Creates a new `ExpandedDocument`.
101    #[must_use]
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    #[inline(always)]
107    /// Creates a new `ExpandedDocument` with room for `capacity` objects.
108    ///
109    /// Worth reaching for whenever the object count is known up front: growing
110    /// the set re-hashes every object already in it, and hashing an object
111    /// walks its whole subtree.
112    #[must_use]
113    pub fn with_capacity(capacity: usize) -> Self {
114        Self {
115            objects: Vec::with_capacity(capacity),
116            buckets: HashMap::with_capacity_and_hasher(capacity, crate::hash::DefaultBuildHasher::default()),
117        }
118    }
119
120    #[inline(always)]
121    /// Returns the number of top-level objects in the document.
122    #[must_use]
123    pub fn len(&self) -> usize {
124        self.objects.len()
125    }
126
127    #[inline(always)]
128    /// Checks whether the document has no top-level objects.
129    #[must_use]
130    pub fn is_empty(&self) -> bool {
131        self.objects.is_empty()
132    }
133
134    #[inline(always)]
135    /// Returns the top-level objects of the document, in insertion order.
136    #[must_use]
137    pub fn objects(&self) -> &[IndexedObject<T, B>] {
138        &self.objects
139    }
140
141    #[inline(always)]
142    /// Consumes the document, returning its top-level objects in insertion
143    /// order.
144    #[must_use]
145    pub fn into_objects(self) -> Vec<IndexedObject<T, B>> {
146        self.objects
147    }
148
149    #[inline(always)]
150    /// Returns an iterator over the top-level objects of the document, in
151    /// insertion order.
152    pub fn iter(&self) -> std::slice::Iter<'_, IndexedObject<T, B>> {
153        self.objects.iter()
154    }
155
156    #[inline(always)]
157    /// Returns an iterator that visits every fragment of the document: each
158    /// top-level object and, recursively, everything it contains.
159    #[must_use]
160    pub fn traverse(&self) -> Traverse<'_, T, B> {
161        Traverse::new(self.iter().map(|o| FragmentRef::IndexedObject(o)))
162    }
163
164    #[inline(always)]
165    /// Counts the fragments of the document matching the given predicate.
166    pub fn count(&self, f: impl FnMut(&FragmentRef<T, B>) -> bool) -> usize {
167        self.traverse().filter(f).count()
168    }
169
170    /// Gives an identifier (`@id`) to every node, using the given generator
171    /// to create fresh identifiers for anonymous nodes.
172    ///
173    /// # Errors
174    ///
175    /// Returns an error when the generator runs out of identifiers.
176    #[inline(always)]
177    pub fn identify_all_with<V: VocabularyMut<Iri = T, BlankId = B>, G: LocalGenerator>(
178        &mut self,
179        vocabulary: &mut V,
180        generator: &mut G,
181    ) -> Result<(), crate::id::GeneratedIdError>
182    where
183        T: Eq + Hash,
184        B: Eq + Hash,
185    {
186        self.try_rewrite(|object| object.identify_all_with(vocabulary, generator))
187    }
188
189    /// Gives an identifier (`@id`) to every node, using the given generator
190    /// to create fresh identifiers for anonymous nodes.
191    ///
192    /// # Errors
193    ///
194    /// Returns an error when the generator runs out of identifiers.
195    #[inline(always)]
196    pub fn identify_all<G: LocalGenerator>(&mut self, generator: &mut G) -> Result<(), crate::id::GeneratedIdError>
197    where
198        T: Eq + Hash,
199        B: Eq + Hash,
200        (): Vocabulary<Iri = T, BlankId = B>,
201    {
202        self.identify_all_with(rdfx::vocabulary::no_vocabulary_mut(), generator)
203    }
204
205    /// Relabels every node identifier with a fresh one from the given
206    /// generator and puts every literal into canonical form.
207    ///
208    /// # Errors
209    ///
210    /// Returns an error when the generator runs out of identifiers.
211    #[inline(always)]
212    pub fn relabel_and_canonicalize_with<V: VocabularyMut<Iri = T, BlankId = B>, G: LocalGenerator>(
213        &mut self,
214        vocabulary: &mut V,
215        generator: &mut G,
216    ) -> Result<(), crate::id::GeneratedIdError>
217    where
218        T: Clone + Eq + Hash,
219        B: Clone + Eq + Hash,
220    {
221        let mut relabeling = HashMap::default();
222        let mut buffer = ryu_js::Buffer::new();
223        self.try_rewrite(|object| {
224            object.relabel_with(vocabulary, generator, &mut relabeling)?;
225            object.canonicalize_with(&mut buffer);
226            Ok(())
227        })
228    }
229
230    /// Relabels every node identifier with a fresh one from the given
231    /// generator and puts every literal into canonical form.
232    ///
233    /// # Errors
234    ///
235    /// Returns an error when the generator runs out of identifiers.
236    #[inline(always)]
237    pub fn relabel_and_canonicalize<G: LocalGenerator>(&mut self, generator: &mut G) -> Result<(), crate::id::GeneratedIdError>
238    where
239        T: Clone + Eq + Hash,
240        B: Clone + Eq + Hash,
241        (): Vocabulary<Iri = T, BlankId = B>,
242    {
243        self.relabel_and_canonicalize_with(rdfx::vocabulary::no_vocabulary_mut(), generator)
244    }
245
246    /// Relabels every node identifier with a fresh one from the given
247    /// generator.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error when the generator runs out of identifiers.
252    #[inline(always)]
253    pub fn relabel_with<V: VocabularyMut<Iri = T, BlankId = B>, G: LocalGenerator>(
254        &mut self,
255        vocabulary: &mut V,
256        generator: &mut G,
257    ) -> Result<(), crate::id::GeneratedIdError>
258    where
259        T: Clone + Eq + Hash,
260        B: Clone + Eq + Hash,
261    {
262        let mut relabeling = HashMap::default();
263        self.try_rewrite(|object| object.relabel_with(vocabulary, generator, &mut relabeling))
264    }
265
266    /// Relabels every node identifier with a fresh one from the given
267    /// generator.
268    ///
269    /// # Errors
270    ///
271    /// Returns an error when the generator runs out of identifiers.
272    #[inline(always)]
273    pub fn relabel<G: LocalGenerator>(&mut self, generator: &mut G) -> Result<(), crate::id::GeneratedIdError>
274    where
275        T: Clone + Eq + Hash,
276        B: Clone + Eq + Hash,
277        (): Vocabulary<Iri = T, BlankId = B>,
278    {
279        self.relabel_with(rdfx::vocabulary::no_vocabulary_mut(), generator)
280    }
281
282    /// Puts every literal of the document into canonical form, using the
283    /// given `buffer` to render numbers.
284    pub fn canonicalize_with(&mut self, buffer: &mut ryu_js::Buffer)
285    where
286        T: Eq + Hash,
287        B: Eq + Hash,
288    {
289        for mut object in self.drain() {
290            object.canonicalize_with(buffer);
291            self.insert(object);
292        }
293    }
294
295    /// Puts every literal of the document into canonical form.
296    pub fn canonicalize(&mut self)
297    where
298        T: Eq + Hash,
299        B: Eq + Hash,
300    {
301        let mut buffer = ryu_js::Buffer::new();
302        self.canonicalize_with(&mut buffer);
303    }
304
305    /// Rewrites every IRI and identifier of the document (recursively) with
306    /// the given functions.
307    pub fn map_ids<U, C>(self, mut map_iri: impl FnMut(T) -> U, mut map_id: impl FnMut(Id<T, B>) -> Id<U, C>) -> ExpandedDocument<U, C>
308    where
309        U: Eq + Hash,
310        C: Eq + Hash,
311    {
312        self.objects
313            .into_iter()
314            .map(|i| i.map_inner(|o| o.map_ids(&mut map_iri, &mut map_id)))
315            .collect()
316    }
317
318    /// Returns the set of all blank identifiers in the given document.
319    #[must_use]
320    pub fn blank_ids(&self) -> HashSet<&B>
321    where
322        B: Eq + Hash,
323    {
324        self.traverse().filter_map(|f| f.into_id().and_then(Id::into_blank)).collect()
325    }
326
327    /// Returns the main node object of the document, if any.
328    ///
329    /// The main node is the unique top level (root) node object. If multiple
330    /// node objects are on the root, `None` is returned.
331    #[must_use]
332    pub fn main_node(&self) -> Option<&Node<T, B>> {
333        let mut result = None;
334
335        for object in self {
336            if let Object::Node(node) = object.inner() {
337                if result.is_some() {
338                    return None;
339                }
340
341                result = Some(&**node);
342            }
343        }
344
345        result
346    }
347
348    /// Consumes the document and returns its main node object, if any.
349    ///
350    /// The main node is the unique top level (root) node object. If multiple
351    /// node objects are on the root, `None` is returned.
352    #[must_use]
353    pub fn into_main_node(self) -> Option<Node<T, B>> {
354        let mut result = None;
355
356        for object in self {
357            if let Object::Node(node) = object.into_inner() {
358                if result.is_some() {
359                    return None;
360                }
361
362                result = Some(*node);
363            }
364        }
365
366        result
367    }
368}
369
370impl<T: Hash + Eq, B: Hash + Eq> ExpandedDocument<T, B> {
371    /// Inserts an object, unless an equal one is already present.
372    ///
373    /// Returns `true` if the object was added.
374    pub fn insert(&mut self, object: IndexedObject<T, B>) -> bool {
375        // Cloned rather than borrowed so it stays usable while `buckets` is
376        // borrowed mutably below. A clone hashes identically to its original,
377        // which is what the cached `full_hash` values rely on.
378        let hasher = self.buckets.hasher().clone();
379        let discriminant = hasher.hash_one(Discriminant(&object));
380
381        // Split the borrow: the bucket lives in `buckets`, the candidates it
382        // points at live in `objects`.
383        let Self { objects, buckets } = self;
384        let bucket = buckets.entry(discriminant).or_default();
385
386        let full_hash = if bucket.is_empty() {
387            None
388        } else {
389            let full_hash = hasher.hash_one(&object);
390
391            for entry in bucket.iter_mut() {
392                let candidate = entry.full_hash.get_or_insert_with(|| hasher.hash_one(&objects[entry.index]));
393
394                if *candidate == full_hash && objects[entry.index] == object {
395                    return false;
396                }
397            }
398
399            Some(full_hash)
400        };
401
402        bucket.push(Entry {
403            index: objects.len(),
404            full_hash,
405        });
406        objects.push(object);
407        true
408    }
409
410    /// Checks whether an equal object is present.
411    pub fn contains(&self, object: &IndexedObject<T, B>) -> bool {
412        let discriminant = self.buckets.hasher().hash_one(Discriminant(object));
413
414        match self.buckets.get(&discriminant) {
415            Some(bucket) => bucket.iter().any(|entry| self.objects[entry.index] == *object),
416            None => false,
417        }
418    }
419
420    /// Empties the document, handing back its objects.
421    ///
422    /// Used by the passes that rewrite every object and put it back: rewriting
423    /// can make two objects equal, so the index has to be rebuilt rather than
424    /// patched.
425    fn drain(&mut self) -> std::vec::IntoIter<IndexedObject<T, B>> {
426        self.buckets.clear();
427        std::mem::take(&mut self.objects).into_iter()
428    }
429
430    /// Rewrites every object with `f`, draining and reinserting to rebuild
431    /// the index.
432    ///
433    /// If `f` fails, the failed object and the untouched remainder are
434    /// reinserted before the error is returned: the document is never
435    /// truncated on the error path (objects rewritten before the failure
436    /// keep their new form).
437    fn try_rewrite<E>(&mut self, mut f: impl FnMut(&mut IndexedObject<T, B>) -> Result<(), E>) -> Result<(), E> {
438        let mut objects = self.drain();
439        let mut result = Ok(());
440        for mut object in objects.by_ref() {
441            let object_result = f(&mut object);
442            self.insert(object);
443            if let Err(e) = object_result {
444                result = Err(e);
445                break;
446            }
447        }
448        for object in objects {
449            self.insert(object);
450        }
451        result
452    }
453}
454
455impl<T: Eq + Hash, B: Eq + Hash> From<Indexed<Node<T, B>>> for ExpandedDocument<T, B> {
456    fn from(value: Indexed<Node<T, B>>) -> Self {
457        let mut result = Self::default();
458
459        result.insert(value.map_inner(Object::node));
460
461        result
462    }
463}
464
465impl<T: Eq + Hash, B: Eq + Hash> TryFromJson<T, B> for ExpandedDocument<T, B> {
466    fn try_from_json_in(vocabulary: &mut impl VocabularyMut<Iri = T, BlankId = B>, value: jstrict::Value) -> Result<Self, InvalidExpandedJson> {
467        match value {
468            jstrict::Value::Array(items) => {
469                let mut result = Self::new();
470
471                for item in items {
472                    result.insert(Indexed::try_from_json_in(vocabulary, item)?);
473                }
474
475                Ok(result)
476            }
477            other => Err(InvalidExpandedJson::Unexpected(other.kind(), jstrict::Kind::Array)),
478        }
479    }
480}
481
482/// Error reading an expanded document from its JSON text.
483#[derive(Debug, thiserror::Error)]
484pub enum ExpandedJsonTextError {
485    /// The bytes are not one well-formed JSON text (RFC 8259).
486    #[error("invalid JSON: {0}")]
487    Syntax(#[from] jstrict::parse::Error),
488    /// The JSON text is not expanded JSON-LD.
489    #[error(transparent)]
490    Invalid(#[from] InvalidExpandedJson),
491}
492
493impl<T: Eq + Hash, B: Eq + Hash> ExpandedDocument<T, B> {
494    /// Reads a document already in expanded form from its JSON text, without
495    /// running the Expansion algorithm: the bytes are parsed with `jstrict`'s
496    /// non-recursive parser and read with [`TryFromJson`]. The parsed tree is
497    /// consumed by the reading, so no JSON tree outlives the call.
498    ///
499    /// # Errors
500    ///
501    /// [`ExpandedJsonTextError::Syntax`] when `bytes` is not JSON, and
502    /// [`ExpandedJsonTextError::Invalid`] for a JSON text that is not expanded
503    /// JSON-LD.
504    pub fn try_from_json_slice_in(vocabulary: &mut impl VocabularyMut<Iri = T, BlankId = B>, bytes: &[u8]) -> Result<Self, ExpandedJsonTextError> {
505        let value = jstrict::parse::parse_slice_value(bytes)?;
506        Ok(Self::try_from_json_in(vocabulary, value)?)
507    }
508}
509
510impl<T, B, N: Vocabulary<Iri = T, BlankId = B>> IntoJsonWithContext<N> for ExpandedDocument<T, B> {
511    /// Writes the document as expanded JSON-LD: an array of the document's
512    /// objects, in insertion order. Recurses once per nesting level on
513    /// sufficient native stack.
514    fn into_json_with(self, vocabulary: &N) -> jstrict::Value {
515        self.objects.into_json_with(vocabulary)
516    }
517}
518
519impl<T: Eq + Hash, B: Eq + Hash> PartialEq for ExpandedDocument<T, B> {
520    /// Comparison between two expanded documents.
521    ///
522    /// Order-independent, as it was when this was backed by a set: both sides
523    /// are deduplicated, so equal lengths plus containment one way is enough.
524    fn eq(&self, other: &Self) -> bool {
525        self.len() == other.len() && self.objects.iter().all(|object| other.contains(object))
526    }
527}
528
529impl<T: Eq + Hash, B: Eq + Hash> Eq for ExpandedDocument<T, B> {}
530
531impl<T, B> IntoIterator for ExpandedDocument<T, B> {
532    type IntoIter = IntoIter<T, B>;
533    type Item = IndexedObject<T, B>;
534
535    #[inline(always)]
536    fn into_iter(self) -> Self::IntoIter {
537        IntoIter(self.objects.into_iter())
538    }
539}
540
541impl<'a, T, B> IntoIterator for &'a ExpandedDocument<T, B> {
542    type IntoIter = std::slice::Iter<'a, IndexedObject<T, B>>;
543    type Item = &'a IndexedObject<T, B>;
544
545    #[inline(always)]
546    fn into_iter(self) -> Self::IntoIter {
547        self.iter()
548    }
549}
550/// Owning iterator over the objects of an expanded document.
551pub struct IntoIter<T, B>(std::vec::IntoIter<IndexedObject<T, B>>);
552
553impl<T, B> Iterator for IntoIter<T, B> {
554    type Item = IndexedObject<T, B>;
555
556    fn next(&mut self) -> Option<Self::Item> {
557        self.0.next()
558    }
559}
560
561impl<T: Hash + Eq, B: Hash + Eq> FromIterator<IndexedObject<T, B>> for ExpandedDocument<T, B> {
562    fn from_iter<I: IntoIterator<Item = IndexedObject<T, B>>>(iter: I) -> Self {
563        let iter = iter.into_iter();
564        let mut result = Self::with_capacity(iter.size_hint().0);
565        result.extend(iter);
566        result
567    }
568}
569
570impl<T: Hash + Eq, B: Hash + Eq> Extend<IndexedObject<T, B>> for ExpandedDocument<T, B> {
571    fn extend<I: IntoIterator<Item = IndexedObject<T, B>>>(&mut self, iter: I) {
572        for object in iter {
573            self.insert(object);
574        }
575    }
576}
577
578impl<T: Hash + Eq, B: Hash + Eq> From<IndexSet<IndexedObject<T, B>>> for ExpandedDocument<T, B> {
579    fn from(set: IndexSet<IndexedObject<T, B>>) -> Self {
580        set.into_iter().collect()
581    }
582}
583
584impl<T: Hash + Eq, B: Hash + Eq> From<Vec<IndexedObject<T, B>>> for ExpandedDocument<T, B> {
585    fn from(items: Vec<IndexedObject<T, B>>) -> Self {
586        items.into_iter().collect()
587    }
588}
589
590#[cfg(all(test, feature = "serde-json"))]
591// Test code may panic on failure; that is the point of a test.
592#[allow(clippy::unwrap_used, clippy::expect_used)]
593mod serde_json_tests {
594    use super::*;
595    use crate::Id;
596    use iri_rs::iri;
597
598    #[test]
599    fn expanded_into_serde_json_with_default_vocab() {
600        let id = Id::iri(IriBuf::from(iri!("https://example.com/x")));
601        let node = Node::<IriBuf, BlankIdBuf>::with_id(id);
602        let doc: ExpandedDocument<IriBuf, BlankIdBuf> = Indexed::new(node, None).into();
603
604        let json = doc.into_serde_json();
605        let arr = json.as_array().expect("expanded document is an array");
606        assert_eq!(arr.len(), 1);
607        assert_eq!(arr[0].get("@id").and_then(|v| v.as_str()), Some("https://example.com/x"));
608    }
609}
610
611// Test code may panic on failure; that is the point of a test.
612#[cfg(all(test, feature = "sonic-rs"))]
613#[allow(clippy::expect_used)]
614mod sonic_rs_tests {
615    use super::*;
616    use crate::Id;
617    use iri_rs::iri;
618    use sonic_rs::JsonValueTrait;
619
620    #[test]
621    fn expanded_into_sonic_rs_with_default_vocab() {
622        let id = Id::iri(IriBuf::from(iri!("https://example.com/x")));
623        let node = Node::<IriBuf, BlankIdBuf>::with_id(id);
624        let doc: ExpandedDocument<IriBuf, BlankIdBuf> = Indexed::new(node, None).into();
625
626        let json = doc.into_sonic_rs();
627        let arr = json.into_array().expect("expanded document is an array");
628        assert_eq!(arr.len(), 1);
629        assert_eq!(arr[0].get("@id").as_str(), Some("https://example.com/x"));
630    }
631}
632
633#[cfg(feature = "serde-json")]
634impl<T, B> ExpandedDocument<T, B> {
635    /// Converts the expanded document into a [`serde_json::Value`] using the
636    /// given `vocabulary`.
637    pub fn into_serde_json_with<N>(self, vocabulary: &N) -> serde_json::Value
638    where
639        N: Vocabulary<Iri = T, BlankId = B>,
640    {
641        self.into_json_with(vocabulary).into_serde_json()
642    }
643}
644
645#[cfg(feature = "serde-json")]
646impl<T, B> ExpandedDocument<T, B>
647where
648    (): Vocabulary<Iri = T, BlankId = B>,
649{
650    /// Converts the expanded document into a [`serde_json::Value`] using the
651    /// default no-vocabulary.
652    #[must_use]
653    pub fn into_serde_json(self) -> serde_json::Value {
654        self.into_serde_json_with(rdfx::vocabulary::no_vocabulary())
655    }
656}
657
658#[cfg(feature = "sonic-rs")]
659impl<T, B> ExpandedDocument<T, B> {
660    /// Converts the expanded document into a [`sonic_rs::Value`] using the
661    /// given `vocabulary`.
662    pub fn into_sonic_rs_with<N>(self, vocabulary: &N) -> sonic_rs::Value
663    where
664        N: Vocabulary<Iri = T, BlankId = B>,
665    {
666        self.into_json_with(vocabulary).into_sonic_rs()
667    }
668}
669
670#[cfg(feature = "sonic-rs")]
671impl<T, B> ExpandedDocument<T, B>
672where
673    (): Vocabulary<Iri = T, BlankId = B>,
674{
675    /// Converts the expanded document into a [`sonic_rs::Value`] using the
676    /// default no-vocabulary.
677    #[must_use]
678    pub fn into_sonic_rs(self) -> sonic_rs::Value {
679        self.into_sonic_rs_with(rdfx::vocabulary::no_vocabulary())
680    }
681}