Skip to main content

triblespace_core/import/
json_tree.rs

1//! Lossless JSON importer that preserves structure and ordering.
2//!
3//! Every JSON value becomes a node tagged with a kind. Objects and arrays are
4//! expressed via explicit entry entities that record field names or indices.
5//! Entity ids are content-addressed so identical subtrees deduplicate across
6//! imports.
7
8
9use anybytes::{Bytes, View};
10use winnow::stream::Stream;
11
12use crate::blob::encodings::longstring::LongString;
13use crate::blob::Blob;
14use crate::blob::IntoBlob;
15use crate::id::{ExclusiveId, Id, RawId, ID_LEN};
16use crate::macros::{entity, id_hex};
17use crate::metadata;
18use crate::repo::BlobStore;
19use crate::trible::Fragment;
20use crate::trible::TribleSet;
21use crate::inline::encodings::boolean::Boolean;
22use crate::inline::encodings::genid::GenId;
23use crate::inline::encodings::hash::{Blake3, Handle};
24use crate::inline::encodings::iu256::U256BE;
25use crate::inline::Inline;
26use triblespace_core_macros::attributes;
27
28use crate::import::json::{
29    parse_number_common, parse_string_common, parse_unicode_escape, EncodeError, JsonImportError,
30};
31
32type ParsedString = View<str>;
33
34attributes! {
35    /// Node kind tag (one of the `kind_*` constants).
36    "D78B9D5A96029FDBBB327E377418AF51" as pub kind: GenId;
37    /// String content stored as a LongString blob.
38    "40BC51924FD5D2058A48D1FA6073F871" as pub string: Handle<LongString>;
39    /// Raw decimal number string (preserves precision).
40    "428E02672FFD0D010D95AE641ADE1730" as pub number_raw: Handle<LongString>;
41    /// Boolean value.
42    "6F43FC771207574BF4CC58D3080C313C" as pub boolean: Boolean;
43    /// Parent entity of an object field entry.
44    "97A4ACD83EC9EA29EE7E487BB058C437" as pub field_parent: GenId;
45    /// Field name stored as a LongString blob.
46    "2B9FCF2A60C9B05FADDA9F022762B822" as pub field_name: Handle<LongString>;
47    /// Ordinal position of a field within its parent object.
48    "38C7B1CDEA580DE70A520B2C8CBC4F14" as pub field_index: U256BE;
49    /// Inline entity referenced by an object field entry.
50    "6E6CA175F925B6AA0844D357B409F15A" as pub field_value: GenId;
51    /// Parent entity of an array entry.
52    "B49E6499D0A2CF5DD9A1E72D9D047747" as pub array_parent: GenId;
53    /// Zero-based index of an array element.
54    "D5DA41A093BD0DE490925126D1150B57" as pub array_index: U256BE;
55    /// Inline entity referenced by an array entry.
56    "33535F41827B476B1EC0CACECE9BEED0" as pub array_value: GenId;
57}
58
59/// JSON object node.
60#[allow(non_upper_case_globals)]
61pub const kind_object: Id = id_hex!("64D8981414502BF750387C617F1F9D09");
62/// JSON array node.
63#[allow(non_upper_case_globals)]
64pub const kind_array: Id = id_hex!("5DC7096A184E658C8E16C54EB207C386");
65/// JSON string node.
66#[allow(non_upper_case_globals)]
67pub const kind_string: Id = id_hex!("58A5EAC244801C5E26AD9178C784781A");
68/// JSON number node.
69#[allow(non_upper_case_globals)]
70pub const kind_number: Id = id_hex!("711555ADF72B9499E6A7F68E0BD3B4B8");
71/// JSON boolean node.
72#[allow(non_upper_case_globals)]
73pub const kind_bool: Id = id_hex!("7D3079C5E20658B6CA5F54771B5D0D30");
74/// JSON null node.
75#[allow(non_upper_case_globals)]
76pub const kind_null: Id = id_hex!("FC1DCF98A3A8418D6090EBD367CFFD7A");
77/// Object field entry.
78#[allow(non_upper_case_globals)]
79pub const kind_field: Id = id_hex!("890FC1F34B9FAD18F93E6EDF1B69A1A2");
80/// Array entry.
81#[allow(non_upper_case_globals)]
82pub const kind_array_entry: Id = id_hex!("EB325EABEA8C35DE7E5D700A5EF9207B");
83
84/// Returns a [`Fragment`] describing the lossless JSON tree schema —
85/// all node kinds, attribute definitions, and value/blob encoding metadata.
86pub fn build_json_tree_metadata() -> Fragment {
87    // The macro-generated `describe()` for this module's attributes!{}
88    // block emits each declared attribute's identity, schema spread,
89    // and a usage entity rooted at
90    // (metadata::attribute, metadata::source_module) with the rust
91    // identifier as `metadata::name`. The `source_module` field
92    // disambiguates "this is the JSON tree schema's usage of `kind`"
93    // from any other crate's usage of the same attribute id, so we no
94    // longer need a separate `json.kind` rename.
95    let mut metadata = describe();
96
97    metadata += describe_kind(kind_object, "json.kind.object", "JSON object node.");
98    metadata += describe_kind(kind_array, "json.kind.array", "JSON array node.");
99    metadata += describe_kind(kind_string, "json.kind.string", "JSON string node.");
100    metadata += describe_kind(kind_number, "json.kind.number", "JSON number node.");
101    metadata += describe_kind(kind_bool, "json.kind.bool", "JSON boolean node.");
102    metadata += describe_kind(kind_null, "json.kind.null", "JSON null node.");
103    metadata += describe_kind(
104        kind_field,
105        "json.kind.field",
106        "JSON object field entry.",
107    );
108    metadata += describe_kind(
109        kind_array_entry,
110        "json.kind.array_entry",
111        "JSON array entry.",
112    );
113
114    metadata
115}
116
117fn describe_kind(kind_id: Id, name: &str, description: &str) -> Fragment {
118    entity! { ExclusiveId::force_ref(&kind_id) @
119        metadata::name:        name.to_owned(),
120        metadata::description: description.to_owned(),
121    }
122}
123
124#[derive(Clone)]
125struct FieldEntry {
126    name: View<str>,
127    name_handle: Inline<Handle<LongString>>,
128    index: u64,
129    value: Id,
130}
131
132#[derive(Clone)]
133struct ArrayEntry {
134    index: u64,
135    value: Id,
136}
137
138/// Lossless JSON importer that preserves ordering and encodes explicit entry nodes.
139///
140/// This importer encodes JSON values as an explicit node/entry graph (a JSON AST),
141/// using content-addressed ids so identical subtrees deduplicate across imports.
142pub struct JsonTreeImporter<'a, Store>
143where
144    Store: BlobStore,
145{
146    store: &'a mut Store,
147    id_salt: Option<[u8; 32]>,
148}
149
150impl<'a, Store> JsonTreeImporter<'a, Store>
151where
152    Store: BlobStore,
153{
154    /// Creates a new lossless importer backed by `store`. Pass an optional
155    /// 32-byte salt to namespace the content-addressed entity ids.
156    pub fn new(store: &'a mut Store, id_salt: Option<[u8; 32]>) -> Self {
157        Self {
158            store,
159            id_salt,
160        }
161    }
162
163    /// Imports a JSON string. Convenience wrapper around [`import_blob`](Self::import_blob).
164    pub fn import_str(&mut self, input: &str) -> Result<Fragment, JsonImportError> {
165        self.import_blob(input.to_owned().to_blob())
166    }
167
168    /// Imports a JSON document from a [`LongString`] blob, returning a
169    /// [`Fragment`] rooted at the document's top-level node.
170    pub fn import_blob(&mut self, blob: Blob<LongString>) -> Result<Fragment, JsonImportError> {
171        let mut data = TribleSet::new();
172        let mut bytes = blob.bytes.clone();
173        self.skip_ws(&mut bytes);
174        let root = self.parse_value(&mut bytes, &mut data)?;
175        self.skip_ws(&mut bytes);
176        if bytes.peek_token().is_some() {
177            return Err(JsonImportError::Syntax("trailing tokens".into()));
178        }
179        Ok(Fragment::rooted(root, data))
180    }
181
182    /// Returns schema metadata for the lossless JSON tree format.
183    /// Delegates to [`build_json_tree_metadata`].
184    pub fn metadata(&self) -> Fragment {
185        build_json_tree_metadata()
186    }
187
188    fn parse_value(
189        &mut self,
190        bytes: &mut Bytes,
191        data: &mut TribleSet,
192    ) -> Result<Id, JsonImportError> {
193        match bytes.peek_token() {
194            Some(b'n') => {
195                self.consume_literal(bytes, b"null")?;
196                let id = self.hash_tagged(b"null", &[]);
197                *data += entity! { ExclusiveId::force_ref(&id) @
198                    kind: kind_null,
199                };
200                Ok(id)
201            }
202            Some(b't') => {
203                self.consume_literal(bytes, b"true")?;
204                let id = self.hash_tagged(b"bool", &[b"true"]);
205                *data += entity! { ExclusiveId::force_ref(&id) @
206                    kind: kind_bool,
207                    boolean: true,
208                };
209                Ok(id)
210            }
211            Some(b'f') => {
212                self.consume_literal(bytes, b"false")?;
213                let id = self.hash_tagged(b"bool", &[b"false"]);
214                *data += entity! { ExclusiveId::force_ref(&id) @
215                    kind: kind_bool,
216                    boolean: false,
217                };
218                Ok(id)
219            }
220            Some(b'"') => {
221                let text = self.parse_string(bytes)?;
222                let id = self.hash_tagged(b"string", &[text.as_ref().as_bytes()]);
223                let handle = self
224                    .store
225                    .put(text)
226                    .map_err(|err| JsonImportError::EncodeString {
227                        field: "string".to_string(),
228                        source: EncodeError::from_error(err),
229                    })?;
230                *data += entity! { ExclusiveId::force_ref(&id) @
231                    kind: kind_string,
232                    string: handle,
233                };
234                Ok(id)
235            }
236            Some(b'{') => self.parse_object(bytes, data),
237            Some(b'[') => self.parse_array(bytes, data),
238            _ => {
239                let number = self.parse_number(bytes)?;
240                let number_view = number
241                    .view::<str>()
242                    .map_err(|_| JsonImportError::Syntax("invalid number".into()))?;
243                let id = self.hash_tagged(b"number", &[number_view.as_ref().as_bytes()]);
244                let handle =
245                    self.store
246                        .put(number_view)
247                        .map_err(|err| JsonImportError::EncodeNumber {
248                            field: "number".to_string(),
249                            source: EncodeError::from_error(err),
250                        })?;
251                *data += entity! { ExclusiveId::force_ref(&id) @
252                    kind: kind_number,
253                    number_raw: handle,
254                };
255                Ok(id)
256            }
257        }
258    }
259
260    fn parse_object(
261        &mut self,
262        bytes: &mut Bytes,
263        data: &mut TribleSet,
264    ) -> Result<Id, JsonImportError> {
265        self.consume_byte(bytes, b'{')?;
266        self.skip_ws(bytes);
267
268        let mut fields: Vec<FieldEntry> = Vec::new();
269        if bytes.peek_token() == Some(b'}') {
270            self.consume_byte(bytes, b'}')?;
271        } else {
272            let mut index: u64 = 0;
273            loop {
274                let name = self.parse_string(bytes)?;
275                self.skip_ws(bytes);
276                self.consume_byte(bytes, b':')?;
277                self.skip_ws(bytes);
278                let value = self.parse_value(bytes, data)?;
279                let name_handle =
280                    self.store
281                        .put(name.clone())
282                        .map_err(|err| JsonImportError::EncodeString {
283                            field: "field".to_string(),
284                            source: EncodeError::from_error(err),
285                        })?;
286                fields.push(FieldEntry {
287                    name,
288                    name_handle,
289                    index,
290                    value,
291                });
292                index = index.saturating_add(1);
293
294                self.skip_ws(bytes);
295                match bytes.peek_token() {
296                    Some(b',') => {
297                        self.consume_byte(bytes, b',')?;
298                        self.skip_ws(bytes);
299                    }
300                    Some(b'}') => {
301                        self.consume_byte(bytes, b'}')?;
302                        break;
303                    }
304                    _ => return Err(JsonImportError::Syntax("unexpected token".into())),
305                }
306            }
307        }
308
309        let object_id = self.hash_object(&fields);
310        *data += entity! { ExclusiveId::force_ref(&object_id) @
311            kind: kind_object,
312        };
313
314        for field in fields {
315            let entry_id = self.hash_field_entry(&object_id, &field);
316            *data += entity! { ExclusiveId::force_ref(&entry_id) @
317                kind: kind_field,
318                field_parent: object_id,
319                field_name: field.name_handle,
320                field_index: field.index,
321                field_value: field.value,
322            };
323        }
324
325        Ok(object_id)
326    }
327
328    fn parse_array(
329        &mut self,
330        bytes: &mut Bytes,
331        data: &mut TribleSet,
332    ) -> Result<Id, JsonImportError> {
333        self.consume_byte(bytes, b'[')?;
334        self.skip_ws(bytes);
335
336        let mut entries: Vec<ArrayEntry> = Vec::new();
337        if bytes.peek_token() == Some(b']') {
338            self.consume_byte(bytes, b']')?;
339        } else {
340            let mut index: u64 = 0;
341            loop {
342                let value = self.parse_value(bytes, data)?;
343                entries.push(ArrayEntry { index, value });
344                index = index.saturating_add(1);
345
346                self.skip_ws(bytes);
347                match bytes.peek_token() {
348                    Some(b',') => {
349                        self.consume_byte(bytes, b',')?;
350                        self.skip_ws(bytes);
351                    }
352                    Some(b']') => {
353                        self.consume_byte(bytes, b']')?;
354                        break;
355                    }
356                    _ => return Err(JsonImportError::Syntax("unexpected token".into())),
357                }
358            }
359        }
360
361        let array_id = self.hash_array(&entries);
362        *data += entity! { ExclusiveId::force_ref(&array_id) @
363            kind: kind_array,
364        };
365
366        for entry in entries {
367            let entry_id = self.hash_array_entry(&array_id, &entry);
368            *data += entity! { ExclusiveId::force_ref(&entry_id) @
369                kind: kind_array_entry,
370                array_parent: array_id,
371                array_index: entry.index,
372                array_value: entry.value,
373            };
374        }
375
376        Ok(array_id)
377    }
378
379    fn hash_object(&self, fields: &[FieldEntry]) -> Id {
380        let mut hasher = self.seeded_hasher();
381        hash_chunk(&mut hasher, b"object");
382        for field in fields {
383            let index_bytes = field.index.to_be_bytes();
384            hash_chunk(&mut hasher, field.name.as_ref().as_bytes());
385            hash_chunk(&mut hasher, &index_bytes);
386            hash_chunk(&mut hasher, field.value.as_ref());
387        }
388        self.finish_hash(hasher)
389    }
390
391    fn hash_array(&self, entries: &[ArrayEntry]) -> Id {
392        let mut hasher = self.seeded_hasher();
393        hash_chunk(&mut hasher, b"array");
394        for entry in entries {
395            let index_bytes = entry.index.to_be_bytes();
396            hash_chunk(&mut hasher, &index_bytes);
397            hash_chunk(&mut hasher, entry.value.as_ref());
398        }
399        self.finish_hash(hasher)
400    }
401
402    fn hash_field_entry(&self, parent: &Id, entry: &FieldEntry) -> Id {
403        let mut hasher = self.seeded_hasher();
404        hash_chunk(&mut hasher, b"field");
405        let index_bytes = entry.index.to_be_bytes();
406        hash_chunk(&mut hasher, parent.as_ref());
407        hash_chunk(&mut hasher, entry.name.as_ref().as_bytes());
408        hash_chunk(&mut hasher, &index_bytes);
409        hash_chunk(&mut hasher, entry.value.as_ref());
410        self.finish_hash(hasher)
411    }
412
413    fn hash_array_entry(&self, parent: &Id, entry: &ArrayEntry) -> Id {
414        let mut hasher = self.seeded_hasher();
415        hash_chunk(&mut hasher, b"array_entry");
416        let index_bytes = entry.index.to_be_bytes();
417        hash_chunk(&mut hasher, parent.as_ref());
418        hash_chunk(&mut hasher, &index_bytes);
419        hash_chunk(&mut hasher, entry.value.as_ref());
420        self.finish_hash(hasher)
421    }
422
423    fn hash_tagged(&self, tag: &[u8], parts: &[&[u8]]) -> Id {
424        let mut hasher = self.seeded_hasher();
425        hash_chunk(&mut hasher, tag);
426        for part in parts {
427            hash_chunk(&mut hasher, part);
428        }
429        self.finish_hash(hasher)
430    }
431
432    fn seeded_hasher(&self) -> Blake3 {
433        let mut hasher = Blake3::new();
434        if let Some(salt) = self.id_salt {
435            hasher.update(salt.as_ref());
436        }
437        hasher
438    }
439
440    fn finish_hash(&self, hasher: Blake3) -> Id {
441        let digest = hasher.finalize();
442        id_from_digest(digest.as_ref())
443    }
444
445    fn skip_ws(&self, bytes: &mut Bytes) {
446        while matches!(bytes.peek_token(), Some(b) if b.is_ascii_whitespace()) {
447            bytes.pop_front();
448        }
449    }
450
451    fn consume_byte(&self, bytes: &mut Bytes, expected: u8) -> Result<(), JsonImportError> {
452        match bytes.pop_front() {
453            Some(b) if b == expected => Ok(()),
454            _ => Err(JsonImportError::Syntax("unexpected token".into())),
455        }
456    }
457
458    fn consume_literal(&self, bytes: &mut Bytes, literal: &[u8]) -> Result<(), JsonImportError> {
459        for expected in literal {
460            self.consume_byte(bytes, *expected)?;
461        }
462        Ok(())
463    }
464
465    fn parse_string(&self, bytes: &mut Bytes) -> Result<ParsedString, JsonImportError> {
466        let raw = parse_string_common(bytes, &mut parse_unicode_escape)?;
467        raw.view::<str>()
468            .map_err(|_| JsonImportError::Syntax("invalid utf-8".into()))
469    }
470
471    fn parse_number(&self, bytes: &mut Bytes) -> Result<Bytes, JsonImportError> {
472        parse_number_common(bytes)
473    }
474}
475
476fn hash_chunk(hasher: &mut Blake3, bytes: &[u8]) {
477    let len = (bytes.len() as u64).to_be_bytes();
478    hasher.update(&len);
479    hasher.update(bytes);
480}
481
482fn id_from_digest(digest: &[u8]) -> Id {
483    let mut raw: RawId = [0u8; ID_LEN];
484    raw.copy_from_slice(&digest[digest.len() - ID_LEN..]);
485    if raw == [0; ID_LEN] {
486        raw[0] = 1;
487    }
488    Id::new(raw).unwrap_or_else(|| unsafe { Id::force(raw) })
489}
490
491#[cfg(test)]
492mod tests {
493    use super::{kind_array_entry, JsonTreeImporter};
494    use crate::blob::MemoryBlobStore;
495    use crate::blob::IntoBlob;
496    use crate::id::Id;
497    use crate::macros::{find, pattern};
498    
499
500    #[test]
501    fn lossless_ids_are_content_based() {
502        let input = r#"{ "a": [1, 2] }"#;
503        let mut blobs = MemoryBlobStore::new();
504        let mut importer = JsonTreeImporter::<_>::new(&mut blobs, None);
505        let root = importer
506            .import_blob(input.to_blob())
507            .unwrap()
508            .root()
509            .expect("import_blob returns a rooted fragment");
510        drop(importer);
511        let mut other = JsonTreeImporter::<_>::new(&mut blobs, None);
512        let other_root = other
513            .import_blob(input.to_blob())
514            .unwrap()
515            .root()
516            .expect("import_blob returns a rooted fragment");
517        assert_eq!(root, other_root);
518    }
519
520    #[test]
521    fn lossless_preserves_array_order() {
522        let input = r#"[1, 2]"#;
523        let mut blobs = MemoryBlobStore::new();
524        let mut importer = JsonTreeImporter::<_>::new(&mut blobs, None);
525        let fragment = importer.import_blob(input.to_blob()).unwrap();
526        let root = fragment
527            .root()
528            .expect("import_blob returns a rooted fragment");
529        let catalog = fragment.facts();
530        let mut entries = find!(
531            (index: ethnum::U256, value: Id),
532            pattern!(catalog, [{
533                _?entry @
534                super::kind: kind_array_entry,
535                super::array_parent: root,
536                super::array_index: ?index,
537                super::array_value: ?value,
538            }])
539        )
540        .collect::<Vec<_>>();
541        entries.sort_by_key(|(index, _)| *index);
542        assert_eq!(entries.len(), 2);
543        assert_eq!(entries[0].0, ethnum::U256::new(0));
544        assert_eq!(entries[1].0, ethnum::U256::new(1));
545    }
546}