structio 0.8.0

High performance JSON and BEVE for Rust structs. No dependencies, no proc-macros, no intermediate representation.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
//! The key an error carries alongside its offset.
//!
//! `Error::index` answers "where", and for JSON read next to `display_with`
//! that is usually the whole answer: the caret sits under the byte that was
//! wrong. It is a poor answer for a member that is *not there*, whose offset
//! can only be the enclosing object's first byte, and it is a thin answer for
//! BEVE, where there is no text to draw a caret against at all. `Error::key`
//! is the key for those cases.
//!
//! What is asserted here: that it is the key the *document* uses rather than
//! the Rust field, that it is the first absent one in declaration order, that
//! codes with no key to give leave it `None` rather than guessing, that both
//! messages carry it, that it survives the document being dropped, that a
//! discarded read never carries one out with it, and that a hand-written
//! reader can set one of its own.

use std::collections::HashSet;

use structio::{
    Documents, ErrorCode, Matrix, MatrixLayout, RequireKeys, SkipUnknown, beve, from_beve,
    from_beve_with, from_str, from_str_with, json, to_beve, to_string,
};

/// Keys deliberately unlike their fields: one renamed, one converted by a rule.
/// A name reported from `KEYS` is therefore always distinguishable from one
/// reported from `stringify!`.
#[derive(Debug, Default, PartialEq)]
struct Accessor {
    byte_offset: u32,
    component_type: u32,
    #[allow(dead_code)]
    normalized: bool,
}

structio::object!(Accessor as "camelCase" {
    #[required] byte_offset,
    #[required] "type" => component_type,
    normalized,
});

fn accessor_json(members: &str) -> String {
    format!("{{{members}}}")
}

#[test]
fn the_key_is_the_one_the_document_uses_not_the_rust_field() {
    // `byte_offset` under a `camelCase` rule, and `component_type` under an
    // explicit key. Neither Rust spelling may appear.
    let e = from_str::<Accessor>(&accessor_json(r#""type":1"#)).unwrap_err();
    assert_eq!(e.code, ErrorCode::MissingKey);
    assert_eq!(e.key, Some("byteOffset"));

    let e = from_str::<Accessor>(&accessor_json(r#""byteOffset":0"#)).unwrap_err();
    assert_eq!(e.key, Some("type"));
}

#[test]
fn the_first_absent_field_in_declaration_order_is_the_one_named() {
    // Both are missing. The answer is stable, and it is the earlier one.
    let e = from_str::<Accessor>("{}").unwrap_err();
    assert_eq!(e.code, ErrorCode::MissingKey);
    assert_eq!(e.key, Some("byteOffset"));
}

#[test]
fn beve_names_the_field_the_same_way() {
    // The format that needs it most: an offset into a binary document is not
    // something a person can read the document against.
    let doc = to_beve(&Partial { component_type: 1 });
    let e = from_beve::<Accessor>(&doc).unwrap_err();
    assert_eq!(e.code, ErrorCode::MissingKey);
    assert_eq!(e.key, Some("byteOffset"));
}

/// A producer that writes only the second of `Accessor`'s two required keys,
/// so the BEVE document above is one a real writer could have made.
#[derive(Default)]
struct Partial {
    component_type: u32,
}
structio::object!(Partial { "type" => component_type });

#[test]
fn a_whole_policy_requiring_everything_names_a_field_too() {
    #[derive(Debug, Default, PartialEq)]
    struct Loose {
        a: u32,
        b: u32,
    }
    structio::object!(Loose { a, b });

    let e = from_str_with::<RequireKeys, Loose>(r#"{"a":1}"#).unwrap_err();
    assert_eq!(e.code, ErrorCode::MissingKey);
    assert_eq!(e.key, Some("b"));

    let doc = to_beve(&OnlyA { a: 1 });
    let e = from_beve_with::<RequireKeys, Loose>(&doc).unwrap_err();
    assert_eq!(e.key, Some("b"));
}

#[derive(Debug, Default)]
struct OnlyA {
    a: u32,
}
structio::object!(OnlyA { a });

#[test]
fn a_code_with_no_key_to_give_carries_none() {
    // The offset already points at the thing that was wrong, so a name here
    // would say the same thing twice. That includes the two codes it would be
    // most tempting to fill in.
    let unknown = r#"{"byteOffset":0,"type":1,"nope":2}"#;
    for e in [
        from_str::<Accessor>(unknown).unwrap_err(),
        from_str::<Accessor>(r#"{"byteOffset":x}"#).unwrap_err(),
        from_str::<Accessor>("[").unwrap_err(),
    ] {
        assert_eq!(e.key, None, "{e:?}");
    }

    // And nothing is lost by leaving it empty for an unknown key: the cursor
    // is already wound back to the key, so the offset points straight at it.
    let e = from_str::<Accessor>(unknown).unwrap_err();
    assert_eq!(e.code, ErrorCode::UnknownKey);
    assert!(unknown[e.index..].starts_with("nope"), "{}", e.index);

    #[derive(Debug, Default, PartialEq)]
    enum Shape {
        #[default]
        Circle,
    }
    structio::unit_enum!(Shape { Circle });

    let e = from_str::<Shape>(r#""Square""#).unwrap_err();
    assert_eq!(e.code, ErrorCode::UnknownVariant);
    assert_eq!(e.key, None);
}

#[test]
fn both_messages_carry_the_key() {
    let text = accessor_json(r#""type":1"#);
    let e = from_str::<Accessor>(&text).unwrap_err();

    let short = e.to_string();
    assert!(short.contains(r#""byteOffset""#), "{short}");
    assert!(short.starts_with("missing object key"), "{short}");

    let long = e.display_with(&text);
    assert!(long.contains(r#""byteOffset""#), "{long}");
    // The two describe the same failure, differing only in how they locate it.
    assert!(
        long.starts_with(r#"missing object key "byteOffset" at line"#),
        "{long}"
    );
    assert!(
        short.starts_with(r#"missing object key "byteOffset" at byte"#),
        "{short}"
    );
}

#[test]
fn an_error_without_a_key_reads_as_it_always_did() {
    // The field is additive: a code with no name renders exactly the string it
    // rendered before there was a field to render.
    let e = from_str::<Accessor>("[").unwrap_err();
    assert_eq!(e.to_string(), "expected '{' at byte 0");
    assert_eq!(
        e.display_with("["),
        "expected '{' at line 1, column 1\n[\n^"
    );
}

#[test]
fn the_key_outlives_the_document() {
    // The reason it is `&'static str`: an `Error` is `Copy`, carries no
    // lifetime, and stays useful after the buffer it indexes is gone. The
    // offset does not survive that, and the name is what is left.
    let e = {
        let owned = accessor_json(r#""type":1"#);
        from_str::<Accessor>(&owned).unwrap_err()
    };
    assert_eq!(e.key, Some("byteOffset"));
    assert!(e.to_string().contains("byteOffset"));
}

/// `StreamError` splits `Io` from `Parse` on the strength of these, so the
/// third field must not have cost either. A compile-time check, because that
/// is what the property is.
const _: fn() = || {
    fn assert<T: Copy + Eq + std::fmt::Debug + 'static>() {}
    assert::<structio::Error>();
};

#[test]
fn the_key_is_part_of_what_makes_two_errors_equal() {
    // Which is the half `Copy + Eq` does not settle: a derive that skipped the
    // field would still compile and would still be `Eq`.
    let named = from_str::<Accessor>("{}").unwrap_err();
    let nameless = structio::Error::new(named.code, named.index);
    assert_eq!(named.code, nameless.code);
    assert_eq!(named.index, nameless.index);
    assert_ne!(named, nameless, "the name has to count");
}

#[test]
fn a_streaming_read_carries_the_key_too() {
    let src = accessor_json(r#""type":1"#);
    let mut docs = Documents::lines(src.as_bytes());
    let e = docs.next_value::<Accessor>().unwrap().unwrap_err();
    let parse = e.as_parse().expect("a parse failure, not i/o");
    assert_eq!(parse.code, ErrorCode::MissingKey);
    assert_eq!(parse.key, Some("byteOffset"));
}

#[test]
fn a_matrix_names_the_member_it_lacks() {
    // The crate's own hand-written reader, which tracks its three keys itself
    // and so has to name them itself.
    let e = from_str_with::<RequireKeys, Matrix<u8>>(r#"{"layout":"row_major"}"#).unwrap_err();
    assert_eq!(e.code, ErrorCode::MissingKey);
    assert_eq!(e.key, Some("extents"));

    let full = Matrix::new(MatrixLayout::RowMajor, vec![2], vec![1u8, 2]).unwrap();
    let object_form = to_string(&full);
    let without_value = object_form.replace(r#","value":[1,2]"#, "");
    let e = from_str_with::<RequireKeys, Matrix<u8>>(&without_value).unwrap_err();
    assert_eq!(e.key, Some("value"));

    // And the same through BEVE's object form, which is the encoding a
    // producer without the matrix extension writes.
    let doc = to_beve(&HalfMatrix {
        layout: "row_major".into(),
        extents: vec![2],
    });
    let e = from_beve_with::<RequireKeys, Matrix<u8>>(&doc).unwrap_err();
    assert_eq!(e.code, ErrorCode::MissingKey);
    assert_eq!(e.key, Some("value"));
}

/// A matrix's object form with its data left out, which the extension form
/// cannot express: an extension carries all three parts by construction.
#[derive(Default)]
struct HalfMatrix {
    layout: String,
    extents: Vec<usize>,
}
structio::object!(HalfMatrix { layout, extents });

#[test]
fn a_hand_written_reader_can_name_its_own_key() {
    // `set_error_key` is the public half of what `read_object` does
    // internally, and the reason it is public: a reader written by hand has
    // the same problem and no other way to solve it.
    #[derive(Debug, Default, PartialEq)]
    struct Pair {
        lo: u32,
        hi: u32,
    }

    impl<'de> json::Read<'de> for Pair {
        fn read<O: structio::Options>(
            &mut self,
            p: &mut json::Parser<'de, O>,
        ) -> Result<(), ErrorCode> {
            let mut seen_hi = false;
            let open = p.position();
            p.read_map(|p, key| match key.as_str() {
                "lo" => self.lo.read(p),
                "hi" => {
                    seen_hi = true;
                    self.hi.read(p)
                }
                _ => p.skip_value(),
            })?;
            if !seen_hi {
                p.rewind(open);
                p.set_error_key("hi");
                return Err(ErrorCode::MissingKey);
            }
            Ok(())
        }
    }

    let e = from_str::<Pair>(r#"{"lo":1}"#).unwrap_err();
    assert_eq!(e.code, ErrorCode::MissingKey);
    assert_eq!(e.key, Some("hi"));
    assert_eq!(e.index, 0, "and located against the object, not its end");
}

#[test]
fn a_key_is_set_only_where_the_read_is_failing() {
    // Nothing clears the field, so the guarantee that a stale name never
    // attaches to a later failure rests on it being written only on a branch
    // that is returning `Err`. A successful read of an object that *could*
    // have failed, followed by a failure that names nothing, is where that
    // would show.
    #[derive(Debug, Default, PartialEq)]
    struct Outer {
        first: Inner,
        second: Inner,
    }
    #[derive(Debug, Default, PartialEq)]
    struct Inner {
        a: u32,
    }
    structio::object!(Outer { first, second });
    structio::object!(Inner {
        #[required]
        a
    });

    // `first` reads cleanly; `second` fails on something with no name.
    let e = from_str::<Outer>(r#"{"first":{"a":1},"second":{"a":x}}"#).unwrap_err();
    assert_eq!(e.code, ErrorCode::ExpectedNumber);
    assert_eq!(e.key, None);

    // And a genuinely missing `a` in the second still names it.
    let e = from_str::<Outer>(r#"{"first":{"a":1},"second":{}}"#).unwrap_err();
    assert_eq!(e.code, ErrorCode::MissingKey);
    assert_eq!(e.key, Some("a"));
}

#[test]
fn a_skipped_unknown_key_leaves_no_key_behind() {
    // `SkipUnknown` steps over a member rather than refusing it, so the reader
    // walks a value it will discard. The failure has to come *after* the skip
    // and have no name of its own, or the missing-key check at the end of the
    // object would overwrite whatever the skip left and the test would pass
    // either way.
    let e = from_str_with::<SkipUnknown, Accessor>(r#"{"nope":{"a":1},"byteOffset":0,"type":x}"#)
        .unwrap_err();
    assert_eq!(e.code, ErrorCode::ExpectedNumber);
    assert_eq!(e.key, None);
}

#[test]
fn a_read_that_is_discarded_carries_no_key_out_of_it() {
    // The one way a stale name could attach to an unrelated error, and the
    // reason `rewind` clears it. A reader that speculates on a *generated*
    // type never sets a name itself: `read_object` sets one behind its back,
    // so a rule saying "clear what you set" would be unfollowable. Winding
    // back is what clears it, which is the operation such a reader already has
    // to perform.
    #[derive(Debug, Default)]
    struct Inner {
        a: u32,
        b: u32,
    }
    structio::object!(Inner {
        #[required]
        a,
        #[required]
        b
    });

    #[derive(Debug, Default)]
    struct Speculative {
        held: u32,
    }

    impl<'de> json::Read<'de> for Speculative {
        fn read<O: structio::Options>(
            &mut self,
            p: &mut json::Parser<'de, O>,
        ) -> Result<(), ErrorCode> {
            let at = p.position();
            let mut probe = Inner::default();
            if json::Read::read(&mut probe, p).is_err() {
                // The whole point: this discards a `MissingKey` naming "b",
                // and has no idea a name was ever set.
                p.rewind(at);
                p.skip_value()?;
                return Err(ErrorCode::ExpectedNumber);
            }
            self.held = probe.a;
            Ok(())
        }
    }

    let e = from_str::<Speculative>(r#"{"a":1}"#).unwrap_err();
    assert_eq!(e.code, ErrorCode::ExpectedNumber);
    assert_eq!(e.key, None, "a discarded read left its key behind");
    assert_eq!(e.to_string(), "expected a number at byte 7");
}

#[test]
fn a_hand_written_beve_reader_can_name_its_own_key_too() {
    #[derive(Debug, Default, PartialEq)]
    struct Pair {
        lo: u32,
        hi: u32,
    }

    impl<'de> beve::Read<'de> for Pair {
        fn read<O: structio::Options>(
            &mut self,
            r: &mut beve::Reader<'de, O>,
        ) -> Result<(), ErrorCode> {
            let mut seen_hi = false;
            let open = r.position();
            r.read_map(|r, key| match key {
                beve::Key::Str("lo") => self.lo.read(r),
                beve::Key::Str("hi") => {
                    seen_hi = true;
                    self.hi.read(r)
                }
                _ => r.skip_value(),
            })?;
            if !seen_hi {
                r.rewind(open);
                r.set_error_key("hi");
                return Err(ErrorCode::MissingKey);
            }
            Ok(())
        }
    }

    let doc = to_beve(&OnlyA { a: 1 });
    let e = from_beve::<Pair>(&doc).unwrap_err();
    assert_eq!(e.code, ErrorCode::MissingKey);
    assert_eq!(e.key, Some("hi"));
    assert_eq!(e.index, 0, "and located against the object, not its end");
}

#[test]
fn a_pointer_read_carries_the_key_of_the_value_it_landed_on() {
    #[derive(Default)]
    struct Wrapper {
        inner: Partial,
    }
    structio::object!(Wrapper { inner });

    let doc = to_beve(&Wrapper {
        inner: Partial { component_type: 1 },
    });
    let e = structio::from_beve_at::<Accessor>(&doc, "/inner").unwrap_err();
    assert_eq!(e.code, ErrorCode::MissingKey);
    assert_eq!(e.key, Some("byteOffset"));
}

#[test]
fn a_second_document_does_not_inherit_the_first_s_key() {
    // Each value gets a fresh parser, so nothing carries across. Asserted
    // rather than assumed, because a reader reused across values would break
    // the whole scheme silently.
    let text = format!(
        "{}\n{{\"byteOffset\":0,\"type\":x}}\n",
        accessor_json(r#""type":1"#)
    );
    let mut docs = Documents::lines(text.as_bytes());

    let first = docs.next_value::<Accessor>().unwrap().unwrap_err();
    let first = first.as_parse().unwrap();
    assert_eq!(
        (first.code, first.key),
        (ErrorCode::MissingKey, Some("byteOffset"))
    );

    let second = docs.next_value::<Accessor>().unwrap().unwrap_err();
    let second = second.as_parse().unwrap();
    assert_eq!((second.code, second.key), (ErrorCode::ExpectedNumber, None));
}

// ---------------------------------------------------------------------------
// Reading the name back out of the document
// ---------------------------------------------------------------------------

/// A key spelled with an escape, so reading it back has to unescape.
const ESCAPED: &str = r#"{"a":1,"no\u0070e":2}"#;
/// The same, where unescaping also has to produce multi-byte UTF-8.
const ESCAPED_MULTIBYTE: &str = r#"{"a":1,"\u00e9t\u00e9":2}"#;

#[test]
fn key_in_reads_an_unknown_key_out_of_the_document() {
    let doc = r#"{"a":1,"nope":2}"#;
    let e = from_str::<OnlyA>(doc).unwrap_err();
    assert_eq!(e.code, ErrorCode::UnknownKey);
    assert_eq!(e.key, None);
    assert_eq!(e.key_in(doc).unwrap().as_str(), "nope");
}

#[test]
fn key_in_unescapes() {
    // The offset names a JSON string body, not a run of bytes, so the answer
    // is the key the document means rather than the text it spells it with.
    // This is also the only path that allocates.
    for (doc, want) in [(ESCAPED, "nope"), (ESCAPED_MULTIBYTE, "\u{e9}t\u{e9}")] {
        let e = from_str::<OnlyA>(doc).unwrap_err();
        assert_eq!(e.code, ErrorCode::UnknownKey, "{doc}");
        let key = e.key_in(doc).unwrap();
        assert_eq!(key.as_str(), want, "{doc}");
        assert!(matches!(key, structio::json::JsonStr::Owned(_)), "{doc}");
    }
}

#[test]
fn a_key_is_the_string_it_means_rather_than_the_variant_that_carries_it() {
    // The same key, once spelled plainly and once through an escape, so one
    // side comes back borrowed and the other owned.
    let plain = from_str::<OnlyA>(r#"{"a":1,"nope":2}"#).unwrap_err();
    let plain = plain.key_in(r#"{"a":1,"nope":2}"#).unwrap();
    let escaped = from_str::<OnlyA>(ESCAPED).unwrap_err();
    let escaped = escaped.key_in(ESCAPED).unwrap();

    assert!(matches!(plain, structio::json::JsonStr::Borrowed(_)));
    assert!(matches!(escaped, structio::json::JsonStr::Owned(_)));

    // Both documents named the same key. Comparing the variants rather than
    // the text would say otherwise.
    assert_eq!(plain, escaped);
    assert_eq!(plain.to_string(), "nope");
    assert_eq!(escaped.to_string(), "nope");

    // And they hash together, so a key read out of an error finds itself in a
    // set of the names a schema knows.
    let known: HashSet<structio::json::JsonStr<'_>> = HashSet::from([plain]);
    assert!(known.contains(&escaped));
}

#[test]
fn key_in_hands_back_a_static_key_unread() {
    // MissingKey names a field of the schema, and that name is not anywhere in
    // the document to be read out of it.
    let e = from_str::<Accessor>(r#"{"type":5}"#).unwrap_err();
    assert_eq!(e.code, ErrorCode::MissingKey);
    assert_eq!(e.key_in(r#"{"type":5}"#).unwrap().as_str(), "byteOffset");
}

#[test]
fn key_in_is_empty_for_a_code_about_no_key() {
    // Which code it is decides this, not whether a string happens to parse at
    // the offset. Each of these three lands somewhere that does parse as one,
    // and the name it would yield is nonsense: "" for a value's opening quote,
    // "[" for a bracket read as a bare body. The other two are here for the
    // shape of the contract, and reach the guard only after the code check.
    for doc in [
        r#"{"a":"nope"}"#,
        r#"{"a":1} "tail""#,
        r#"["a"]"#,
        r#"{"a":x}"#,
        r#"{"a":1,}"#,
    ] {
        let e = from_str::<OnlyA>(doc).unwrap_err();
        assert_ne!(e.code, ErrorCode::UnknownKey, "{doc:?}");
        assert!(e.key_in(doc).is_none(), "{doc:?} gave {e:?}");
    }
}

#[test]
fn key_in_survives_the_wrong_document() {
    // A diagnostic helper handed a buffer that did not produce the error must
    // not panic, including when the offset lands inside a character.
    let e = from_str::<OnlyA>(r#"{"a":1,"nope":2}"#).unwrap_err();
    assert_eq!(e.index, 8);
    for other in [
        "",
        "{}",
        "x",
        "\u{1f600}\u{1f600}", // exactly 8 bytes: the offset is the end
        "xxxxxx\u{1f600}",    // 8 lands inside the last character
        "\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}", // 10 bytes, every other one a boundary
    ] {
        let _ = e.key_in(other);
    }
    // Rounding down, as display_with does, would be wrong here rather than
    // merely imprecise: it would read a name out of a character's tail.
    assert!(e.key_in("xxxxxx\u{1f600}").is_none());
}

#[test]
fn read_map_located_agrees_with_the_generated_reader() {
    use structio::json::Parser;

    // The two ways to meet a key have to mean the same byte by it, or an
    // offset minted in a hand-written reader would name something else once
    // it reached an Error.
    for doc in [
        r#"{"a":1,"nope":2}"#,
        r#"{ "a" : 1 , "nope" : 2 }"#,
        "{\n  \"a\": 1,\n  \"nope\": 2\n}",
        r#"{"a":1,"":2}"#,
        "{\"a\":1,\"\u{1f600}\":2}",
        ESCAPED,
    ] {
        let from_schema = from_str::<OnlyA>(doc).unwrap_err();
        assert_eq!(from_schema.code, ErrorCode::UnknownKey);

        let mut by_hand = None;
        Parser::new(doc)
            .read_map_located(|p, key, at| {
                if key.as_str() != "a" {
                    by_hand = Some((key.into_string(), at));
                }
                p.skip_value()
            })
            .unwrap();

        let (name, at) = by_hand.unwrap();
        assert_eq!(at, from_schema.index, "{doc:?}");
        assert_eq!(name, from_schema.key_in(doc).unwrap().as_str(), "{doc:?}");
    }
}

#[test]
fn beve_read_map_located_agrees_with_the_generated_reader() {
    use std::collections::BTreeMap;
    use structio::beve::{Key, Reader};
    use structio::{from_beve, to_beve};

    #[derive(Debug, Default)]
    struct JustA {
        a: u32,
    }
    structio::object!(JustA { a });

    // Written as a map so the document holds a key no schema claims.
    let doc = to_beve(&BTreeMap::from([
        ("a".to_string(), 1u32),
        ("nope".to_string(), 2),
    ]));

    let from_schema = from_beve::<JustA>(&doc).unwrap_err();
    assert_eq!(from_schema.code, ErrorCode::UnknownKey);

    let mut by_hand = None;
    Reader::new(&doc)
        .read_map_located(|r, key, at| {
            if let Key::Str(name) = key
                && name != "a"
            {
                by_hand = Some((name.to_string(), at));
            }
            r.skip_value()
        })
        .unwrap();

    let (name, at) = by_hand.unwrap();
    assert_eq!(name, "nope");
    // The offset names the key's text, past the length prefix, exactly as the
    // generated reader's does.
    assert_eq!(at, from_schema.index);
    assert_eq!(&doc[at..at + name.len()], name.as_bytes());
}

#[test]
fn beve_read_map_located_locates_integer_keys() {
    use std::collections::BTreeMap;
    use structio::beve::{Key, Reader};
    use structio::to_beve;

    // An integer key has no length prefix, so the offset is the first of its
    // little-endian bytes. Reading them back is the check that it is.
    let doc = to_beve(&BTreeMap::from([(7u32, 1u8), (9, 2)]));

    let mut seen = Vec::new();
    Reader::new(&doc)
        .read_map_located(|r, key, at| {
            if let Key::Unsigned(n) = key {
                seen.push((n, at));
            }
            r.skip_value()
        })
        .unwrap();

    assert_eq!(seen.len(), 2);
    for (n, at) in seen {
        let width = 4; // u32 keys
        let mut bytes = [0u8; 16];
        bytes[..width].copy_from_slice(&doc[at..at + width]);
        assert_eq!(u128::from_le_bytes(bytes), n);
    }
}

#[test]
fn key_in_reads_an_unknown_variant_too() {
    // The other half of what key_in promises, and the half a code-filter
    // mutation used to get away with.
    #[derive(Debug, Default, PartialEq)]
    enum Shape {
        #[default]
        Circle,
    }
    structio::unit_enum!(Shape { Circle });

    #[derive(Debug, Default, PartialEq)]
    enum Payload {
        #[default]
        Empty,
        Circle(u32),
    }
    structio::tagged_enum!(Payload { Empty, Circle(_) });

    let doc = r#""Square""#;
    let e = from_str::<Shape>(doc).unwrap_err();
    assert_eq!(e.code, ErrorCode::UnknownVariant);
    assert_eq!(e.key_in(doc).unwrap().as_str(), "Square");

    let doc = r#"{"Square":1}"#;
    let e = from_str::<Payload>(doc).unwrap_err();
    assert_eq!(e.code, ErrorCode::UnknownVariant);
    assert_eq!(e.key_in(doc).unwrap().as_str(), "Square");
}

#[test]
fn key_in_will_not_read_a_name_off_a_value() {
    // A reader that refuses after the colon would report the value, and the
    // text there can parse as a string body and yield a plausible name. The
    // quote before the offset is what rules that out.
    let doc = r#"{"a":"nope"}"#;
    // Offset 5 is the value's opening quote, which is what a reader refusing
    // after the colon reports. Without the check it reads as the empty key.
    let forged = structio::Error::new(ErrorCode::UnknownKey, 5);
    assert!(
        forged.key_in(doc).is_none(),
        "read {:?} off a value",
        forged.key_in(doc).map(|k| k.as_str().to_string())
    );

    // And offset 0 is never a key: the shallowest one follows `{"`.
    assert!(
        structio::Error::new(ErrorCode::UnknownKey, 0)
            .key_in(doc)
            .is_none()
    );

    // The limit of the check, pinned so it is not mistaken for a proof: offset
    // 6 is the text inside the value, and a quote precedes that too. Nothing
    // local distinguishes it from a key, so it is read as one. No reader in
    // the crate reports there, which is why a cheap check is the right one.
    assert_eq!(
        structio::Error::new(ErrorCode::UnknownKey, 6)
            .key_in(doc)
            .unwrap()
            .as_str(),
        "nope"
    );
}

#[test]
fn matrix_names_the_key_it_refused() {
    // Matrix reads its members by hand through a map callback, which runs
    // after the colon. It winds back, so its UnknownKey means what every other
    // one means and key_in can read it.
    use structio::Matrix;

    for doc in [
        r#"{"bogus":1,"layout":"layout_right","extents":[1,1],"value":[1.0]}"#,
        r#"{"layout":"layout_right","extents":[1,1],"value":[1.0],"bogus":"hello"}"#,
    ] {
        let e = from_str::<Matrix<f64>>(doc).unwrap_err();
        assert_eq!(e.code, ErrorCode::UnknownKey, "{doc}");
        assert_eq!(e.key_in(doc).unwrap().as_str(), "bogus", "{doc}");
        assert!(doc[e.index..].starts_with("bogus"), "{doc}");
    }
}

#[test]
fn beve_matrix_names_the_key_it_refused() {
    use std::collections::BTreeMap;
    use structio::Matrix;

    // The object form, with a member the shape does not name.
    let doc = structio::to_beve(&BTreeMap::from([("bogus".to_string(), 1u8)]));

    let e = structio::from_beve::<Matrix<f64>>(&doc).unwrap_err();
    assert_eq!(e.code, ErrorCode::UnknownKey);
    assert_eq!(&doc[e.index..e.index + 5], b"bogus");
}

#[test]
fn beve_read_map_located_locates_signed_keys() {
    use std::collections::BTreeMap;
    use structio::beve::{Key, Reader};
    use structio::to_beve;

    // The signed arm takes its position on a different line from the unsigned
    // one, so it needs its own check.
    for doc in [
        to_beve(&BTreeMap::from([(-7i8, 1u8), (9, 2)])),
        to_beve(&BTreeMap::from([(-7i16, 1u8), (9, 2)])),
        to_beve(&BTreeMap::from([(-7i32, 1u8), (9, 2)])),
        to_beve(&BTreeMap::from([(-7i64, 1u8), (9, 2)])),
    ] {
        let mut seen = Vec::new();
        Reader::new(&doc)
            .read_map_located(|r, key, at| {
                if let Key::Signed(n) = key {
                    seen.push((n, at));
                }
                r.skip_value()
            })
            .unwrap();
        assert_eq!(seen.len(), 2, "{doc:?}");
        for (n, at) in seen {
            // The width is whatever the encoder chose; reading it back at the
            // reported offset is what proves the offset is the key's first byte.
            let width = (doc.len() - at).min(16);
            let mut bytes = [0u8; 16];
            bytes[..width].copy_from_slice(&doc[at..at + width]);
            let raw = i128::from_le_bytes(bytes);
            assert_eq!(raw & 0xff, n & 0xff, "{n} at {at}");
        }
    }
}