use std::fmt;
use crate::{Map, Number, Seg, Value, render_path};
pub fn from_json(value: serde_json::Value) -> Value {
match value {
serde_json::Value::Null => Value::Null,
serde_json::Value::Bool(b) => Value::Bool(b),
serde_json::Value::Number(n) => Value::Number(number_from_json(&n)),
serde_json::Value::String(s) => Value::String(s),
serde_json::Value::Array(items) => Value::Array(items.into_iter().map(from_json).collect()),
serde_json::Value::Object(map) => {
Value::Object(map.into_iter().map(|(k, v)| (k, from_json(v))).collect())
}
}
}
pub fn object_from_json(map: serde_json::Map<String, serde_json::Value>) -> Map {
map.into_iter().map(|(k, v)| (k, from_json(v))).collect()
}
pub fn to_json(value: Value) -> Result<serde_json::Value, NonFiniteFloat> {
let mut nonfinite = Vec::new();
collect_unjsonable(&value, &mut Vec::new(), &mut nonfinite);
if !nonfinite.is_empty() {
return Err(NonFiniteFloat { entries: nonfinite });
}
Ok(to_json_unchecked(value))
}
fn to_json_unchecked(value: Value) -> serde_json::Value {
match value {
Value::Null => serde_json::Value::Null,
Value::Bool(b) => serde_json::Value::Bool(b),
Value::Number(n) => serde_json::Value::Number(number_to_json(n)),
Value::String(s) | Value::Datetime(s) => serde_json::Value::String(s),
Value::Array(items) => {
serde_json::Value::Array(items.into_iter().map(to_json_unchecked).collect())
}
Value::Object(map) => serde_json::Value::Object(
map.into_iter()
.map(|(k, v)| (k, to_json_unchecked(v)))
.collect(),
),
}
}
pub fn from_toml(value: toml::Value) -> Value {
match value {
toml::Value::String(s) => Value::String(s),
toml::Value::Integer(i) => Value::Number(Number::I64(i)),
toml::Value::Float(f) => Value::Number(Number::F64(f)),
toml::Value::Boolean(b) => Value::Bool(b),
toml::Value::Datetime(dt) => Value::Datetime(dt.to_string()),
toml::Value::Array(items) => Value::Array(items.into_iter().map(from_toml).collect()),
toml::Value::Table(table) => {
Value::Object(table.into_iter().map(|(k, v)| (k, from_toml(v))).collect())
}
}
}
pub fn to_toml(value: Value) -> Result<toml::Value, TomlError> {
let mut nulls = Vec::new();
let mut integers = Vec::new();
let mut datetimes = Vec::new();
collect_untomlable(
&value,
&mut Vec::new(),
&mut nulls,
&mut integers,
&mut datetimes,
);
if !nulls.is_empty() {
return Err(TomlError::Null(NullInToml { entries: nulls }));
}
if !integers.is_empty() {
return Err(TomlError::Integer(IntegerOutOfRange { entries: integers }));
}
if !datetimes.is_empty() {
return Err(TomlError::Datetime(BadDatetime { entries: datetimes }));
}
Ok(to_toml_unchecked(value))
}
fn to_toml_unchecked(value: Value) -> toml::Value {
match value {
Value::Null => {
unreachable!("nulls are rejected by to_toml before conversion");
}
Value::Bool(b) => toml::Value::Boolean(b),
Value::Number(n) => number_to_toml(n),
Value::String(s) => toml::Value::String(s),
Value::Datetime(s) => toml::Value::Datetime(
s.parse()
.expect("unparseable datetimes are rejected by to_toml before conversion"),
),
Value::Array(items) => {
toml::Value::Array(items.into_iter().map(to_toml_unchecked).collect())
}
Value::Object(map) => {
let mut table = toml::Table::new();
for (k, v) in map {
table.insert(k, to_toml_unchecked(v));
}
toml::Value::Table(table)
}
}
}
fn number_from_json(n: &serde_json::Number) -> Number {
if let Some(i) = n.as_i64() {
Number::I64(i)
} else if let Some(u) = n.as_u64() {
Number::from_u64(u)
} else if let Some(f) = n.as_f64() {
Number::F64(f)
} else {
Number::F64(0.0)
}
}
fn number_to_json(n: Number) -> serde_json::Number {
match n {
Number::I64(i) => i.into(),
Number::U64(u) => u.into(),
Number::F64(f) => serde_json::Number::from_f64(f)
.expect("non-finite floats are rejected by to_json before conversion"),
}
}
fn number_to_toml(n: Number) -> toml::Value {
match n {
Number::I64(i) => toml::Value::Integer(i),
Number::U64(u) => toml::Value::Integer(
i64::try_from(u)
.expect("out-of-range integers are rejected by to_toml before conversion"),
),
Number::F64(f) => toml::Value::Float(f),
}
}
fn collect_untomlable(
value: &Value,
cur: &mut Vec<Seg>,
nulls: &mut Vec<Vec<Seg>>,
integers: &mut Vec<(Vec<Seg>, u64)>,
datetimes: &mut Vec<(Vec<Seg>, String)>,
) {
match value {
Value::Null => nulls.push(cur.clone()),
Value::Number(Number::U64(u)) if i64::try_from(*u).is_err() => {
integers.push((cur.clone(), *u));
}
Value::Datetime(s) if s.parse::<toml::value::Datetime>().is_err() => {
datetimes.push((cur.clone(), s.clone()));
}
Value::Object(obj) => {
for (k, v) in obj {
cur.push(Seg::Key(k.clone()));
collect_untomlable(v, cur, nulls, integers, datetimes);
cur.pop();
}
}
Value::Array(items) => {
for (i, v) in items.iter().enumerate() {
cur.push(Seg::Index(i));
collect_untomlable(v, cur, nulls, integers, datetimes);
cur.pop();
}
}
_ => {}
}
}
fn collect_unjsonable(value: &Value, cur: &mut Vec<Seg>, nonfinite: &mut Vec<(Vec<Seg>, f64)>) {
match value {
Value::Number(Number::F64(f)) if !f.is_finite() => nonfinite.push((cur.clone(), *f)),
Value::Object(obj) => {
for (k, v) in obj {
cur.push(Seg::Key(k.clone()));
collect_unjsonable(v, cur, nonfinite);
cur.pop();
}
}
Value::Array(items) => {
for (i, v) in items.iter().enumerate() {
cur.push(Seg::Index(i));
collect_unjsonable(v, cur, nonfinite);
cur.pop();
}
}
_ => {}
}
}
pub(crate) fn nonfinite_spelling(f: f64) -> &'static str {
if f.is_nan() {
"nan"
} else if f.is_sign_positive() {
"inf"
} else {
"-inf"
}
}
pub fn replace_nulls(value: &mut Value, placeholder: &str) {
match value {
Value::Null => *value = Value::String(placeholder.to_string()),
Value::Object(obj) => {
for v in obj.values_mut() {
replace_nulls(v, placeholder);
}
}
Value::Array(items) => {
for v in items.iter_mut() {
replace_nulls(v, placeholder);
}
}
_ => {}
}
}
#[derive(Debug)]
pub struct NullInToml {
entries: Vec<Vec<Seg>>,
}
impl fmt::Display for NullInToml {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "cannot serialize null to TOML")?;
for path in &self.entries {
write!(f, "\n --> {}", render_path(path))?;
}
Ok(())
}
}
impl std::error::Error for NullInToml {}
#[derive(Debug)]
pub struct BadDatetime {
entries: Vec<(Vec<Seg>, String)>,
}
impl fmt::Display for BadDatetime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "cannot serialize datetime to TOML")?;
for (path, text) in &self.entries {
write!(f, "\n --> {}: `{text}`", render_path(path))?;
}
Ok(())
}
}
impl std::error::Error for BadDatetime {}
#[derive(Debug)]
pub struct IntegerOutOfRange {
entries: Vec<(Vec<Seg>, u64)>,
}
impl fmt::Display for IntegerOutOfRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "cannot serialize integer to TOML")?;
for (path, n) in &self.entries {
write!(f, "\n --> {}: `{n}`", render_path(path))?;
}
Ok(())
}
}
impl std::error::Error for IntegerOutOfRange {}
#[derive(Debug)]
pub struct NonFiniteFloat {
entries: Vec<(Vec<Seg>, f64)>,
}
impl fmt::Display for NonFiniteFloat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "cannot serialize non-finite number to JSON")?;
for (path, value) in &self.entries {
write!(
f,
"\n --> {}: `{}`",
render_path(path),
nonfinite_spelling(*value)
)?;
}
Ok(())
}
}
impl std::error::Error for NonFiniteFloat {}
#[derive(Debug, thiserror::Error)]
pub enum TomlError {
#[error("{0}")]
Null(NullInToml),
#[error("{0}")]
Integer(IntegerOutOfRange),
#[error("{0}")]
Datetime(BadDatetime),
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn ir(v: serde_json::Value) -> Value {
from_json(v)
}
fn json(v: Value) -> serde_json::Value {
to_json(v).expect("no non-finite floats")
}
#[test]
fn a_toml_datetime_stays_a_datetime_in_the_ir() {
let parsed: toml::Value =
toml::from_str("date = 1979-05-27T07:32:00Z\n").expect("valid toml");
let v = from_toml(parsed);
let Value::Object(map) = &v else {
panic!("expected an object, got {v:?}");
};
assert_eq!(
map["date"],
Value::Datetime("1979-05-27T07:32:00Z".to_string())
);
assert_eq!(json(v), json!({"date": "1979-05-27T07:32:00Z"}));
}
#[test]
fn every_toml_datetime_form_round_trips() {
let src = "\
offset = 1979-05-27T07:32:00Z
offset_frac = 1979-05-27T00:32:00.999999-07:00
local = 1979-05-27T07:32:00
date = 1979-05-27
time = 07:32:00.5
";
let parsed: toml::Value = toml::from_str(src).expect("valid toml");
let back = to_toml(from_toml(parsed.clone())).expect("every spelling re-parses");
assert_eq!(back, parsed);
}
#[test]
fn to_toml_rejects_nulls_with_array_indices() {
let err = to_toml(ir(json!({"a": {"b": null}, "c": [1, null], "d": 2}))).unwrap_err();
let TomlError::Null(report) = err else {
panic!("expected a null report, got {err}");
};
let rendered: Vec<_> = report.entries.iter().map(|p| render_path(p)).collect();
assert_eq!(rendered, vec!["a.b", "c[1]"]);
}
#[test]
fn to_toml_reports_every_datetime_that_does_not_reparse() {
let value = Value::Object(Map::from_iter([
("created".to_string(), Value::Datetime("nope".to_string())),
(
"events".to_string(),
Value::Array(vec![
Value::Datetime("1979-05-27T07:32:00Z".to_string()),
Value::Datetime("yesterday".to_string()),
]),
),
]));
let err = to_toml(value).unwrap_err();
assert!(matches!(err, TomlError::Datetime(_)), "{err}");
assert_eq!(
err.to_string(),
"cannot serialize datetime to TOML\n --> created: `nope`\n --> events[1]: `yesterday`"
);
}
#[test]
fn a_null_is_reported_before_a_malformed_datetime() {
let all_three = || {
Value::Object(Map::from_iter([
("a".to_string(), Value::Null),
("n".to_string(), Value::Number(Number::U64(u64::MAX))),
("d".to_string(), Value::Datetime("nope".to_string())),
]))
};
assert!(matches!(
to_toml(all_three()).unwrap_err(),
TomlError::Null(_)
));
let Value::Object(mut map) = all_three() else {
unreachable!()
};
map.shift_remove("a");
assert!(matches!(
to_toml(Value::Object(map.clone())).unwrap_err(),
TomlError::Integer(_)
));
map.shift_remove("n");
assert!(matches!(
to_toml(Value::Object(map)).unwrap_err(),
TomlError::Datetime(_)
));
}
#[test]
fn to_toml_reports_every_integer_above_i64_max() {
let value = ir(json!({
"id": 10_000_000_000_000_000_001_u64,
"ok": 42,
"ids": [1, 18_446_744_073_709_551_615_u64],
}));
let err = to_toml(value).unwrap_err();
assert!(matches!(err, TomlError::Integer(_)), "{err}");
assert_eq!(
err.to_string(),
"cannot serialize integer to TOML\n --> id: `10000000000000000001`\n \
--> ids[1]: `18446744073709551615`"
);
}
#[test]
fn a_u64_small_enough_for_an_i64_still_converts() {
let value = Value::Object(Map::from_iter([(
"n".to_string(),
Value::Number(Number::U64(1)),
)]));
let toml = to_toml(value).expect("1 fits an i64");
assert_eq!(toml["n"], toml::Value::Integer(1));
}
#[test]
fn to_json_reports_every_non_finite_float() {
let value = Value::Object(Map::from_iter([
(
"timeout".to_string(),
Value::Number(Number::F64(f64::INFINITY)),
),
("ok".to_string(), Value::Number(Number::F64(1.5))),
(
"backoff".to_string(),
Value::Array(vec![
Value::Number(Number::F64(f64::NEG_INFINITY)),
Value::Number(Number::F64(f64::NAN)),
]),
),
]));
let err = to_json(value).unwrap_err();
assert_eq!(
err.to_string(),
"cannot serialize non-finite number to JSON\n --> timeout: `inf`\n \
--> backoff[0]: `-inf`\n --> backoff[1]: `nan`"
);
}
#[test]
fn the_new_reports_end_without_a_newline() {
let big = ir(json!({"id": 10_000_000_000_000_000_001_u64}));
assert!(!to_toml(big).unwrap_err().to_string().ends_with('\n'));
let inf = Value::Object(Map::from_iter([(
"t".to_string(),
Value::Number(Number::F64(f64::INFINITY)),
)]));
assert!(!to_json(inf).unwrap_err().to_string().ends_with('\n'));
}
#[test]
fn non_finite_floats_are_fine_in_toml() {
let value = Value::Object(Map::from_iter([(
"timeout".to_string(),
Value::Number(Number::F64(f64::INFINITY)),
)]));
assert_eq!(
toml::to_string(&to_toml(value).expect("toml has inf")).expect("serializes"),
"timeout = inf\n"
);
}
#[test]
fn numbers_bools_and_tables_round_trip() {
let src = json!({
"n": 1,
"f": 1.5,
"ok": true,
"name": "svc",
"xs": [1, 2],
"nested": {"k": 3}
});
let toml = to_toml(ir(src.clone())).expect("no nulls");
assert_eq!(json(from_toml(toml)), src);
}
#[test]
fn large_unsigned_integers_survive_json_round_trip() {
let src = json!({"id": 10_000_000_000_000_000_001_u64});
assert_eq!(json(ir(src.clone())), src);
}
#[test]
fn replace_nulls_substitutes_everywhere_and_unblocks_toml() {
let mut v = ir(json!({"a": {"b": null}, "c": [1, null, 3], "d": 2}));
replace_nulls(&mut v, "none");
assert_eq!(
json(v.clone()),
json!({"a": {"b": "none"}, "c": [1, "none", 3], "d": 2})
);
to_toml(v).expect("the substitution left no nulls");
}
#[test]
fn replace_nulls_is_the_identity_without_nulls() {
let src = json!({"a": 1, "xs": [1, 2], "nested": {"k": "v"}});
let mut v = ir(src.clone());
replace_nulls(&mut v, "none");
assert_eq!(json(v), src);
}
}