1use std::fmt::Write;
9
10use serde_json::Value;
11
12#[derive(Clone, Copy, Debug)]
14pub struct Style {
15 pub indent: Option<usize>,
17 pub ensure_ascii: bool,
19}
20
21impl Style {
22 pub const INDENT2: Style = Style {
24 indent: Some(2),
25 ensure_ascii: true,
26 };
27 pub const INDENT2_UTF8: Style = Style {
29 indent: Some(2),
30 ensure_ascii: false,
31 };
32 pub const COMPACT: Style = Style {
34 indent: None,
35 ensure_ascii: true,
36 };
37 pub const COMPACT_UTF8: Style = Style {
39 indent: None,
40 ensure_ascii: false,
41 };
42}
43
44pub const RAW_MARKER: char = '\u{0}';
52
53pub fn py_float(v: f64) -> Value {
56 if v.is_finite() {
57 return serde_json::Number::from_f64(v)
58 .map(Value::Number)
59 .unwrap_or(Value::Null);
60 }
61 let token = if v.is_nan() {
62 "NaN"
63 } else if v > 0.0 {
64 "Infinity"
65 } else {
66 "-Infinity"
67 };
68 Value::String(format!("{RAW_MARKER}{token}"))
69}
70
71pub fn dumps(v: &Value, style: Style) -> String {
73 let mut out = String::new();
74 write_value(&mut out, v, style, 0);
75 out
76}
77
78fn write_value(out: &mut String, v: &Value, style: Style, depth: usize) {
79 match v {
80 Value::Null => out.push_str("null"),
81 Value::Bool(true) => out.push_str("true"),
82 Value::Bool(false) => out.push_str("false"),
83 Value::Number(n) => {
84 if let Some(i) = n.as_i64() {
85 let _ = write!(out, "{i}");
86 } else if let Some(u) = n.as_u64() {
87 let _ = write!(out, "{u}");
88 } else if let Some(f) = n.as_f64() {
89 out.push_str(&float_repr(f));
90 }
91 }
92 Value::String(s) => match s.strip_prefix(RAW_MARKER) {
93 Some(token) => out.push_str(token),
94 None => write_string(out, s, style.ensure_ascii),
95 },
96 Value::Array(items) => {
97 if items.is_empty() {
98 out.push_str("[]");
99 return;
100 }
101 out.push('[');
102 for (i, item) in items.iter().enumerate() {
103 if i > 0 {
104 out.push(',');
105 }
106 newline(out, style, depth + 1);
107 write_value(out, item, style, depth + 1);
108 }
109 newline(out, style, depth);
110 out.push(']');
111 }
112 Value::Object(map) => {
113 if map.is_empty() {
114 out.push_str("{}");
115 return;
116 }
117 out.push('{');
118 for (i, (k, item)) in map.iter().enumerate() {
119 if i > 0 {
120 out.push(',');
121 }
122 newline(out, style, depth + 1);
123 write_string(out, k, style.ensure_ascii);
124 out.push_str(": ");
125 write_value(out, item, style, depth + 1);
126 }
127 newline(out, style, depth);
128 out.push('}');
129 }
130 }
131}
132
133fn newline(out: &mut String, style: Style, depth: usize) {
136 match style.indent {
137 Some(n) => {
138 out.push('\n');
139 for _ in 0..depth * n {
140 out.push(' ');
141 }
142 }
143 None => {
144 if out.ends_with(',') {
147 out.push(' ');
148 }
149 }
150 }
151}
152
153pub fn write_string(out: &mut String, s: &str, ensure_ascii: bool) {
157 out.push('"');
158 for c in s.chars() {
159 match c {
160 '"' => out.push_str("\\\""),
161 '\\' => out.push_str("\\\\"),
162 '\n' => out.push_str("\\n"),
163 '\r' => out.push_str("\\r"),
164 '\t' => out.push_str("\\t"),
165 '\u{08}' => out.push_str("\\b"),
166 '\u{0c}' => out.push_str("\\f"),
167 c if (c as u32) < 0x20 => {
168 let _ = write!(out, "\\u{:04x}", c as u32);
169 }
170 c if ensure_ascii && (c as u32) > 0x7f => {
171 let cp = c as u32;
172 if cp > 0xffff {
173 let v = cp - 0x10000;
174 let hi = 0xd800 + (v >> 10);
175 let lo = 0xdc00 + (v & 0x3ff);
176 let _ = write!(out, "\\u{hi:04x}\\u{lo:04x}");
177 } else {
178 let _ = write!(out, "\\u{cp:04x}");
179 }
180 }
181 c => out.push(c),
182 }
183 }
184 out.push('"');
185}
186
187pub fn float_repr(f: f64) -> String {
191 if f.is_nan() {
192 return "NaN".to_string();
193 }
194 if f.is_infinite() {
195 return if f > 0.0 { "Infinity" } else { "-Infinity" }.to_string();
196 }
197 if f == 0.0 {
198 return if f.is_sign_negative() { "-0.0" } else { "0.0" }.to_string();
199 }
200 let sci = format!("{f:e}");
202 let (mant, exp) = sci
203 .split_once('e')
204 .expect("LowerExp always has an exponent");
205 let exp: i32 = exp.parse().expect("exponent is an integer");
206 let (neg, mant) = match mant.strip_prefix('-') {
207 Some(m) => (true, m),
208 None => (false, mant),
209 };
210 let digits: String = mant.chars().filter(|c| *c != '.').collect();
211 let mut out = String::new();
212 if neg {
213 out.push('-');
214 }
215 if (-4..16).contains(&exp) {
216 let point = exp + 1; if point <= 0 {
219 out.push_str("0.");
220 for _ in 0..(-point) {
221 out.push('0');
222 }
223 out.push_str(&digits);
224 } else if (point as usize) >= digits.len() {
225 out.push_str(&digits);
226 for _ in 0..(point as usize - digits.len()) {
227 out.push('0');
228 }
229 out.push_str(".0");
230 } else {
231 out.push_str(&digits[..point as usize]);
232 out.push('.');
233 out.push_str(&digits[point as usize..]);
234 }
235 } else {
236 out.push_str(&digits[..1]);
237 if digits.len() > 1 {
238 out.push('.');
239 out.push_str(&digits[1..]);
240 }
241 let _ = write!(out, "e{}{:02}", if exp < 0 { '-' } else { '+' }, exp.abs());
242 }
243 out
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use serde_json::json;
250
251 #[test]
252 fn float_repr_matches_python() {
253 let cases = [
254 (1e-4, "0.0001"),
255 (1e-5, "1e-05"),
256 (1e15, "1000000000000000.0"),
257 (1e16, "1e+16"),
258 (0.000123456, "0.000123456"),
259 (1234567890123456.0, "1234567890123456.0"),
260 (12345678901234567.0, "1.2345678901234568e+16"),
261 (-0.0, "-0.0"),
262 (2.5e-7, "2.5e-07"),
263 (5.0, "5.0"),
264 (0.3, "0.3"),
265 (0.1, "0.1"),
266 (100.0, "100.0"),
267 (-1.5, "-1.5"),
268 (123.456e-7, "1.23456e-05"),
269 ];
270 for (f, want) in cases {
271 assert_eq!(float_repr(f), want, "repr({f})");
272 }
273 }
274
275 #[test]
276 fn indent2_layout_matches_python() {
277 let v = json!({"a": [1, 2.0, "x"], "e": {}, "l": [], "n": null, "b": true});
278 let want = "{\n \"a\": [\n 1,\n 2.0,\n \"x\"\n ],\n \"e\": {},\n \"l\": [],\n \"n\": null,\n \"b\": true\n}";
279 assert_eq!(dumps(&v, Style::INDENT2), want);
280 }
281
282 #[test]
283 fn compact_uses_python_separators() {
284 let v = json!({"a": [1, 2], "b": "c"});
285 assert_eq!(dumps(&v, Style::COMPACT), r#"{"a": [1, 2], "b": "c"}"#);
286 assert_eq!(dumps(&json!([]), Style::COMPACT), "[]");
287 assert_eq!(dumps(&json!({}), Style::COMPACT), "{}");
288 }
289
290 #[test]
291 fn non_finite_floats_are_bare_tokens() {
292 let v =
293 json!({"a": py_float(f64::NEG_INFINITY), "b": py_float(1.5), "c": py_float(f64::NAN)});
294 assert_eq!(
295 dumps(&v, Style::COMPACT),
296 r#"{"a": -Infinity, "b": 1.5, "c": NaN}"#
297 );
298 assert_eq!(dumps(&py_float(f64::INFINITY), Style::COMPACT), "Infinity");
299 }
300
301 #[test]
302 fn ensure_ascii_escapes_like_python() {
303 let v = json!(["é", "—", "😀", "tab\there", "q\"", "bs\\", "\u{01}", "a/b"]);
304 assert_eq!(
305 dumps(&v, Style::COMPACT),
306 "[\"\\u00e9\", \"\\u2014\", \"\\ud83d\\ude00\", \"tab\\there\", \"q\\\"\", \"bs\\\\\", \"\\u0001\", \"a/b\"]"
307 );
308 assert_eq!(
309 dumps(&v, Style::COMPACT_UTF8),
310 "[\"\u{e9}\", \"\u{2014}\", \"\u{1F600}\", \"tab\\there\", \"q\\\"\", \"bs\\\\\", \"\\u0001\", \"a/b\"]"
311 );
312 }
313}