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 {
68 entries: paths.into_iter().map(|path| (path, None)).collect(),
69 });
70 }
71 Ok(to_toml_unchecked(value))
72}
73
74fn to_toml_unchecked(value: Value) -> toml::Value {
75 match value {
76 Value::Null => {
77 unreachable!("nulls are rejected by to_toml before conversion");
78 }
79 Value::Bool(b) => toml::Value::Boolean(b),
80 Value::Number(n) => number_to_toml(n),
81 Value::String(s) => toml::Value::String(s),
82 Value::Datetime(s) => toml::Value::Datetime(
86 s.parse()
87 .expect("a Datetime is only ever produced by the TOML parser, so it re-parses"),
88 ),
89 Value::Array(items) => {
90 toml::Value::Array(items.into_iter().map(to_toml_unchecked).collect())
91 }
92 Value::Object(map) => {
93 let mut table = toml::Table::new();
94 for (k, v) in map {
95 table.insert(k, to_toml_unchecked(v));
96 }
97 toml::Value::Table(table)
98 }
99 }
100}
101
102fn number_from_json(n: &serde_json::Number) -> Number {
103 if let Some(i) = n.as_i64() {
104 Number::I64(i)
105 } else if let Some(u) = n.as_u64() {
106 Number::from_u64(u)
107 } else if let Some(f) = n.as_f64() {
108 Number::F64(f)
109 } else {
110 Number::F64(0.0)
111 }
112}
113
114fn number_to_json(n: Number) -> serde_json::Number {
115 match n {
116 Number::I64(i) => i.into(),
117 Number::U64(u) => u.into(),
118 Number::F64(f) => serde_json::Number::from_f64(f).unwrap_or_else(|| 0.into()),
119 }
120}
121
122fn number_to_toml(n: Number) -> toml::Value {
123 match n {
124 Number::I64(i) => toml::Value::Integer(i),
125 Number::U64(u) => match i64::try_from(u) {
128 Ok(i) => toml::Value::Integer(i),
129 Err(_) => toml::Value::Float(u as f64),
130 },
131 Number::F64(f) => toml::Value::Float(f),
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
139enum Seg {
140 Key(String),
141 Index(usize),
142}
143
144fn render_path(path: &[Seg]) -> String {
145 let mut out = String::new();
146 for seg in path {
147 match seg {
148 Seg::Key(k) => {
149 if !out.is_empty() {
150 out.push('.');
151 }
152 out.push_str(k);
153 }
154 Seg::Index(i) => out.push_str(&format!("[{i}]")),
155 }
156 }
157 out
158}
159
160fn collect_nulls(value: &Value, cur: &mut Vec<Seg>, out: &mut Vec<Vec<Seg>>) {
161 match value {
162 Value::Null => out.push(cur.clone()),
163 Value::Object(obj) => {
164 for (k, v) in obj {
165 cur.push(Seg::Key(k.clone()));
166 collect_nulls(v, cur, out);
167 cur.pop();
168 }
169 }
170 Value::Array(items) => {
171 for (i, v) in items.iter().enumerate() {
172 cur.push(Seg::Index(i));
173 collect_nulls(v, cur, out);
174 cur.pop();
175 }
176 }
177 _ => {}
178 }
179}
180
181fn resolve<'a>(value: &'a Value, path: &[Seg]) -> Option<&'a Value> {
182 let mut cur = value;
183 for seg in path {
184 cur = match (seg, cur) {
185 (Seg::Key(k), Value::Object(obj)) => obj.get(k)?,
186 (Seg::Index(i), Value::Array(items)) => items.get(*i)?,
187 _ => return None,
188 };
189 }
190 Some(cur)
191}
192
193#[derive(Debug)]
199pub struct NullInToml {
200 entries: Vec<(Vec<Seg>, Option<String>)>,
202}
203
204impl NullInToml {
205 pub fn with_origins<S: fmt::Display>(mut self, sources: &[(S, Value)]) -> Self {
207 for (path, origin) in &mut self.entries {
208 *origin = sources
209 .iter()
210 .rev()
211 .find(|(_, v)| resolve(v, path) == Some(&Value::Null))
212 .map(|(name, _)| name.to_string());
213 }
214 self
215 }
216}
217
218impl fmt::Display for NullInToml {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 writeln!(f, "cannot serialize null to TOML")?;
221 let rendered: Vec<(String, &Option<String>)> = self
222 .entries
223 .iter()
224 .map(|(path, origin)| (render_path(path), origin))
225 .collect();
226 let width = rendered
227 .iter()
228 .map(|(path, _)| path.len())
229 .max()
230 .unwrap_or(0);
231 for (path, origin) in &rendered {
232 match origin {
233 Some(origin) => writeln!(f, " --> {path:<width$} (from {origin})")?,
234 None => writeln!(f, " --> {path}")?,
235 }
236 }
237 write!(f, "help: emit JSON with -f json, or remove the null")
238 }
239}
240
241impl std::error::Error for NullInToml {}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use serde_json::json;
247
248 fn ir(v: serde_json::Value) -> Value {
249 from_json(v)
250 }
251
252 #[test]
253 fn a_toml_datetime_stays_a_datetime_in_the_ir() {
254 let parsed: toml::Value =
255 toml::from_str("date = 1979-05-27T07:32:00Z\n").expect("valid toml");
256 let v = from_toml(parsed);
257 let Value::Object(map) = &v else {
258 panic!("expected an object, got {v:?}");
259 };
260 assert_eq!(
261 map["date"],
262 Value::Datetime("1979-05-27T07:32:00Z".to_string())
263 );
264 assert_eq!(to_json(v), json!({"date": "1979-05-27T07:32:00Z"}));
266 }
267
268 #[test]
271 fn every_toml_datetime_form_round_trips() {
272 let src = "\
273offset = 1979-05-27T07:32:00Z
274offset_frac = 1979-05-27T00:32:00.999999-07:00
275local = 1979-05-27T07:32:00
276date = 1979-05-27
277time = 07:32:00.5
278";
279 let parsed: toml::Value = toml::from_str(src).expect("valid toml");
280 let back = to_toml(from_toml(parsed.clone())).expect("no nulls");
281 assert_eq!(back, parsed);
282 }
283
284 #[test]
285 fn to_toml_rejects_nulls_with_array_indices() {
286 let err = to_toml(ir(json!({"a": {"b": null}, "c": [1, null], "d": 2}))).unwrap_err();
287 let rendered: Vec<_> = err.entries.iter().map(|(p, _)| render_path(p)).collect();
288 assert_eq!(rendered, vec!["a.b", "c[1]"]);
289 }
290
291 #[test]
292 fn numbers_bools_and_tables_round_trip() {
293 let src = json!({
294 "n": 1,
295 "f": 1.5,
296 "ok": true,
297 "name": "svc",
298 "xs": [1, 2],
299 "nested": {"k": 3}
300 });
301 let toml = to_toml(ir(src.clone())).expect("no nulls");
302 assert_eq!(to_json(from_toml(toml)), src);
303 }
304
305 #[test]
307 fn large_unsigned_integers_survive_json_round_trip() {
308 let src = json!({"id": 10_000_000_000_000_000_001_u64});
309 assert_eq!(to_json(ir(src.clone())), src);
310 }
311
312 #[test]
313 fn with_origins_names_the_last_writer() {
314 let err = to_toml(ir(json!({"proxy": null}))).unwrap_err();
315 let sources = [
316 ("base.json", ir(json!({"proxy": null}))),
317 ("over.json", ir(json!({"proxy": null}))),
318 ];
319 let err = err.with_origins(&sources);
320 let msg = err.to_string();
321 assert!(msg.contains("from over.json"), "{msg}");
322 assert!(!msg.contains("from base.json"), "{msg}");
323 }
324}