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,
};
#[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() {
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() {
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() {
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"));
}
#[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() {
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:?}");
}
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}");
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() {
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() {
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"));
}
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() {
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() {
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"));
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"));
}
#[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() {
#[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() {
#[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
});
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);
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() {
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() {
#[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() {
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() {
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));
}
const ESCAPED: &str = r#"{"a":1,"no\u0070e":2}"#;
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() {
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() {
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(_)));
assert_eq!(plain, escaped);
assert_eq!(plain.to_string(), "nope");
assert_eq!(escaped.to_string(), "nope");
let known: HashSet<structio::json::JsonStr<'_>> = HashSet::from([plain]);
assert!(known.contains(&escaped));
}
#[test]
fn key_in_hands_back_a_static_key_unread() {
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() {
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() {
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}", "xxxxxx\u{1f600}", "\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}", ] {
let _ = e.key_in(other);
}
assert!(e.key_in("xxxxxx\u{1f600}").is_none());
}
#[test]
fn read_map_located_agrees_with_the_generated_reader() {
use structio::json::Parser;
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 });
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");
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;
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; 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() {
#[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() {
let doc = r#"{"a":"nope"}"#;
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())
);
assert!(
structio::Error::new(ErrorCode::UnknownKey, 0)
.key_in(doc)
.is_none()
);
assert_eq!(
structio::Error::new(ErrorCode::UnknownKey, 6)
.key_in(doc)
.unwrap()
.as_str(),
"nope"
);
}
#[test]
fn matrix_names_the_key_it_refused() {
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;
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;
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 {
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}");
}
}
}