Skip to main content

triblespace_core/import/
json.rs

1//! Deterministic JSON *object* importer built on a winnow-based streaming parser.
2//!
3//! This importer hashes attribute/value pairs to derive entity identifiers.
4//! Identical JSON objects therefore converge to the same id, enabling structural
5//! deduplication.
6//!
7//! Note: this importer only accepts a top-level JSON object, or a top-level JSON
8//! array containing objects. Primitive roots are rejected.
9
10use std::collections::{HashMap, HashSet};
11use std::fmt;
12use std::str::FromStr;
13
14use anybytes::{Bytes, View};
15use winnow::stream::Stream;
16
17use crate::blob::encodings::longstring::LongString;
18use crate::blob::Blob;
19use crate::blob::IntoBlob;
20use crate::attribute::Attribute;
21use crate::id::{ExclusiveId, Id, RawId, ID_LEN};
22use crate::macros::entity;
23use crate::metadata;
24use crate::metadata::{MetaDescribe, Describe};
25use crate::repo::BlobStore;
26use crate::trible::{Fragment, Trible, TribleSet};
27use crate::inline::encodings::boolean::Boolean;
28use crate::inline::encodings::f64::F64;
29use crate::inline::encodings::genid::GenId;
30use crate::inline::encodings::hash::{Blake3, Handle};
31use crate::inline::encodings::UnknownInline;
32use crate::inline::{RawInline, IntoInline, Inline, InlineEncoding};
33
34/// Error returned by [`JsonObjectImporter`] when importing a JSON document.
35#[derive(Debug)]
36pub enum JsonImportError {
37    /// The document root is a primitive (string, number, bool, null) — only
38    /// objects and arrays of objects are accepted.
39    PrimitiveRoot,
40    /// A string field could not be encoded into the target inline encoding.
41    EncodeString {
42        /// Name of the JSON field.
43        field: String,
44        /// Underlying encoding error.
45        source: EncodeError,
46    },
47    /// A number field could not be encoded into the target inline encoding.
48    EncodeNumber {
49        /// Name of the JSON field.
50        field: String,
51        /// Underlying encoding error.
52        source: EncodeError,
53    },
54    /// The JSON input is syntactically invalid.
55    Syntax(String),
56}
57
58impl fmt::Display for JsonImportError {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::PrimitiveRoot => write!(f, "cannot import JSON primitives as the document root"),
62            Self::EncodeString { field, source } => {
63                write!(f, "failed to encode string field {field:?}: {source}")
64            }
65            Self::EncodeNumber { field, source } => {
66                write!(f, "failed to encode number field {field:?}: {source}")
67            }
68            Self::Syntax(msg) => write!(f, "failed to parse JSON: {msg}"),
69        }
70    }
71}
72
73impl std::error::Error for JsonImportError {
74    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
75        match self {
76            Self::PrimitiveRoot | Self::Syntax(_) => None,
77            Self::EncodeString { source, .. } | Self::EncodeNumber { source, .. } => {
78                Some(source.as_error())
79            }
80        }
81    }
82}
83
84/// Opaque wrapper around a value-encoding error during JSON import.
85#[derive(Debug)]
86pub struct EncodeError(Box<dyn std::error::Error + Send + Sync + 'static>);
87
88impl EncodeError {
89    /// Creates an encode error from a plain message string.
90    pub fn message(message: impl Into<String>) -> Self {
91        #[derive(Debug)]
92        struct Message(String);
93
94        impl fmt::Display for Message {
95            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96                f.write_str(&self.0)
97            }
98        }
99
100        impl std::error::Error for Message {}
101
102        Self(Box::new(Message(message.into())))
103    }
104
105    fn as_error(&self) -> &(dyn std::error::Error + 'static) {
106        self.0.as_ref()
107    }
108
109    /// Wraps an existing error as an encode error.
110    pub fn from_error(err: impl std::error::Error + Send + Sync + 'static) -> Self {
111        Self(Box::new(err))
112    }
113}
114
115impl fmt::Display for EncodeError {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        fmt::Display::fmt(self.0.as_ref(), f)
118    }
119}
120
121impl std::error::Error for EncodeError {
122    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
123        Some(self.0.as_ref())
124    }
125}
126
127type ParsedString = View<str>;
128
129/// Deterministic JSON importer that derives entity ids from attribute/value pairs.
130///
131/// This importer expects either:
132/// - a top-level JSON object, or
133/// - a top-level array of JSON objects.
134///
135/// Use [`crate::import::json_tree::JsonTreeImporter`] when you need a lossless
136/// representation of arbitrary JSON values (including primitive roots).
137pub struct JsonObjectImporter<'a, Store>
138where
139    Store: BlobStore,
140{
141    store: &'a mut Store,
142    bool_attrs: HashMap<View<str>, Attribute<Boolean>>,
143    num_attrs: HashMap<View<str>, Attribute<F64>>,
144    str_attrs: HashMap<View<str>, Attribute<Handle<LongString>>>,
145    genid_attrs: HashMap<View<str>, Attribute<GenId>>,
146    id_salt: Option<[u8; 32]>,
147    array_fields: HashSet<View<str>>,
148}
149
150impl<'a, Store> JsonObjectImporter<'a, Store>
151where
152    Store: BlobStore,
153{
154    fn attr_from_field<S: InlineEncoding + MetaDescribe>(
155        &mut self,
156        field: &ParsedString,
157    ) -> Result<Attribute<S>, JsonImportError> {
158        let handle =
159            self.store
160                .put(field.clone())
161                .map_err(|err| JsonImportError::EncodeString {
162                    field: field.as_ref().to_owned(),
163                    source: EncodeError::from_error(err),
164                })?;
165        Ok(Attribute::<S>::from(entity! {
166            metadata::name:         handle,
167            metadata::value_encoding: <S as MetaDescribe>::id(),
168        }))
169    }
170
171    fn bool_attr(
172        &mut self,
173        field: &ParsedString,
174    ) -> Result<Attribute<Boolean>, JsonImportError> {
175        let key = field.clone();
176        if let Some(attr) = self.bool_attrs.get(&key) {
177            return Ok(attr.clone());
178        }
179        let attr = self.attr_from_field::<Boolean>(field)?;
180        self.bool_attrs.insert(key, attr.clone());
181        Ok(attr)
182    }
183
184    fn num_attr(&mut self, field: &ParsedString) -> Result<Attribute<F64>, JsonImportError> {
185        let key = field.clone();
186        if let Some(attr) = self.num_attrs.get(&key) {
187            return Ok(attr.clone());
188        }
189        let attr = self.attr_from_field::<F64>(field)?;
190        self.num_attrs.insert(key, attr.clone());
191        Ok(attr)
192    }
193
194    fn str_attr(
195        &mut self,
196        field: &ParsedString,
197    ) -> Result<Attribute<Handle<LongString>>, JsonImportError> {
198        let key = field.clone();
199        if let Some(attr) = self.str_attrs.get(&key) {
200            return Ok(attr.clone());
201        }
202        let attr = self.attr_from_field::<Handle<LongString>>(field)?;
203        self.str_attrs.insert(key, attr.clone());
204        Ok(attr)
205    }
206
207    fn genid_attr(
208        &mut self,
209        field: &ParsedString,
210    ) -> Result<Attribute<GenId>, JsonImportError> {
211        let key = field.clone();
212        if let Some(attr) = self.genid_attrs.get(&key) {
213            return Ok(attr.clone());
214        }
215        let attr = self.attr_from_field::<GenId>(field)?;
216        self.genid_attrs.insert(key, attr.clone());
217        Ok(attr)
218    }
219
220    /// Creates a new importer backed by `store`. Pass an optional 32-byte
221    /// salt to namespace the deterministic entity ids.
222    pub fn new(store: &'a mut Store, id_salt: Option<[u8; 32]>) -> Self {
223        Self {
224            store,
225            bool_attrs: HashMap::new(),
226            num_attrs: HashMap::new(),
227            str_attrs: HashMap::new(),
228            genid_attrs: HashMap::new(),
229            id_salt,            array_fields: HashSet::new(),
230        }
231    }
232
233    /// Imports a JSON string. Convenience wrapper around [`import_blob`](Self::import_blob).
234    pub fn import_str(&mut self, input: &str) -> Result<Fragment, JsonImportError> {
235        self.import_blob(input.to_owned().to_blob())
236    }
237
238    /// Imports a JSON document from a [`LongString`] blob, returning a
239    /// [`Fragment`] with the root entity ids as exports.
240    pub fn import_blob(&mut self, blob: Blob<LongString>) -> Result<Fragment, JsonImportError> {
241        let mut bytes = blob.bytes.clone();
242        self.skip_ws(&mut bytes);
243
244        let mut roots = Vec::new();
245        let mut staged = TribleSet::new();
246        match bytes.peek_token() {
247            Some(b'{') => {
248                let (root, obj_staged) = self.parse_object(&mut bytes)?;
249                staged += obj_staged;
250                roots.push(root.forget());
251            }
252            Some(b'[') => {
253                self.consume_byte(&mut bytes, b'[')?;
254                self.skip_ws(&mut bytes);
255                if bytes.peek_token() == Some(b']') {
256                    self.consume_byte(&mut bytes, b']')?;
257                } else {
258                    loop {
259                        self.skip_ws(&mut bytes);
260                        if bytes.peek_token() != Some(b'{') {
261                            return Err(JsonImportError::PrimitiveRoot);
262                        }
263                        let (root, obj_staged) = self.parse_object(&mut bytes)?;
264                        staged += obj_staged;
265                        roots.push(root.forget());
266                        self.skip_ws(&mut bytes);
267                        match bytes.peek_token() {
268                            Some(b',') => {
269                                self.consume_byte(&mut bytes, b',')?;
270                                continue;
271                            }
272                            Some(b']') => {
273                                self.consume_byte(&mut bytes, b']')?;
274                                break;
275                            }
276                            _ => return Err(JsonImportError::PrimitiveRoot),
277                        }
278                    }
279                }
280            }
281            _ => return Err(JsonImportError::PrimitiveRoot),
282        }
283
284        self.skip_ws(&mut bytes);
285        Ok(Fragment::new(roots, staged))
286    }
287
288    fn parse_object(
289        &mut self,
290        bytes: &mut Bytes,
291    ) -> Result<(ExclusiveId, TribleSet), JsonImportError> {
292        self.consume_byte(bytes, b'{')?;
293        self.skip_ws(bytes);
294        let mut pairs: Vec<(RawId, RawInline)> = Vec::new();
295        let mut staged = TribleSet::new();
296
297        if bytes.peek_token() == Some(b'}') {
298            self.consume_byte(bytes, b'}')?;
299        } else {
300            loop {
301                let field = self.parse_string(bytes)?;
302                self.skip_ws(bytes);
303                self.consume_byte(bytes, b':')?;
304                self.skip_ws(bytes);
305                self.parse_value(bytes, &field, &mut pairs, &mut staged)?;
306                self.skip_ws(bytes);
307                match bytes.peek_token() {
308                    Some(b',') => {
309                        self.consume_byte(bytes, b',')?;
310                        self.skip_ws(bytes);
311                    }
312                    Some(b'}') => {
313                        self.consume_byte(bytes, b'}')?;
314                        break;
315                    }
316                    _ => return Err(JsonImportError::Syntax("unexpected token".into())),
317                }
318            }
319        }
320
321        let entity = self.derive_id(&pairs)?;
322        for (attr_raw, value_raw) in pairs {
323            let attr_id = Id::new(attr_raw).ok_or(JsonImportError::PrimitiveRoot)?;
324            let value = Inline::<UnknownInline>::new(value_raw);
325            staged.insert(&Trible::new(&entity, &attr_id, &value));
326        }
327
328        Ok((entity, staged))
329    }
330
331    fn parse_array(
332        &mut self,
333        bytes: &mut Bytes,
334        field: &ParsedString,
335        pairs: &mut Vec<(RawId, RawInline)>,
336        staged: &mut TribleSet,
337    ) -> Result<(), JsonImportError> {
338        self.consume_byte(bytes, b'[')?;
339        self.array_fields.insert(field.clone());
340        self.skip_ws(bytes);
341        if bytes.peek_token() == Some(b']') {
342            self.consume_byte(bytes, b']')?;
343            return Ok(());
344        }
345
346        loop {
347            self.parse_value(bytes, field, pairs, staged)?;
348            self.skip_ws(bytes);
349            match bytes.peek_token() {
350                Some(b',') => {
351                    self.consume_byte(bytes, b',')?;
352                    self.skip_ws(bytes);
353                }
354                Some(b']') => {
355                    self.consume_byte(bytes, b']')?;
356                    break;
357                }
358                _ => return Err(JsonImportError::Syntax("unexpected token".into())),
359            }
360        }
361        Ok(())
362    }
363
364    fn parse_value(
365        &mut self,
366        bytes: &mut Bytes,
367        field: &ParsedString,
368        pairs: &mut Vec<(RawId, RawInline)>,
369        staged: &mut TribleSet,
370    ) -> Result<(), JsonImportError> {
371        match bytes.peek_token() {
372            Some(b'n') => {
373                self.consume_literal(bytes, b"null")?;
374                Ok(())
375            }
376            Some(b't') => {
377                self.consume_literal(bytes, b"true")?;
378                let attr = self.bool_attr(field)?;
379                pairs.push((attr.raw(), attr.inline_from(true).raw));
380                Ok(())
381            }
382            Some(b'f') => {
383                self.consume_literal(bytes, b"false")?;
384                let attr = self.bool_attr(field)?;
385                pairs.push((attr.raw(), attr.inline_from(false).raw));
386                Ok(())
387            }
388            Some(b'"') => {
389                let text = self.parse_string(bytes)?;
390                let field_name = field.as_ref().to_owned();
391                let attr = self.str_attr(field)?;
392                let handle: Inline<Handle<LongString>> = self
393                    .store
394                    .put(text)
395                    .map_err(|err| JsonImportError::EncodeString {
396                        field: field_name,
397                        source: EncodeError::from_error(err),
398                    })?;
399                pairs.push((attr.raw(), handle.raw));
400                Ok(())
401            }
402            Some(b'{') => {
403                let (child, child_staged) = self.parse_object(bytes)?;
404                *staged += child_staged;
405                let attr = self.genid_attr(field)?;
406                let value = GenId::inline_from(&child);
407                pairs.push((attr.raw(), value.raw));
408                Ok(())
409            }
410            Some(b'[') => self.parse_array(bytes, field, pairs, staged),
411            _ => {
412                let num = self.parse_number(bytes)?;
413                let num_str = num
414                    .view::<str>()
415                    .map_err(|_| JsonImportError::Syntax("invalid number".into()))?;
416                let number: f64 = f64::from_str(num_str.as_ref()).map_err(|err| {
417                    JsonImportError::EncodeNumber {
418                        field: field.as_ref().to_owned(),
419                        source: EncodeError::from_error(err),
420                    }
421                })?;
422                if !number.is_finite() {
423                    return Err(JsonImportError::EncodeNumber {
424                        field: field.as_ref().to_owned(),
425                        source: EncodeError::message("non-finite number"),
426                    });
427                }
428                let attr = self.num_attr(field)?;
429                let encoded: Inline<F64> = number.to_inline();
430                pairs.push((attr.raw(), encoded.raw));
431                Ok(())
432            }
433        }
434    }
435
436    fn derive_id(&self, pairs: &[(RawId, RawInline)]) -> Result<ExclusiveId, JsonImportError> {
437        let mut sorted = pairs.to_vec();
438        sorted
439            .sort_by(|(a_attr, a_val), (b_attr, b_val)| a_attr.cmp(b_attr).then(a_val.cmp(b_val)));
440
441        let mut hasher = Blake3::new();
442        if let Some(salt) = self.id_salt {
443            hasher.update(salt.as_ref());
444        }
445        for (attr, value) in &sorted {
446            hasher.update(attr);
447            hasher.update(value);
448        }
449        let digest: [u8; 32] = hasher.finalize();
450        let mut raw = [0u8; ID_LEN];
451        raw.copy_from_slice(&digest[digest.len() - ID_LEN..]);
452        let id = Id::new(raw).ok_or(JsonImportError::PrimitiveRoot)?;
453        Ok(ExclusiveId::force(id))
454    }
455
456    fn skip_ws(&self, bytes: &mut Bytes) {
457        while matches!(bytes.peek_token(), Some(b) if b.is_ascii_whitespace()) {
458            bytes.pop_front();
459        }
460    }
461
462    fn consume_byte(&self, bytes: &mut Bytes, expected: u8) -> Result<(), JsonImportError> {
463        match bytes.pop_front() {
464            Some(b) if b == expected => Ok(()),
465            _ => Err(JsonImportError::Syntax("unexpected token".into())),
466        }
467    }
468
469    fn consume_literal(&self, bytes: &mut Bytes, literal: &[u8]) -> Result<(), JsonImportError> {
470        for expected in literal {
471            self.consume_byte(bytes, *expected)?;
472        }
473        Ok(())
474    }
475
476    fn parse_string(&self, bytes: &mut Bytes) -> Result<ParsedString, JsonImportError> {
477        let raw = parse_string_common(bytes, &mut parse_unicode_escape)?;
478        raw.view::<str>()
479            .map_err(|_| JsonImportError::Syntax("invalid utf-8".into()))
480    }
481
482    fn parse_number(&self, bytes: &mut Bytes) -> Result<Bytes, JsonImportError> {
483        parse_number_common(bytes)
484    }
485
486    /// Returns a [`Fragment`] describing every attribute and schema
487    /// encountered so far, suitable for committing alongside the data.
488    pub fn metadata(&mut self) -> Fragment {
489        let mut meta = Fragment::default();
490        meta += <Boolean as MetaDescribe>::describe();
491        meta += <F64 as MetaDescribe>::describe();
492        meta += <GenId as MetaDescribe>::describe();
493        meta += <Handle<LongString> as MetaDescribe>::describe();
494        for (key, attr) in self.bool_attrs.iter() {
495            meta += attr.describe();
496            if self.array_fields.contains(key) {
497                let attr_id = attr.id();
498                let entity = ExclusiveId::force_ref(&attr_id);
499                meta += entity! { &entity @ metadata::tag: metadata::KIND_MULTI };
500            }
501        }
502        for (key, attr) in self.num_attrs.iter() {
503            meta += attr.describe();
504            if self.array_fields.contains(key) {
505                let attr_id = attr.id();
506                let entity = ExclusiveId::force_ref(&attr_id);
507                meta += entity! { &entity @ metadata::tag: metadata::KIND_MULTI };
508            }
509        }
510        for (key, attr) in self.str_attrs.iter() {
511            meta += attr.describe();
512            if self.array_fields.contains(key) {
513                let attr_id = attr.id();
514                let entity = ExclusiveId::force_ref(&attr_id);
515                meta += entity! { &entity @ metadata::tag: metadata::KIND_MULTI };
516            }
517        }
518        for (key, attr) in self.genid_attrs.iter() {
519            meta += attr.describe();
520            if self.array_fields.contains(key) {
521                let attr_id = attr.id();
522                let entity = ExclusiveId::force_ref(&attr_id);
523                meta += entity! { &entity @ metadata::tag: metadata::KIND_MULTI };
524            }
525        }
526        meta
527    }
528
529    /// Resets the cached attribute mappings. Call between unrelated import
530    /// batches if you want field names to be re-derived.
531    pub fn clear(&mut self) {
532        self.bool_attrs.clear();
533        self.num_attrs.clear();
534        self.str_attrs.clear();
535        self.genid_attrs.clear();
536        self.array_fields.clear();
537    }
538}
539
540pub(crate) fn parse_unicode_escape(bytes: &mut Bytes) -> Result<Vec<u8>, JsonImportError> {
541    use winnow::error::InputError;
542    use winnow::token::take;
543    use winnow::Parser;
544
545    let mut grab = take::<_, _, InputError<Bytes>>(4usize);
546    let hex = grab
547        .parse_next(bytes)
548        .map_err(|_| JsonImportError::Syntax("unterminated unicode escape".into()))?;
549
550    let mut code: u32 = 0;
551    for h in hex.as_ref() {
552        code = (code << 4)
553            | match h {
554                b'0'..=b'9' => (h - b'0') as u32,
555                b'a'..=b'f' => (h - b'a' + 10) as u32,
556                b'A'..=b'F' => (h - b'A' + 10) as u32,
557                _ => return Err(JsonImportError::Syntax("invalid unicode escape".into())),
558            };
559    }
560
561    if let Some(ch) = char::from_u32(code) {
562        let mut buf = [0u8; 4];
563        let encoded = ch.encode_utf8(&mut buf);
564        Ok(encoded.as_bytes().to_vec())
565    } else {
566        Err(JsonImportError::Syntax("invalid unicode escape".into()))
567    }
568}
569
570pub(crate) fn parse_string_common(
571    bytes: &mut Bytes,
572    unicode_escape: &mut impl FnMut(&mut Bytes) -> Result<Vec<u8>, JsonImportError>,
573) -> Result<Bytes, JsonImportError> {
574    let consume_byte = |bytes: &mut Bytes, expected: u8| -> Result<(), JsonImportError> {
575        match bytes.pop_front() {
576            Some(b) if b == expected => Ok(()),
577            _ => Err(JsonImportError::Syntax("unexpected token".into())),
578        }
579    };
580
581    consume_byte(bytes, b'"')?;
582    {
583        use winnow::error::InputError;
584        use winnow::token::take_while;
585        use winnow::Parser;
586
587        let mut tentative = bytes.clone();
588        let mut segment = take_while::<_, _, InputError<Bytes>>(0.., |b: u8| {
589            b != b'"' && b != b'\\' && b != b'\n' && b != b'\r'
590        });
591
592        if let Ok(prefix) = segment.parse_next(&mut tentative) {
593            if tentative.peek_token() == Some(b'"') {
594                tentative.pop_front();
595                *bytes = tentative;
596                return Ok(prefix);
597            }
598        }
599    }
600
601    let mut out = Vec::new();
602    loop {
603        use winnow::error::InputError;
604        use winnow::token::take_while;
605        use winnow::Parser;
606
607        let mut segment = take_while::<_, _, InputError<Bytes>>(0.., |b: u8| {
608            b != b'\\' && b != b'"' && b != b'\n' && b != b'\r'
609        });
610        let chunk = segment
611            .parse_next(bytes)
612            .map_err(|_| JsonImportError::Syntax("unterminated string".into()))?;
613        out.extend_from_slice(chunk.as_ref());
614
615        match bytes.peek_token() {
616            Some(b'"') => {
617                bytes.pop_front();
618                return Ok(Bytes::from(out));
619            }
620            Some(b'\\') => {
621                bytes.pop_front();
622                let esc = bytes
623                    .pop_front()
624                    .ok_or_else(|| JsonImportError::Syntax("unterminated escape".into()))?;
625                match esc {
626                    b'"' => out.push(b'"'),
627                    b'\\' => out.push(b'\\'),
628                    b'/' => out.push(b'/'),
629                    b'b' => out.push(0x08),
630                    b'f' => out.push(0x0c),
631                    b'n' => out.push(b'\n'),
632                    b'r' => out.push(b'\r'),
633                    b't' => out.push(b'\t'),
634                    b'u' => out.extend_from_slice(&unicode_escape(bytes)?),
635                    _ => return Err(JsonImportError::Syntax("invalid escape sequence".into())),
636                }
637            }
638            Some(b'\n') | Some(b'\r') | None => {
639                return Err(JsonImportError::Syntax("unterminated string".into()))
640            }
641            _ => unreachable!("peek_token only yields bytes"),
642        }
643    }
644}
645
646pub(crate) fn parse_number_common(bytes: &mut Bytes) -> Result<Bytes, JsonImportError> {
647    use winnow::error::InputError;
648    use winnow::token::take_while;
649    use winnow::Parser;
650
651    let mut number = take_while::<_, _, InputError<Bytes>>(1.., |b: u8| {
652        b.is_ascii_digit() || b == b'-' || b == b'+' || b == b'.' || b == b'e' || b == b'E'
653    });
654
655    number
656        .parse_next(bytes)
657        .map_err(|_: InputError<Bytes>| JsonImportError::Syntax("expected number".into()))
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663    use crate::blob::MemoryBlobStore;
664    use crate::blob::IntoBlob;
665    use crate::prelude::Attribute;
666    
667    use anybytes::View;
668
669    #[test]
670    fn deterministic_imports_simple_object() {
671        let input = r#"{ "title": "Dune", "pages": 412 }"#;
672        let mut blobs = MemoryBlobStore::new();
673        let mut importer = JsonObjectImporter::<_>::new(&mut blobs, None);
674        let fragment = importer.import_blob(input.to_blob()).unwrap();
675        let roots = fragment.exports().collect::<Vec<_>>();
676        assert_eq!(roots.len(), 1);
677        assert_eq!(fragment.facts().len(), 2);
678        assert!(!importer.metadata().facts().is_empty());
679    }
680
681    fn extract_handle_raw(facts: &TribleSet, expected_attr: &str) -> RawInline {
682        use crate::blob::IntoBlob;
683        use crate::metadata::MetaDescribe;
684        let h: Inline<Handle<LongString>> = String::from(expected_attr)
685            .to_blob()
686            .get_handle();
687        let attr = Attribute::<Handle<LongString>>::from(crate::macros::entity! {
688            metadata::name:         h,
689            metadata::value_encoding: <Handle<LongString> as MetaDescribe>::id(),
690        })
691        .id();
692        let trible = facts
693            .iter()
694            .find(|t| *t.a() == attr)
695            .expect("missing string trible");
696        trible.v::<Handle<LongString>>().raw
697    }
698
699    fn read_text(blobs: &mut MemoryBlobStore, handle_raw: RawInline) -> String {
700        let entries: Vec<_> = blobs.reader().unwrap().into_iter().collect();
701        let (_, blob) = entries
702            .iter()
703            .find(|(h, _)| {
704                let h: Inline<Handle<LongString>> = (*h).transmute();
705                h.raw == handle_raw
706            })
707            .expect("handle not found in blob store");
708
709        let text: View<str> = blob
710            .clone()
711            .transmute::<LongString>()
712            .try_from_blob()
713            .expect("blob should decode as string");
714        text.as_ref().to_owned()
715    }
716
717    #[test]
718    fn parses_escaped_string() {
719        let input = r#"{ "text": "hello\nworld" }"#;
720        let mut blobs = MemoryBlobStore::new();
721        let mut importer = JsonObjectImporter::<_>::new(&mut blobs, None);
722        let fragment = importer.import_blob(input.to_blob()).unwrap();
723        let handle = extract_handle_raw(fragment.facts(), "text");
724        drop(importer);
725        let text = read_text(&mut blobs, handle);
726        assert_eq!(text, "hello\nworld");
727    }
728
729    #[test]
730    fn parses_unicode_escape() {
731        let input = r#"{ "text": "smile: \u263A" }"#;
732        let mut blobs = MemoryBlobStore::new();
733        let mut importer = JsonObjectImporter::<_>::new(&mut blobs, None);
734        let fragment = importer.import_blob(input.to_blob()).unwrap();
735        let handle = extract_handle_raw(fragment.facts(), "text");
736        drop(importer);
737        let text = read_text(&mut blobs, handle);
738        assert_eq!(text, "smile: \u{263A}");
739    }
740}