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