1use std::fmt;
10
11use knf_core::{Number, Value};
12use knf_dotted::{Seg, render_path};
13
14pub fn from_json(value: serde_json::Value) -> Value {
16 match value {
17 serde_json::Value::Null => Value::Null,
18 serde_json::Value::Bool(b) => Value::Bool(b),
19 serde_json::Value::Number(n) => Value::Number(number_from_json(&n)),
20 serde_json::Value::String(s) => Value::String(s),
21 serde_json::Value::Array(items) => Value::Array(items.into_iter().map(from_json).collect()),
22 serde_json::Value::Object(map) => {
23 Value::Object(map.into_iter().map(|(k, v)| (k, from_json(v))).collect())
24 }
25 }
26}
27
28pub fn to_json(value: Value) -> serde_json::Value {
30 match value {
31 Value::Null => serde_json::Value::Null,
32 Value::Bool(b) => serde_json::Value::Bool(b),
33 Value::Number(n) => serde_json::Value::Number(number_to_json(n)),
34 Value::String(s) | Value::Datetime(s) => serde_json::Value::String(s),
35 Value::Array(items) => serde_json::Value::Array(items.into_iter().map(to_json).collect()),
36 Value::Object(map) => {
37 serde_json::Value::Object(map.into_iter().map(|(k, v)| (k, to_json(v))).collect())
38 }
39 }
40}
41
42pub fn from_toml(value: toml::Value) -> Value {
45 match value {
46 toml::Value::String(s) => Value::String(s),
47 toml::Value::Integer(i) => Value::Number(Number::I64(i)),
48 toml::Value::Float(f) => Value::Number(Number::F64(f)),
49 toml::Value::Boolean(b) => Value::Bool(b),
50 toml::Value::Datetime(dt) => Value::Datetime(dt.to_string()),
51 toml::Value::Array(items) => Value::Array(items.into_iter().map(from_toml).collect()),
52 toml::Value::Table(table) => {
53 Value::Object(table.into_iter().map(|(k, v)| (k, from_toml(v))).collect())
54 }
55 }
56}
57
58pub fn to_toml(value: Value) -> Result<toml::Value, NullInToml> {
65 let mut paths = Vec::new();
66 collect_nulls(&value, &mut Vec::new(), &mut paths);
67 if !paths.is_empty() {
68 return Err(NullInToml { entries: paths });
69 }
70 Ok(to_toml_unchecked(value))
71}
72
73fn to_toml_unchecked(value: Value) -> toml::Value {
74 match value {
75 Value::Null => {
76 unreachable!("nulls are rejected by to_toml before conversion");
77 }
78 Value::Bool(b) => toml::Value::Boolean(b),
79 Value::Number(n) => number_to_toml(n),
80 Value::String(s) => toml::Value::String(s),
81 Value::Datetime(s) => toml::Value::Datetime(
87 s.parse()
88 .expect("a Datetime always originates in the TOML parser, so it re-parses"),
89 ),
90 Value::Array(items) => {
91 toml::Value::Array(items.into_iter().map(to_toml_unchecked).collect())
92 }
93 Value::Object(map) => {
94 let mut table = toml::Table::new();
95 for (k, v) in map {
96 table.insert(k, to_toml_unchecked(v));
97 }
98 toml::Value::Table(table)
99 }
100 }
101}
102
103fn number_from_json(n: &serde_json::Number) -> Number {
104 if let Some(i) = n.as_i64() {
105 Number::I64(i)
106 } else if let Some(u) = n.as_u64() {
107 Number::from_u64(u)
108 } else if let Some(f) = n.as_f64() {
109 Number::F64(f)
110 } else {
111 Number::F64(0.0)
112 }
113}
114
115fn number_to_json(n: Number) -> serde_json::Number {
116 match n {
117 Number::I64(i) => i.into(),
118 Number::U64(u) => u.into(),
119 Number::F64(f) => serde_json::Number::from_f64(f).unwrap_or_else(|| 0.into()),
120 }
121}
122
123fn number_to_toml(n: Number) -> toml::Value {
124 match n {
125 Number::I64(i) => toml::Value::Integer(i),
126 Number::U64(u) => match i64::try_from(u) {
129 Ok(i) => toml::Value::Integer(i),
130 Err(_) => toml::Value::Float(u as f64),
131 },
132 Number::F64(f) => toml::Value::Float(f),
133 }
134}
135
136fn collect_nulls(value: &Value, cur: &mut Vec<Seg>, out: &mut Vec<Vec<Seg>>) {
139 match value {
140 Value::Null => out.push(cur.clone()),
141 Value::Object(obj) => {
142 for (k, v) in obj {
143 cur.push(Seg::Key(k.clone()));
144 collect_nulls(v, cur, out);
145 cur.pop();
146 }
147 }
148 Value::Array(items) => {
149 for (i, v) in items.iter().enumerate() {
150 cur.push(Seg::Index(i));
151 collect_nulls(v, cur, out);
152 cur.pop();
153 }
154 }
155 _ => {}
156 }
157}
158
159pub fn replace_nulls(value: &mut Value, placeholder: &str) {
170 match value {
171 Value::Null => *value = Value::String(placeholder.to_string()),
172 Value::Object(obj) => {
173 for v in obj.values_mut() {
174 replace_nulls(v, placeholder);
175 }
176 }
177 Value::Array(items) => {
178 for v in items.iter_mut() {
179 replace_nulls(v, placeholder);
180 }
181 }
182 _ => {}
183 }
184}
185
186#[derive(Debug)]
197pub struct NullInToml {
198 entries: Vec<Vec<Seg>>,
199}
200
201impl fmt::Display for NullInToml {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 writeln!(f, "cannot serialize null to TOML")?;
204 for path in &self.entries {
205 writeln!(f, " --> {}", render_path(path))?;
206 }
207 write!(
208 f,
209 "help: emit JSON with -f json, substitute with --null-as, or remove the null"
210 )
211 }
212}
213
214impl std::error::Error for NullInToml {}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use serde_json::json;
220
221 fn ir(v: serde_json::Value) -> Value {
222 from_json(v)
223 }
224
225 #[test]
226 fn a_toml_datetime_stays_a_datetime_in_the_ir() {
227 let parsed: toml::Value =
228 toml::from_str("date = 1979-05-27T07:32:00Z\n").expect("valid toml");
229 let v = from_toml(parsed);
230 let Value::Object(map) = &v else {
231 panic!("expected an object, got {v:?}");
232 };
233 assert_eq!(
234 map["date"],
235 Value::Datetime("1979-05-27T07:32:00Z".to_string())
236 );
237 assert_eq!(to_json(v), json!({"date": "1979-05-27T07:32:00Z"}));
239 }
240
241 #[test]
244 fn every_toml_datetime_form_round_trips() {
245 let src = "\
246offset = 1979-05-27T07:32:00Z
247offset_frac = 1979-05-27T00:32:00.999999-07:00
248local = 1979-05-27T07:32:00
249date = 1979-05-27
250time = 07:32:00.5
251";
252 let parsed: toml::Value = toml::from_str(src).expect("valid toml");
253 let back = to_toml(from_toml(parsed.clone())).expect("no nulls");
254 assert_eq!(back, parsed);
255 }
256
257 #[test]
258 fn to_toml_rejects_nulls_with_array_indices() {
259 let err = to_toml(ir(json!({"a": {"b": null}, "c": [1, null], "d": 2}))).unwrap_err();
260 let rendered: Vec<_> = err.entries.iter().map(|p| render_path(p)).collect();
261 assert_eq!(rendered, vec!["a.b", "c[1]"]);
262 }
263
264 #[test]
265 fn numbers_bools_and_tables_round_trip() {
266 let src = json!({
267 "n": 1,
268 "f": 1.5,
269 "ok": true,
270 "name": "svc",
271 "xs": [1, 2],
272 "nested": {"k": 3}
273 });
274 let toml = to_toml(ir(src.clone())).expect("no nulls");
275 assert_eq!(to_json(from_toml(toml)), src);
276 }
277
278 #[test]
280 fn large_unsigned_integers_survive_json_round_trip() {
281 let src = json!({"id": 10_000_000_000_000_000_001_u64});
282 assert_eq!(to_json(ir(src.clone())), src);
283 }
284
285 #[test]
290 fn replace_nulls_substitutes_everywhere_and_unblocks_toml() {
291 let mut v = ir(json!({"a": {"b": null}, "c": [1, null, 3], "d": 2}));
292 replace_nulls(&mut v, "none");
293 assert_eq!(
294 to_json(v.clone()),
295 json!({"a": {"b": "none"}, "c": [1, "none", 3], "d": 2})
296 );
297 to_toml(v).expect("the substitution left no nulls");
298 }
299
300 #[test]
303 fn replace_nulls_is_the_identity_without_nulls() {
304 let src = json!({"a": 1, "xs": [1, 2], "nested": {"k": "v"}});
305 let mut v = ir(src.clone());
306 replace_nulls(&mut v, "none");
307 assert_eq!(to_json(v), src);
308 }
309}