1use 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#[derive(Debug)]
36pub enum JsonImportError {
37 PrimitiveRoot,
40 EncodeString {
42 field: String,
44 source: EncodeError,
46 },
47 EncodeNumber {
49 field: String,
51 source: EncodeError,
53 },
54 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#[derive(Debug)]
86pub struct EncodeError(Box<dyn std::error::Error + Send + Sync + 'static>);
87
88impl EncodeError {
89 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 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
129pub 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 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 pub fn import_str(&mut self, input: &str) -> Result<Fragment, JsonImportError> {
235 self.import_blob(input.to_owned().to_blob())
236 }
237
238 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 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 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}