use std::collections::BTreeMap;
use structio::{
Documents, ErrorCode, Keys, RequireKeys, Same, SkipNull, SkipUnknown, beve, from_beve,
from_beve_with, from_str, from_str_with, read_beve_into, to_beve, to_string_with,
};
#[derive(Debug, Default, PartialEq)]
struct Asset {
version: String,
min_version: u32,
generator: String,
}
structio::object!(Asset {
#[required]
version,
#[required]
"minVersion" => min_version,
generator,
});
#[derive(Debug, Default, PartialEq)]
struct Loose {
version: String,
min_version: u32,
generator: String,
}
structio::object!(Loose {
version,
"minVersion" => min_version,
generator,
});
fn asset() -> Asset {
Asset {
version: "2.0".into(),
min_version: 1,
generator: "g".into(),
}
}
const MINIMAL: &str = r#"{"version":"2.0","minVersion":1}"#;
#[derive(Default)]
struct NoVersion {
min_version: u32,
generator: String,
}
structio::object!(NoVersion { "minVersion" => min_version, generator });
#[derive(Default)]
struct NoMinVersion {
version: String,
generator: String,
}
structio::object!(NoMinVersion { version, generator });
#[derive(Default)]
struct OnlyGenerator {
generator: String,
}
structio::object!(OnlyGenerator { generator });
#[derive(Default)]
struct NoGenerator {
version: String,
min_version: u32,
}
structio::object!(NoGenerator { version, "minVersion" => min_version });
#[test]
fn the_mask_names_the_marked_fields_in_declaration_order() {
assert_eq!(Asset::REQUIRED, 0b011);
assert_eq!(Loose::REQUIRED, 0);
}
#[test]
fn a_declaration_that_marks_nothing_requires_nothing() {
assert!(from_str::<Loose>("{}").is_ok());
assert!(from_str::<Loose>(r#"{"generator":"g"}"#).is_ok());
assert!(from_beve::<Loose>(&to_beve(&Loose::default())).is_ok());
}
#[test]
fn an_unmarked_field_may_still_be_left_out() {
let got = from_str::<Asset>(MINIMAL).unwrap();
assert_eq!(got.version, "2.0");
assert_eq!(got.min_version, 1);
assert_eq!(got.generator, "");
let doc = to_beve(&NoGenerator {
version: "2.0".into(),
min_version: 1,
});
let got = from_beve::<Asset>(&doc).unwrap();
assert_eq!(got.version, "2.0");
assert_eq!(got.generator, "");
}
#[test]
fn a_marked_field_left_out_is_a_missing_key() {
for json in [
r#"{"minVersion":1,"generator":"g"}"#,
r#"{"version":"2.0","generator":"g"}"#,
r#"{"generator":"g"}"#,
"{}",
] {
assert_eq!(
from_str::<Asset>(json).unwrap_err().code,
ErrorCode::MissingKey,
"for {json:?}"
);
}
}
#[test]
fn a_marked_field_left_out_of_a_beve_object_is_a_missing_key() {
let docs = [
to_beve(&NoVersion::default()),
to_beve(&NoMinVersion::default()),
to_beve(&OnlyGenerator::default()),
to_beve(&BTreeMap::<String, u32>::new()),
];
for (i, doc) in docs.iter().enumerate() {
assert_eq!(
from_beve::<Asset>(doc).unwrap_err().code,
ErrorCode::MissingKey,
"for document {i}"
);
}
assert_eq!(from_beve::<Asset>(&to_beve(&asset())).unwrap(), asset());
}
#[test]
fn the_marked_fields_may_arrive_in_any_order() {
for json in [
r#"{"version":"2.0","minVersion":1,"generator":"g"}"#,
r#"{"generator":"g","minVersion":1,"version":"2.0"}"#,
r#"{"minVersion":1,"generator":"g","version":"2.0"}"#,
] {
assert_eq!(from_str::<Asset>(json).unwrap(), asset(), "for {json:?}");
}
}
#[test]
fn the_error_is_located_at_the_object() {
let err = from_str::<Asset>(r#"{"version":"2.0"}"#).unwrap_err();
assert_eq!(err.code, ErrorCode::MissingKey);
assert_eq!(err.index, 0);
let err = from_beve::<Asset>(&to_beve(&OnlyGenerator::default())).unwrap_err();
assert_eq!(err.code, ErrorCode::MissingKey);
assert_eq!(err.index, 0);
}
#[test]
fn a_nested_struct_carries_its_own_requirement() {
#[derive(Debug, Default)]
struct Outer {
asset: Asset,
name: String,
}
structio::object!(Outer { asset, name });
let json = r#"{"asset":{"version":"2.0","minVersion":1},"name":"n"}"#;
assert!(from_str::<Outer>(json).is_ok());
let json = r#"{"asset":{"generator":"g"},"name":"n"}"#;
let err = from_str::<Outer>(json).unwrap_err();
assert_eq!(err.code, ErrorCode::MissingKey);
assert_eq!(err.index, json.find(r#"{"generator""#).unwrap());
}
#[test]
fn reading_into_a_populated_value_still_requires_the_marks() {
let mut into = asset();
assert_eq!(
structio::read_into(&mut into, r#"{"generator":"h"}"#)
.unwrap_err()
.code,
ErrorCode::MissingKey
);
let doc = to_beve(&OnlyGenerator {
generator: "h".into(),
});
let mut into = asset();
assert_eq!(
read_beve_into(&mut into, &doc).unwrap_err().code,
ErrorCode::MissingKey
);
}
#[test]
fn a_streamed_document_meets_the_marks() {
let input = "{\"version\":\"2.0\",\"minVersion\":1}\n{\"generator\":\"g\"}\n";
let mut docs = Documents::lines(input.as_bytes());
let first: Asset = docs.next_value().unwrap().unwrap();
assert_eq!(first.version, "2.0");
let err = docs.next_value::<Asset>().unwrap().unwrap_err();
assert_eq!(err.as_parse().unwrap().code, ErrorCode::MissingKey);
}
#[test]
fn a_pointer_read_meets_the_marks() {
#[derive(Default)]
struct Outer {
good: NoGenerator,
bad: OnlyGenerator,
}
structio::object!(Outer { good, bad });
let doc = to_beve(&Outer {
good: NoGenerator {
version: "2.0".into(),
min_version: 1,
},
bad: OnlyGenerator {
generator: "g".into(),
},
});
assert_eq!(
beve::from_slice_at::<Asset>(&doc, "/good").unwrap().version,
"2.0"
);
assert_eq!(
beve::from_slice_at::<Asset>(&doc, "/bad").unwrap_err().code,
ErrorCode::MissingKey
);
}
#[test]
fn require_keys_still_requires_the_unmarked_field() {
assert!(from_str::<Asset>(MINIMAL).is_ok());
assert_eq!(
from_str_with::<RequireKeys, Asset>(MINIMAL)
.unwrap_err()
.code,
ErrorCode::MissingKey
);
let complete = r#"{"version":"2.0","minVersion":1,"generator":"g"}"#;
assert_eq!(
from_str_with::<RequireKeys, Asset>(complete).unwrap(),
asset()
);
let doc = to_beve(&asset());
assert_eq!(from_beve_with::<RequireKeys, Asset>(&doc).unwrap(), asset());
}
#[test]
fn an_unknown_key_neither_satisfies_a_mark_nor_disturbs_one() {
let missing = r#"{"zzz":1,"generator":"g"}"#;
let complete = r#"{"zzz":1,"version":"2.0","minVersion":1}"#;
assert_eq!(
from_str::<Asset>(missing).unwrap_err().code,
ErrorCode::UnknownKey
);
assert_eq!(
from_str_with::<SkipUnknown, Asset>(missing)
.unwrap_err()
.code,
ErrorCode::MissingKey
);
assert_eq!(
from_str_with::<SkipUnknown, Asset>(complete)
.unwrap()
.version,
"2.0"
);
}
#[test]
fn skip_null_drops_a_member_a_mark_then_demands() {
#[derive(Debug, Default)]
struct Note {
text: Option<String>,
}
structio::object!(Note {
#[required]
text
});
let written = to_string_with::<SkipNull, _>(&Note { text: None });
assert_eq!(written, "{}");
assert_eq!(
from_str::<Note>(&written).unwrap_err().code,
ErrorCode::MissingKey
);
assert!(from_str::<Note>(r#"{"text":null}"#).is_ok());
}
#[test]
fn the_marker_composes_with_the_rest_of_a_declaration() {
#[derive(Debug, Default)]
struct Every {
plain: u32,
renamed: u32,
adapted: u32,
both: u32,
}
structio::json_object!(Every {
#[required]
plain,
#[required]
"wire" => renamed,
#[required]
adapted as Same,
#[required]
"b" => both as Same,
});
assert_eq!(Every::REQUIRED, 0b1111);
assert!(from_str::<Every>(r#"{"plain":1,"wire":2,"adapted":3,"b":4}"#).is_ok());
assert_eq!(
from_str::<Every>(r#"{"plain":1,"wire":2,"adapted":3}"#)
.unwrap_err()
.code,
ErrorCode::MissingKey
);
#[derive(Debug, Default)]
struct BeveOnly {
a: u32,
b: u32,
}
structio::beve_object!(BeveOnly {
#[required]
a,
b
});
assert_eq!(BeveOnly::REQUIRED, 0b01);
}
#[test]
fn a_borrowing_declaration_may_mark_a_field() {
#[derive(Debug, Default)]
struct Frame<'a> {
id: u32,
payload: &'a [u8],
}
structio::beve_object!(['de] Frame<'de> {
#[required]
id,
payload,
});
assert_eq!(<Frame<'_> as Keys>::REQUIRED, 0b01);
#[derive(Default)]
struct OnlyPayload {
payload: Vec<u8>,
}
structio::beve_object!(OnlyPayload { payload });
let doc = to_beve(&OnlyPayload {
payload: vec![1, 2, 3],
});
assert_eq!(
from_beve::<Frame<'_>>(&doc).unwrap_err().code,
ErrorCode::MissingKey
);
}
#[test]
fn a_generic_declaration_may_mark_a_field() {
#[derive(Debug)]
struct Page<T> {
items: Vec<T>,
cursor: Option<String>,
}
impl<T> Default for Page<T> {
fn default() -> Self {
Page {
items: Vec::new(),
cursor: None,
}
}
}
structio::object!([T: structio::ReadWrite + Default] Page<T> {
#[required]
items,
cursor,
});
assert_eq!(<Page<u32> as Keys>::REQUIRED, 0b01);
assert!(from_str::<Page<u32>>(r#"{"items":[1,2]}"#).is_ok());
assert_eq!(
from_str::<Page<u32>>(r#"{"cursor":"c"}"#).unwrap_err().code,
ErrorCode::MissingKey
);
}
macro_rules! wide {
($first:ident, $($mid:ident),*; $last:ident, $($past:ident),* $(,)?) => {
#[derive(Debug, Default)]
struct Wide {
$first: u32,
$($mid: u32,)*
$last: u32,
$($past: u32),*
}
structio::object!(Wide {
#[required]
$first,
$($mid,)*
#[required]
$last,
$($past),*
});
};
}
wide!(
f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20,
f21, f22, f23, f24, f25, f26, f27, f28, f29, f30, f31, f32, f33, f34, f35, f36, f37, f38, f39,
f40, f41, f42, f43, f44, f45, f46, f47, f48, f49, f50, f51, f52, f53, f54, f55, f56, f57, f58,
f59, f60, f61, f62;
f63, f64, f65, f66, f67, f68, f69,
);
#[test]
fn a_struct_too_wide_for_the_policy_may_still_mark_a_field() {
assert_eq!(Wide::KEYS.len(), 70);
assert_eq!(Wide::REQUIRED, 1 | (1 << 63));
assert_eq!(
from_str::<Wide>(r#"{"f0":1,"f63":2,"f69":3}"#).unwrap().f69,
3
);
assert_eq!(
from_str::<Wide>(r#"{"f0":1,"f69":2}"#).unwrap_err().code,
ErrorCode::MissingKey
);
assert_eq!(
from_str::<Wide>(r#"{"f63":1,"f69":2}"#).unwrap_err().code,
ErrorCode::MissingKey
);
assert_eq!(
from_str::<Wide>(r#"{"f64":1}"#).unwrap_err().code,
ErrorCode::MissingKey
);
let doc = to_beve(&BTreeMap::from([("f64".to_string(), 1u32)]));
assert_eq!(
from_beve::<Wide>(&doc).unwrap_err().code,
ErrorCode::MissingKey
);
let doc = to_beve(&BTreeMap::from([
("f0".to_string(), 1u32),
("f63".to_string(), 2u32),
]));
assert_eq!(from_beve::<Wide>(&doc).unwrap().f63, 2);
}