use structio::{ErrorCode, Options, from_str, json, to_string};
#[derive(Default, Debug, PartialEq, Clone)]
struct Fixed {
mantissa: i128,
scale: usize,
}
impl Fixed {
fn parse(text: &str) -> Result<Self, ErrorCode> {
let (negative, rest) = match text.strip_prefix('-') {
Some(r) => (true, r),
None => (false, text),
};
if rest.contains(['e', 'E']) {
return Err(ErrorCode::InvalidNumber);
}
let (whole, frac) = rest.split_once('.').unwrap_or((rest, ""));
let mut mantissa: i128 = 0;
for b in whole.bytes().chain(frac.bytes()) {
mantissa = mantissa
.checked_mul(10)
.and_then(|v| v.checked_add(i128::from(b - b'0')))
.ok_or(ErrorCode::NumberOutOfRange)?;
}
Ok(Fixed {
mantissa: if negative { -mantissa } else { mantissa },
scale: frac.len(),
})
}
fn to_text(&self) -> String {
let digits = self.mantissa.unsigned_abs().to_string();
let sign = if self.mantissa < 0 { "-" } else { "" };
if self.scale == 0 {
return format!("{sign}{digits}");
}
let padded = format!("{:0>width$}", digits, width = self.scale + 1);
let point = padded.len() - self.scale;
format!("{sign}{}.{}", &padded[..point], &padded[point..])
}
}
impl<'de> json::Read<'de> for Fixed {
fn read<O: Options>(&mut self, p: &mut json::Parser<'de, O>) -> Result<(), ErrorCode> {
*self = Fixed::parse(p.read_number_str()?)?;
Ok(())
}
}
impl json::Write for Fixed {
fn write<O: Options>(&self, w: &mut json::Writer<'_, O>) {
w.write_number_str(&self.to_text());
}
}
#[derive(Default, Debug, PartialEq)]
struct Ledger {
balance: Fixed,
entries: Vec<Fixed>,
}
structio::json_object!(Ledger { balance, entries });
#[test]
fn the_token_is_the_literal_every_other_reader_would_have_read() {
let valid = [
"0",
"-0",
"1",
"-1",
"0.5",
"-0.5",
"1.25",
"1e5",
"1E5",
"1e+5",
"1e-5",
"-1.5E-300",
"0e0",
"123456789012345678901234567890",
"1.7976931348623157e309",
"0.000000000000000000000000000001",
];
for text in valid {
let mut p = json::Parser::new(text);
assert_eq!(p.read_number_str().unwrap(), text, "reading {text:?}");
assert_eq!(p.position(), text.len(), "cursor after {text:?}");
let mut f = json::Parser::new(text);
f.read_f64()
.unwrap_or_else(|e| panic!("{text:?} as f64: {e:?}"));
assert_eq!(p.position(), f.position(), "extent of {text:?}");
}
for (doc, token) in [("-12.5e3,", "-12.5e3"), ("0x1", "0"), ("1 ", "1")] {
let mut p = json::Parser::new(doc);
assert_eq!(p.read_number_str().unwrap(), token, "reading {doc:?}");
assert_eq!(p.position(), token.len(), "cursor after {doc:?}");
}
assert_eq!(
from_str::<Ledger>(r#"{"balance":0x1,"entries":[]}"#)
.unwrap_err()
.code,
ErrorCode::ExpectedComma
);
}
#[test]
fn the_grammar_refuses_what_no_reader_would_take() {
let invalid = [
"", "-", "+1", "01", "-01", ".5", "1.", "1.e5", "1e", "1e+", "1e-", "Infinity", "NaN",
"true",
];
for text in invalid {
assert!(
json::Parser::new(text).read_number_str().is_err(),
"accepted {text:?}"
);
assert!(
json::Parser::new(text).read_f64().is_err(),
"read_f64 accepted {text:?}"
);
}
}
#[test]
fn skipping_is_looser_than_reading() {
let sloppy = "1e--2.3.4";
let mut skipper = json::Parser::new(sloppy);
skipper.skip_value().unwrap();
assert_eq!(skipper.position(), sloppy.len());
let mut reader = json::Parser::new(sloppy);
assert!(reader.read_number_str().is_err());
}
#[test]
fn the_text_points_into_the_document() {
let doc = String::from(" -1.5e10");
let mut p = json::Parser::new(&doc);
p.skip_ws();
let text = p.read_number_str().unwrap();
assert_eq!(text, "-1.5e10");
assert!(std::ptr::eq(text.as_ptr(), doc[2..].as_ptr()));
}
#[test]
fn digits_survive_a_value_no_float_could_hold() {
let json = r#"{"balance":-1234567890.1234567890123456789,"entries":[0.001,1000000]}"#;
let ledger: Ledger = from_str(json).unwrap();
assert_eq!(
ledger.balance,
Fixed {
mantissa: -12345678901234567890123456789,
scale: 19,
}
);
assert_eq!(to_string(&ledger), json);
}
#[test]
fn a_malformed_number_reaches_the_caller_as_an_error() {
let err = from_str::<Ledger>(r#"{"balance":01,"entries":[]}"#).unwrap_err();
assert_eq!(err.code, ErrorCode::InvalidNumber);
let err = from_str::<Ledger>(r#"{"balance":1e5,"entries":[]}"#).unwrap_err();
assert_eq!(err.code, ErrorCode::InvalidNumber);
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "requires a JSON number literal")]
fn writing_a_non_number_is_a_debug_assertion() {
let mut w = json::Writer::<structio::Standard>::new();
w.write_number_str("1.2.3");
}