use std::io;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::ser::{CompactFormatter, Formatter, PrettyFormatter, Serializer};
use serde_json::{Map, Value};
pub fn to_string<T: Serialize + ?Sized>(value: &T) -> serde_json::Result<String> {
write(value, JsFormatter(CompactFormatter))
}
pub trait JsonValue: Serialize {}
impl JsonValue for Value {}
impl JsonValue for Map<String, Value> {}
pub fn stringify(value: &impl JsonValue) -> String {
to_string(value).unwrap_or_default()
}
pub fn number(value: f64) -> Value {
if value.fract() == 0.0 && value.abs() <= MAX_SAFE_INTEGER as f64 {
Value::from(value as i64)
} else {
serde_json::Number::from_f64(value).map_or(Value::Null, Value::Number)
}
}
pub fn js_number(text: &str) -> f64 {
let text = text.trim_matches(|c: char| c.is_whitespace() || c == '\u{feff}');
if text.is_empty() {
return 0.0;
}
for (prefix, radix) in [("0x", 16), ("0o", 8), ("0b", 2)] {
if let Some(digits) = text
.get(..2)
.filter(|start| start.eq_ignore_ascii_case(prefix))
.and_then(|_| text.get(2..))
{
let value = digits.chars().try_fold(0.0_f64, |value, c| {
c.to_digit(radix)
.map(|digit| value * f64::from(radix) + f64::from(digit))
});
return value.filter(|_| !digits.is_empty()).unwrap_or(f64::NAN);
}
}
match text {
"Infinity" | "+Infinity" => return f64::INFINITY,
"-Infinity" => return f64::NEG_INFINITY,
_ => {}
}
if !text
.bytes()
.all(|byte| byte.is_ascii_digit() || b"+-.eE".contains(&byte))
{
return f64::NAN;
}
text.parse().unwrap_or(f64::NAN)
}
pub fn to_string_pretty<T: Serialize + ?Sized>(
value: &T,
indent: &str,
) -> serde_json::Result<String> {
write(
value,
JsFormatter(PrettyFormatter::with_indent(indent.as_bytes())),
)
}
fn write<T: Serialize + ?Sized>(
value: &T,
formatter: impl Formatter,
) -> serde_json::Result<String> {
let mut out = Vec::new();
value.serialize(&mut Serializer::with_formatter(&mut out, formatter))?;
Ok(String::from_utf8(out).expect("serde_json writes UTF-8"))
}
struct JsFormatter<F>(F);
const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
impl<F: Formatter> Formatter for JsFormatter<F> {
fn write_f64<W: ?Sized + io::Write>(&mut self, writer: &mut W, value: f64) -> io::Result<()> {
writer.write_all(ryu_js::Buffer::new().format(value).as_bytes())
}
fn write_i64<W: ?Sized + io::Write>(&mut self, writer: &mut W, value: i64) -> io::Result<()> {
if value.unsigned_abs() > MAX_SAFE_INTEGER {
return self.write_f64(writer, value as f64);
}
self.0.write_i64(writer, value)
}
fn write_u64<W: ?Sized + io::Write>(&mut self, writer: &mut W, value: u64) -> io::Result<()> {
if value > MAX_SAFE_INTEGER {
return self.write_f64(writer, value as f64);
}
self.0.write_u64(writer, value)
}
fn begin_array<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
self.0.begin_array(writer)
}
fn end_array<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
self.0.end_array(writer)
}
fn begin_array_value<W: ?Sized + io::Write>(
&mut self,
writer: &mut W,
first: bool,
) -> io::Result<()> {
self.0.begin_array_value(writer, first)
}
fn end_array_value<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
self.0.end_array_value(writer)
}
fn begin_object<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
self.0.begin_object(writer)
}
fn end_object<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
self.0.end_object(writer)
}
fn begin_object_key<W: ?Sized + io::Write>(
&mut self,
writer: &mut W,
first: bool,
) -> io::Result<()> {
self.0.begin_object_key(writer, first)
}
fn end_object_key<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
self.0.end_object_key(writer)
}
fn begin_object_value<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
self.0.begin_object_value(writer)
}
fn end_object_value<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
self.0.end_object_value(writer)
}
}
pub fn parse<T: DeserializeOwned>(text: &str) -> serde_json::Result<T> {
serde_json::from_str(text.strip_prefix('\u{feff}').unwrap_or(text))
}
#[cfg(test)]
mod tests {
use serde_json::{Value, json};
use super::*;
#[test]
fn reads_numbers_like_javascript() {
let number = |text| super::number(js_number(text));
assert_eq!(number("8080"), json!(8080));
assert_eq!(number(" 1.5e1 "), json!(15));
assert_eq!(number(""), json!(0));
assert_eq!(number("x"), Value::Null);
assert_eq!(number("0x1F90"), json!(8080));
assert_eq!(number(" 0o17 "), json!(15));
assert_eq!(number("0B101"), json!(5));
for nan in ["0x", "-0x10", "0x+1", "inf", "NaN", "1e", "1_000"] {
assert!(js_number(nan).is_nan(), "{nan}");
}
assert_eq!(js_number("-Infinity"), f64::NEG_INFINITY);
assert_eq!(number("Infinity"), Value::Null);
}
#[test]
fn parses_past_a_byte_order_mark() {
assert_eq!(
parse::<Value>("\u{feff}{\"a\":1}").ok(),
Some(json!({"a": 1}))
);
assert_eq!(parse::<Value>("[]").ok(), Some(json!([])));
assert!(parse::<Value>("\u{feff}\u{feff}[]").is_err());
}
#[test]
fn numbers_match_javascript() {
let cases: [(f64, &str); 12] = [
(0.0, "0"),
(-0.0, "0"),
(5.0, "5"),
(-42.0, "-42"),
(0.000003, "0.000003"),
(1e-7, "1e-7"),
(1e21, "1e+21"),
(0.1 + 0.2, "0.30000000000000004"),
(9007199254740992.0, "9007199254740992"),
(123.456, "123.456"),
(5e-324, "5e-324"),
(f64::MAX, "1.7976931348623157e+308"),
];
for (value, expected) in cases {
assert_eq!(to_string(&value).unwrap(), expected, "{value:e}");
}
}
#[test]
fn large_integers_round_like_javascript() {
let value: Value = serde_json::from_str(
"[9007199254740991,9007199254740993,12345678901234567890,-12345678901234567]",
)
.unwrap();
assert_eq!(
to_string(&value).unwrap(),
"[9007199254740991,9007199254740992,12345678901234567000,-12345678901234568]"
);
}
#[test]
fn parsed_numbers_round_trip() {
let text = r#"[0,0.000003,0.0037020000000000004,1e+21,1893456000000,-1.5e-7]"#;
let value: Value = serde_json::from_str(text).unwrap();
assert_eq!(to_string(&value).unwrap(), text);
}
#[test]
fn pretty_matches_javascript_layout() {
let value = json!({ "a": [], "b": {}, "c": [1, { "d": null }] });
let expected = "{\n \"a\": [],\n \"b\": {},\n \"c\": [\n 1,\n {\n \"d\": null\n }\n ]\n}";
assert_eq!(to_string_pretty(&value, " ").unwrap(), expected);
}
#[test]
fn non_finite_numbers_are_null() {
assert_eq!(
to_string(&[f64::NAN, f64::INFINITY]).unwrap(),
"[null,null]"
);
}
}