use structio::{
ErrorCode, Options, from_beve, from_str, json, to_beve, to_string, transcode::beve_to_json,
};
#[derive(Default, Debug, PartialEq)]
struct Names {
byte_offset: u32,
vec3_x: u32,
http_url: u32,
type_: u32,
_scratch: u32,
id: u32,
}
structio::object!(Names as "camelCase" {
byte_offset,
vec3_x,
http_url,
type_,
_scratch,
id,
});
#[test]
fn a_rule_converts_every_key_the_declaration_does_not_spell_out() {
assert_eq!(
to_string(&Names::default()),
r#"{"byteOffset":0,"vec3X":0,"httpUrl":0,"type":0,"scratch":0,"id":0}"#
);
}
#[test]
fn the_converted_key_is_the_one_that_reads_back() {
let v: Names =
from_str(r#"{"byteOffset":1,"vec3X":2,"httpUrl":3,"type":4,"scratch":5,"id":6}"#).unwrap();
assert_eq!(
v,
Names {
byte_offset: 1,
vec3_x: 2,
http_url: 3,
type_: 4,
_scratch: 5,
id: 6,
}
);
}
#[test]
fn the_rust_name_is_no_longer_a_key() {
assert_eq!(
from_str::<Names>(r#"{"byte_offset":1}"#).unwrap_err().code,
ErrorCode::UnknownKey
);
}
macro_rules! styled {
($name:ident, $rule:literal) => {
#[derive(Default)]
struct $name {
http_byte_offset: u32,
}
structio::json_object!($name as $rule { http_byte_offset });
};
}
styled!(Lower, "lowercase");
styled!(Upper, "UPPERCASE");
styled!(Pascal, "PascalCase");
styled!(Camel, "camelCase");
styled!(Snake, "snake_case");
styled!(Screaming, "SCREAMING_SNAKE_CASE");
styled!(Kebab, "kebab-case");
styled!(ScreamingKebab, "SCREAMING-KEBAB-CASE");
#[test]
fn every_rule_spells_the_same_name_its_own_way() {
let keys = [
to_string(&Lower::default()),
to_string(&Upper::default()),
to_string(&Pascal::default()),
to_string(&Camel::default()),
to_string(&Snake::default()),
to_string(&Screaming::default()),
to_string(&Kebab::default()),
to_string(&ScreamingKebab::default()),
];
assert_eq!(
keys,
[
r#"{"httpbyteoffset":0}"#,
r#"{"HTTPBYTEOFFSET":0}"#,
r#"{"HttpByteOffset":0}"#,
r#"{"httpByteOffset":0}"#,
r#"{"http_byte_offset":0}"#,
r#"{"HTTP_BYTE_OFFSET":0}"#,
r#"{"http-byte-offset":0}"#,
r#"{"HTTP-BYTE-OFFSET":0}"#,
]
.map(String::from)
);
}
#[derive(Default, Debug, PartialEq)]
#[allow(non_snake_case)]
struct Awkward {
caféBar: u32,
r#type: u32,
}
structio::json_object!(Awkward as "camelCase" { caféBar, r#type });
#[test]
fn a_capital_beside_a_non_ascii_byte_keeps_its_case() {
assert!(to_string(&Awkward::default()).contains("\"caféBar\""));
}
#[test]
fn a_raw_identifier_loses_its_prefix() {
let json = to_string(&Awkward::default());
assert!(json.contains("\"type\""), "{json}");
assert!(!json.contains("r#"), "{json}");
}
#[derive(Default, Debug, PartialEq)]
struct Mixed {
http_url: String,
byte_offset: u32,
schema_version: u32,
}
structio::object!(Mixed as "camelCase" {
"httpURL" => http_url,
#[required] byte_offset,
schema_version,
});
#[test]
fn an_explicit_key_wins_over_the_rule() {
let v = Mixed {
http_url: "x".into(),
byte_offset: 1,
schema_version: 2,
};
assert_eq!(
to_string(&v),
r#"{"httpURL":"x","byteOffset":1,"schemaVersion":2}"#
);
assert_eq!(from_str::<Mixed>(&to_string(&v)).unwrap(), v);
}
#[test]
fn a_required_field_is_still_required_under_a_rule() {
assert_eq!(
from_str::<Mixed>(r#"{"httpURL":"x","schemaVersion":2}"#)
.unwrap_err()
.code,
ErrorCode::MissingKey
);
}
struct Halved;
impl<'de> json::ReadAs<'de, u32> for Halved {
fn read<O: Options>(value: &mut u32, p: &mut json::Parser<'de, O>) -> Result<(), ErrorCode> {
let mut half = 0u32;
json::Read::read(&mut half, p)?;
*value = half * 2;
Ok(())
}
}
impl json::WriteAs<u32> for Halved {
fn write<O: Options>(value: &u32, w: &mut json::Writer<'_, O>) {
json::Write::write(&(*value / 2), w);
}
}
#[derive(Default, Debug, PartialEq)]
struct Adapted {
tick_count: u32,
}
structio::json_object!(Adapted as "kebab-case" { tick_count as Halved });
#[test]
fn a_field_adapter_and_a_rule_coexist() {
assert_eq!(
to_string(&Adapted { tick_count: 10 }),
r#"{"tick-count":5}"#
);
assert_eq!(
from_str::<Adapted>(r#"{"tick-count":5}"#).unwrap(),
Adapted { tick_count: 10 }
);
}
#[derive(Default, Debug, PartialEq)]
struct Ruled {
byte_offset: u32,
http_url: String,
}
structio::object!(Ruled as "camelCase" { byte_offset, http_url });
#[derive(Default, Debug, PartialEq)]
struct Spelled {
byte_offset: u32,
http_url: String,
}
structio::object!(Spelled {
"byteOffset" => byte_offset,
"httpUrl" => http_url,
});
fn ruled() -> Ruled {
Ruled {
byte_offset: 7,
http_url: "https://example.invalid".into(),
}
}
fn spelled() -> Spelled {
Spelled {
byte_offset: 7,
http_url: "https://example.invalid".into(),
}
}
#[test]
fn a_rule_is_shorthand_for_the_keys_it_writes_out() {
assert_eq!(to_string(&ruled()), to_string(&spelled()));
assert_eq!(to_beve(&ruled()), to_beve(&spelled()));
}
#[test]
fn the_beve_document_reads_back_through_the_hash() {
assert_eq!(from_beve::<Ruled>(&to_beve(&ruled())).unwrap(), ruled());
}
#[test]
fn a_transcoded_document_carries_the_converted_keys() {
assert_eq!(
beve_to_json(&to_beve(&ruled())).unwrap(),
to_string(&ruled())
);
}
#[derive(Default, Debug, PartialEq)]
enum Mode {
#[default]
ReadOnly,
ReadWrite,
HTTPProxy,
Off,
}
structio::unit_enum!(Mode as "kebab-case" { ReadOnly, ReadWrite, HTTPProxy, Off });
#[test]
fn a_variant_name_is_split_into_words_too() {
assert_eq!(to_string(&Mode::ReadWrite), r#""read-write""#);
assert_eq!(to_string(&Mode::HTTPProxy), r#""http-proxy""#);
assert_eq!(to_string(&Mode::Off), r#""off""#);
assert_eq!(from_str::<Mode>(r#""read-only""#).unwrap(), Mode::ReadOnly);
}
#[test]
fn a_run_of_unit_variants_is_still_a_beve_string_array() {
let modes = vec![Mode::ReadWrite, Mode::HTTPProxy];
let doc = to_beve(&modes);
assert_eq!(from_beve::<Vec<Mode>>(&doc).unwrap(), modes);
assert_eq!(
beve_to_json(&doc).unwrap(),
r#"["read-write","http-proxy"]"#
);
}
#[derive(Default, Debug, PartialEq)]
enum Event {
#[default]
KeyDown,
MouseMove(u32),
TextInput(String),
}
structio::tagged_enum!(Event as "camelCase" {
KeyDown,
MouseMove(_),
"text" => TextInput(_),
});
#[test]
fn a_tagged_variant_is_keyed_by_its_converted_name() {
assert_eq!(to_string(&Event::KeyDown), r#""keyDown""#);
assert_eq!(to_string(&Event::MouseMove(3)), r#"{"mouseMove":3}"#);
assert_eq!(
to_string(&Event::TextInput("hi".into())),
r#"{"text":"hi"}"#
);
for e in [
Event::KeyDown,
Event::MouseMove(3),
Event::TextInput("hi".into()),
] {
let json = to_string(&e);
assert_eq!(from_str::<Event>(&json).unwrap(), e);
assert_eq!(from_beve::<Event>(&to_beve(&e)).unwrap(), e);
}
}
#[derive(Default, Debug, PartialEq)]
struct Envelope<T> {
schema_version: u32,
inner_value: T,
}
structio::object!([T: structio::ReadWrite + Default] Envelope<T> as "camelCase" {
schema_version,
inner_value,
});
#[test]
fn a_rule_survives_the_generics_form() {
let v = Envelope {
schema_version: 2,
inner_value: 9u32,
};
assert_eq!(to_string(&v), r#"{"schemaVersion":2,"innerValue":9}"#);
assert_eq!(from_str::<Envelope<u32>>(&to_string(&v)).unwrap(), v);
}
#[derive(Default, Debug, PartialEq)]
struct Raw {
r#type: u32,
r#fn: u32,
plain: u32,
}
structio::object!(Raw {
r#type,
r#fn,
plain,
});
#[test]
fn a_raw_identifier_drops_its_prefix() {
let v = Raw {
r#type: 1,
r#fn: 2,
plain: 3,
};
assert_eq!(to_string(&v), r#"{"type":1,"fn":2,"plain":3}"#);
assert_eq!(from_str::<Raw>(&to_string(&v)).unwrap(), v);
}
#[test]
fn a_raw_key_reads_back_through_the_hash_and_through_beve() {
let v = Raw {
r#type: 7,
r#fn: 8,
plain: 9,
};
assert_eq!(
from_str::<Raw>(r#"{"type":7,"fn":8,"plain":9}"#).unwrap(),
v
);
assert_eq!(from_beve::<Raw>(&to_beve(&v)).unwrap(), v);
assert_eq!(
beve_to_json(&to_beve(&v)).unwrap(),
r#"{"type":7,"fn":8,"plain":9}"#
);
}
#[test]
fn the_prefixed_key_is_gone_rather_than_also_accepted() {
assert_eq!(
from_str::<Raw>(r#"{"r#type":1,"fn":2,"plain":3}"#)
.unwrap_err()
.code,
ErrorCode::UnknownKey
);
}
#[derive(Default, Debug, PartialEq)]
struct RawCased {
r#type: u32,
r#byte_offset: u32,
}
structio::object!(RawCased as "camelCase" {
r#type,
r#byte_offset,
});
#[test]
fn a_rule_sees_the_name_without_its_prefix() {
let v = RawCased {
r#type: 1,
r#byte_offset: 2,
};
assert_eq!(to_string(&v), r#"{"type":1,"byteOffset":2}"#);
assert_eq!(from_str::<RawCased>(&to_string(&v)).unwrap(), v);
}
#[derive(Default, Debug, PartialEq)]
struct RawLiteral {
r#type: u32,
}
structio::object!(RawLiteral {
"r#type" => r#type,
});
#[test]
fn an_explicit_key_is_never_unrawed() {
assert_eq!(to_string(&RawLiteral { r#type: 5 }), r##"{"r#type":5}"##);
}
#[derive(Default, Debug, PartialEq)]
#[allow(non_camel_case_types)]
enum RawVariant {
#[default]
r#type,
Plain,
}
structio::unit_enum!(RawVariant { r#type, Plain });
#[test]
fn a_raw_variant_name_drops_its_prefix_too() {
assert_eq!(to_string(&RawVariant::r#type), r#""type""#);
assert_eq!(
from_str::<RawVariant>(r#""type""#).unwrap(),
RawVariant::r#type
);
assert_eq!(
from_beve::<RawVariant>(&to_beve(&RawVariant::r#type)).unwrap(),
RawVariant::r#type
);
}