1use std::collections::BTreeMap;
19
20use time::{format_description, macros::format_description as fd};
21
22use crate::{Binary, Value};
23
24mod options;
26pub use options::{BinaryEncoding, Options, QuoteStyle, TimestampPrecision};
27
28pub fn format(value: &Value) -> String {
30 format_impl(value, &Options::compact(), 0)
31}
32
33pub fn format_pretty(value: &Value) -> String {
35 format_impl(value, &Options::pretty(), 0)
36}
37
38pub fn format_with_opts(value: &Value, opts: &Options) -> String {
40 format_impl(value, opts, 0)
41}
42
43fn format_impl(value: &Value, opts: &Options, depth: usize) -> String {
44 match value {
45 Value::Null => "null".to_string(),
46 Value::Bool(b) => b.to_string(),
47 Value::Int(i) => format_int(*i, opts),
48 Value::Float(f) => format_float(*f, opts),
49 Value::String(s) => {
50 let quote = match opts.quote_style {
51 QuoteStyle::Double => '"',
52 QuoteStyle::Single => '\'',
53 QuoteStyle::PreferDouble => {
54 if s.contains('"') && !s.contains('\'') {
55 '\''
56 } else {
57 '"'
58 }
59 }
60 };
61 format_string(s, quote, opts.escape_unicode)
62 }
63 Value::Binary(b) => format_binary(b, opts.binary_encoding),
64 Value::Timestamp(t) => format_timestamp(t, opts),
65 Value::List(items) => {
66 if opts.indent.is_empty() {
67 format_list_compact(items, opts)
68 } else {
69 format_list_pretty(items, opts, depth)
70 }
71 }
72 Value::Map(map) => {
73 if opts.indent.is_empty() {
74 format_map_compact(map, opts)
75 } else {
76 format_map_pretty(map, opts, depth)
77 }
78 }
79 }
80}
81
82fn format_int(i: i64, opts: &Options) -> String {
83 if opts.leading_plus && i >= 0 {
84 format!("+{}", i)
85 } else {
86 i.to_string()
87 }
88}
89
90fn format_float(f: f64, opts: &Options) -> String {
91 let base_string = if f.is_infinite() {
92 if f.is_sign_negative() {
93 "-inf".to_string()
94 } else {
95 "inf".to_string()
96 }
97 } else if f.is_nan() {
98 "nan".to_string()
99 } else if f.fract() == 0.0 && f.abs() < 1e15 {
100 format!("{:.1}", f)
102 } else {
103 f.to_string()
104 };
105
106 if opts.leading_plus && !f.is_nan() && !base_string.starts_with('-') {
108 format!("+{}", base_string)
109 } else {
110 base_string
111 }
112}
113
114const TIMESTAMP_FORMAT_SECONDS: &[format_description::FormatItem<'static>] = fd!(
115 "[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]"
116);
117
118const TIMESTAMP_FORMAT_MILLIS: &[format_description::FormatItem<'static>] = fd!(
119 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3][offset_hour sign:mandatory]:[offset_minute]"
120);
121
122const TIMESTAMP_FORMAT_MICROS: &[format_description::FormatItem<'static>] = fd!(
123 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6][offset_hour sign:mandatory]:[offset_minute]"
124);
125
126const TIMESTAMP_FORMAT_NANOS: &[format_description::FormatItem<'static>] = fd!(
127 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:9][offset_hour sign:mandatory]:[offset_minute]"
128);
129
130fn format_timestamp(t: &crate::Timestamp, opts: &Options) -> String {
131 let format: &[format_description::FormatItem<'_>] = match opts.timestamp_precision {
133 TimestampPrecision::Auto => {
134 let formatted = t
136 .format(&time::format_description::well_known::Rfc3339)
137 .unwrap_or_else(|_| t.to_string());
138
139 let final_str = if !opts.use_zulu && formatted.ends_with('Z') {
141 let mut s = formatted;
142 s.pop();
143 s.push_str("+00:00");
144 s
145 } else {
146 formatted
147 };
148 return format!("ts\"{}\"", final_str);
149 }
150 TimestampPrecision::Seconds => TIMESTAMP_FORMAT_SECONDS,
151 TimestampPrecision::Milliseconds => TIMESTAMP_FORMAT_MILLIS,
152 TimestampPrecision::Microseconds => TIMESTAMP_FORMAT_MICROS,
153 TimestampPrecision::Nanoseconds => TIMESTAMP_FORMAT_NANOS,
154 };
155
156 let formatted = t.format(format).unwrap_or_else(|_| t.to_string());
158 let final_str = if opts.use_zulu && formatted.ends_with("+00:00") {
159 let mut s = formatted;
160 s.truncate(s.len() - 6);
161 s.push('Z');
162 s
163 } else {
164 formatted
165 };
166
167 format!("ts\"{}\"", final_str)
168}
169
170fn format_string(s: &str, quote: char, escape_unicode: bool) -> String {
171 let mut result = String::with_capacity(s.len() + 2);
172 result.push(quote);
173
174 for ch in s.chars() {
175 match ch {
176 '"' if quote == '"' => result.push_str("\\\""),
177 '\'' if quote == '\'' => result.push_str("\\'"),
178 '\\' => result.push_str("\\\\"),
179 '/' => result.push_str("\\/"),
180 '\n' => result.push_str("\\n"),
181 '\t' => result.push_str("\\t"),
182 '\r' => result.push_str("\\r"),
183 '\x08' => result.push_str("\\b"),
184 '\x0C' => result.push_str("\\f"),
185 c if c.is_control() => {
186 use std::fmt::Write;
187 write!(&mut result, "\\u{:04x}", c as u32).unwrap();
188 }
189 c if escape_unicode && !c.is_ascii() => {
190 use std::fmt::Write;
191 let code = c as u32;
192 if code <= 0xFFFF {
193 write!(&mut result, "\\u{:04x}", code).unwrap();
195 } else {
196 let adjusted = code - 0x10000;
198 let high = 0xD800 + (adjusted >> 10);
199 let low = 0xDC00 + (adjusted & 0x3FF);
200 write!(&mut result, "\\u{:04x}\\u{:04x}", high, low).unwrap();
201 }
202 }
203 c => result.push(c),
204 }
205 }
206
207 result.push(quote);
208 result
209}
210
211fn format_binary(binary: &Binary, encoding: BinaryEncoding) -> String {
212 match encoding {
213 BinaryEncoding::Base64 => {
214 use base64::{Engine as _, engine::general_purpose};
215 let encoded = general_purpose::STANDARD.encode(&binary.0);
216 format!("b64\"{}\"", encoded)
217 }
218 BinaryEncoding::Hex => {
219 let hex: String = binary.0.iter().map(|b| format!("{:02x}", b)).collect();
220 format!("hex\"{}\"", hex)
221 }
222 }
223}
224
225fn format_list_compact(items: &[Value], opts: &Options) -> String {
226 if items.is_empty() {
227 return "[]".to_string();
228 }
229
230 let formatted: Vec<String> = items
231 .iter()
232 .map(|item| format_impl(item, opts, 0))
233 .collect();
234 format!("[{}]", formatted.join(","))
235}
236
237fn format_list_pretty(items: &[Value], opts: &Options, depth: usize) -> String {
238 if items.is_empty() {
239 return "[]".to_string();
240 }
241
242 let indent = opts.indent.repeat(depth);
243 let item_indent = opts.indent.repeat(depth + 1);
244 let mut result = String::from("[\n");
245
246 for (i, item) in items.iter().enumerate() {
247 result.push_str(&item_indent);
248 result.push_str(&format_impl(item, opts, depth + 1));
249 if i < items.len() - 1 || opts.trailing_commas {
250 result.push(',');
251 }
252 result.push('\n');
253 }
254
255 result.push_str(&indent);
256 result.push(']');
257 result
258}
259
260fn format_map_compact(map: &BTreeMap<String, Value>, opts: &Options) -> String {
261 if map.is_empty() {
262 return "{}".to_string();
263 }
264
265 let entries: Vec<_> = if opts.sort_keys {
266 let mut sorted: Vec<_> = map.iter().collect();
267 sorted.sort_by_key(|(k, _)| *k);
268 sorted
269 } else {
270 map.iter().collect()
271 };
272
273 let formatted: Vec<String> = entries
274 .iter()
275 .map(|(k, v)| {
276 let key_str = if opts.unquoted_keys && can_be_unquoted(k) {
277 k.to_string()
278 } else {
279 let quote = match opts.quote_style {
280 QuoteStyle::Double => '"',
281 QuoteStyle::Single => '\'',
282 QuoteStyle::PreferDouble => {
283 if k.contains('"') && !k.contains('\'') {
284 '\''
285 } else {
286 '"'
287 }
288 }
289 };
290 format_string(k, quote, opts.escape_unicode)
291 };
292 format!("{}:{}", key_str, format_impl(v, opts, 0))
293 })
294 .collect();
295 format!("{{{}}}", formatted.join(","))
296}
297
298fn format_map_pretty(map: &BTreeMap<String, Value>, opts: &Options, depth: usize) -> String {
299 if map.is_empty() {
300 return "{}".to_string();
301 }
302
303 let indent = opts.indent.repeat(depth);
304 let item_indent = opts.indent.repeat(depth + 1);
305 let mut result = String::from("{\n");
306
307 let entries: Vec<_> = if opts.sort_keys {
308 let mut sorted: Vec<_> = map.iter().collect();
309 sorted.sort_by_key(|(k, _)| *k);
310 sorted
311 } else {
312 map.iter().collect()
313 };
314 for (i, (key, value)) in entries.iter().enumerate() {
315 result.push_str(&item_indent);
316
317 if opts.unquoted_keys && can_be_unquoted(key) {
319 result.push_str(key);
320 } else {
321 let quote = match opts.quote_style {
322 QuoteStyle::Double => '"',
323 QuoteStyle::Single => '\'',
324 QuoteStyle::PreferDouble => {
325 if key.contains('"') && !key.contains('\'') {
326 '\''
327 } else {
328 '"'
329 }
330 }
331 };
332 result.push_str(&format_string(key, quote, opts.escape_unicode));
333 }
334
335 result.push_str(": ");
336 result.push_str(&format_impl(value, opts, depth + 1));
337
338 if i < entries.len() - 1 || opts.trailing_commas {
339 result.push(',');
340 }
341 result.push('\n');
342 }
343
344 result.push_str(&indent);
345 result.push('}');
346 result
347}
348
349fn can_be_unquoted(key: &str) -> bool {
350 if key.is_empty() {
351 return false;
352 }
353
354 if matches!(key, "null" | "true" | "false" | "inf" | "nan") {
356 return false;
357 }
358
359 let mut chars = key.chars();
360 let first = chars.next().unwrap();
361
362 if !first.is_ascii_alphabetic() && first != '_' {
364 return false;
365 }
366
367 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
369}
370
371#[cfg(test)]
372mod tests {
373 use rstest::rstest;
374
375 use super::*;
376 use crate::parse;
377
378 #[rstest]
379 #[case(Value::Null, "null")]
380 #[case(Value::Bool(true), "true")]
381 #[case(Value::Bool(false), "false")]
382 #[case(Value::Int(42), "42")]
383 #[case(Value::Int(-123), "-123")]
384 fn test_format_primitives(#[case] value: Value, #[case] expected: &str) {
385 assert_eq!(format(&value), expected);
386 }
387
388 #[rstest]
389 #[case(3.0, "3.0")]
390 #[case(2.5, "2.5")]
391 #[case(f64::INFINITY, "inf")]
392 #[case(f64::NEG_INFINITY, "-inf")]
393 fn test_format_float(#[case] value: f64, #[case] expected: &str) {
394 assert_eq!(format(&Value::Float(value)), expected);
395 }
396
397 #[test]
398 fn test_format_float_nan() {
399 assert!(format(&Value::Float(f64::NAN)).contains("nan"));
400 }
401
402 #[test]
403 fn test_format_string() {
404 assert_eq!(format(&Value::String("hello".to_string())), "\"hello\"");
405 assert_eq!(
406 format(&Value::String("hello\nworld".to_string())),
407 "\"hello\\nworld\""
408 );
409 assert_eq!(
410 format(&Value::String("tab\there".to_string())),
411 "\"tab\\there\""
412 );
413 }
414
415 #[test]
416 fn test_format_binary() {
417 let binary = Binary(vec![72, 101, 108, 108, 111]); assert_eq!(format(&Value::Binary(binary)), "b64\"SGVsbG8=\"");
419 }
420
421 #[test]
422 fn test_format_list() {
423 let list = vec![Value::Int(1), Value::Int(2), Value::Int(3)];
424 assert_eq!(format(&Value::List(list)), "[1,2,3]");
425
426 assert_eq!(format(&Value::List(vec![])), "[]");
427 }
428
429 #[test]
430 fn test_format_map() {
431 let mut map = BTreeMap::new();
432 map.insert("name".to_string(), Value::String("Alice".to_string()));
433 map.insert("age".to_string(), Value::Int(30));
434
435 let formatted = format(&Value::Map(map));
436 assert!(formatted.contains("age:30"));
438 assert!(formatted.contains("name:\"Alice\""));
439 }
440
441 #[test]
442 fn test_round_trip() {
443 let null = Value::Null;
445 assert_eq!(parse(&format(&null)).unwrap(), null);
446
447 let bool_val = Value::Bool(true);
449 assert_eq!(parse(&format(&bool_val)).unwrap(), bool_val);
450
451 let int_val = Value::Int(42);
453 assert_eq!(parse(&format(&int_val)).unwrap(), int_val);
454
455 let float_val = Value::Float(2.5);
457 assert_eq!(parse(&format(&float_val)).unwrap(), float_val);
458
459 let string_val = Value::String("hello world".to_string());
461 assert_eq!(parse(&format(&string_val)).unwrap(), string_val);
462
463 let list_val = Value::List(vec![Value::Int(1), Value::Int(2)]);
465 assert_eq!(parse(&format(&list_val)).unwrap(), list_val);
466
467 let mut map = BTreeMap::new();
469 map.insert("key".to_string(), Value::Int(42));
470 let map_val = Value::Map(map);
471 assert_eq!(parse(&format(&map_val)).unwrap(), map_val);
472 }
473
474 #[test]
475 fn test_pretty_format() {
476 let mut map = BTreeMap::new();
477 map.insert("name".to_string(), Value::String("Alice".to_string()));
478 map.insert("age".to_string(), Value::Int(30));
479
480 let pretty = format_pretty(&Value::Map(map));
481 assert!(pretty.contains('\n'));
482 assert!(pretty.contains(" "));
483 }
484
485 #[rstest]
486 #[case("hello", true)]
487 #[case("_private", true)]
488 #[case("key123", true)]
489 #[case("_", true)]
490 #[case("", false)]
491 #[case("123", false)]
492 #[case("null", false)]
493 #[case("true", false)]
494 #[case("false", false)]
495 #[case("kebab-case", false)]
496 fn test_can_be_unquoted(#[case] input: &str, #[case] expected: bool) {
497 assert_eq!(can_be_unquoted(input), expected);
498 }
499
500 #[rstest]
501 #[case(Value::Int(42), "+42")]
502 #[case(Value::Int(0), "+0")]
503 #[case(Value::Int(-42), "-42")]
504 #[case(Value::Float(2.5), "+2.5")]
505 #[case(Value::Float(-2.5), "-2.5")]
506 #[case(Value::Float(f64::INFINITY), "+inf")]
507 #[case(Value::Float(f64::NEG_INFINITY), "-inf")]
508 #[case(Value::Float(f64::NAN), "nan")]
509 fn test_leading_plus(#[case] value: Value, #[case] expected: &str) {
510 let opts = Options::compact().with_leading_plus(true);
511 assert_eq!(format_with_opts(&value, &opts), expected);
512 }
513
514 #[rstest]
515 #[case(Value::Int(42), "42")]
516 #[case(Value::Float(2.5), "2.5")]
517 fn test_no_leading_plus(#[case] value: Value, #[case] expected: &str) {
518 let opts = Options::compact();
519 assert_eq!(format_with_opts(&value, &opts), expected);
520 }
521
522 #[test]
523 fn test_sort_keys() {
524 let mut map = BTreeMap::new();
525 map.insert("zebra".to_string(), Value::Int(1));
526 map.insert("apple".to_string(), Value::Int(2));
527 map.insert("banana".to_string(), Value::Int(3));
528
529 let sorted_opts = Options::compact().with_sort_keys(true);
531 let sorted = format_with_opts(&Value::Map(map), &sorted_opts);
532
533 assert_eq!(sorted, "{apple:2,banana:3,zebra:1}");
535
536 let pretty_sorted = Options::pretty().with_sort_keys(true);
538 let mut map2 = BTreeMap::new();
539 map2.insert("z".to_string(), Value::Int(1));
540 map2.insert("a".to_string(), Value::Int(2));
541 let result = format_with_opts(&Value::Map(map2), &pretty_sorted);
542 assert!(result.find("a").unwrap() < result.find("z").unwrap());
543 }
544
545 #[test]
546 fn test_escape_unicode() {
547 let opts = Options::compact().with_escape_unicode(true);
548
549 let ascii = Value::String("hello".to_string());
551 assert_eq!(format_with_opts(&ascii, &opts), "\"hello\"");
552
553 let unicode = Value::String("cafΓ©".to_string());
555 assert_eq!(format_with_opts(&unicode, &opts), "\"caf\\u00e9\"");
556
557 let emoji = Value::String("Hello π".to_string());
559 assert_eq!(format_with_opts(&emoji, &opts), "\"Hello \\ud83c\\udf0d\"");
560
561 let chinese = Value::String("δ½ ε₯½".to_string());
563 assert_eq!(format_with_opts(&chinese, &opts), "\"\\u4f60\\u597d\"");
564
565 let no_escape = Options::compact().with_escape_unicode(false);
567 let result = format_with_opts(&unicode, &no_escape);
568 assert_eq!(result, "\"cafΓ©\"");
569 }
570
571 #[rstest]
572 #[case("π", "\"\\ud83d\\ude00\"")]
573 #[case("π", "\"\\ud83d\\udc4d\"")]
574 #[case("π", "\"\\ud834\\udd1e\"")]
575 #[case("πππ", "\"\\ud83d\\ude00\\ud83d\\ude01\\ud83d\\ude02\"")]
576 #[case("Hello π World", "\"Hello \\ud83d\\ude00 World\"")]
577 #[case("δΈζ", "\"\\u4e2d\\u6587\"")]
578 fn test_surrogate_pair_encoding(#[case] input: &str, #[case] expected: &str) {
579 let opts = Options::compact().with_escape_unicode(true);
580 let value = Value::String(input.to_string());
581 assert_eq!(format_with_opts(&value, &opts), expected);
582 }
583
584 #[rstest]
585 #[case("π")]
586 #[case("π")]
587 #[case("π")]
588 #[case("π")]
589 #[case("Hello π World")]
590 #[case("πππ")]
591 fn test_surrogate_pair_round_trip(#[case] original: &str) {
592 let opts = Options::compact().with_escape_unicode(true);
593 let value = Value::String(original.to_string());
594 let formatted = format_with_opts(&value, &opts);
595 let parsed = crate::parse(&formatted).expect("Failed to parse");
596
597 if let Value::String(s) = parsed {
598 assert_eq!(s, original, "Round-trip failed for: {}", original);
599 } else {
600 panic!("Expected String value");
601 }
602 }
603
604 #[test]
605 fn test_format_timestamp_default() {
606 use crate::Timestamp;
607
608 let ts = Timestamp::from_unix_timestamp(1234567890).unwrap();
609 let value = Value::Timestamp(ts);
610
611 let result = format(&value);
613 assert_eq!(result, "ts\"2009-02-13T23:31:30Z\"");
614 }
615
616 #[rstest]
617 #[case(true, "ts\"2009-02-13T23:31:30Z\"")]
618 #[case(false, "ts\"2009-02-13T23:31:30+00:00\"")]
619 fn test_format_timestamp_zulu(#[case] use_zulu: bool, #[case] expected: &str) {
620 use crate::Timestamp;
621
622 let ts = Timestamp::from_unix_timestamp(1234567890).unwrap();
623 let value = Value::Timestamp(ts);
624 let opts = Options::compact().with_use_zulu(use_zulu);
625 let result = format_with_opts(&value, &opts);
626 assert_eq!(result, expected);
627 }
628
629 #[rstest]
630 #[case(true, "ts\"2009-02-13T23:31:30.123456789Z\"")]
631 #[case(false, "ts\"2009-02-13T23:31:30.123456789+00:00\"")]
632 fn test_format_timestamp_fractional_zulu(#[case] use_zulu: bool, #[case] expected: &str) {
633 use crate::Timestamp;
634
635 let ts = Timestamp::from_unix_timestamp_nanos(1234567890123456789).unwrap();
636 let value = Value::Timestamp(ts);
637 let opts = Options::compact().with_use_zulu(use_zulu);
638 let result = format_with_opts(&value, &opts);
639 assert_eq!(result, expected);
640 }
641
642 #[rstest]
643 #[case(TimestampPrecision::Auto, "ts\"2009-02-13T23:31:30.123456789Z\"")]
644 #[case(TimestampPrecision::Seconds, "ts\"2009-02-13T23:31:30Z\"")]
645 #[case(TimestampPrecision::Milliseconds, "ts\"2009-02-13T23:31:30.123Z\"")]
646 #[case(TimestampPrecision::Microseconds, "ts\"2009-02-13T23:31:30.123456Z\"")]
647 #[case(
648 TimestampPrecision::Nanoseconds,
649 "ts\"2009-02-13T23:31:30.123456789Z\""
650 )]
651 fn test_format_timestamp_precision(
652 #[case] precision: TimestampPrecision,
653 #[case] expected: &str,
654 ) {
655 use crate::Timestamp;
656
657 let ts = Timestamp::from_unix_timestamp_nanos(1234567890123456789).unwrap();
658 let value = Value::Timestamp(ts);
659 let opts = Options::compact().with_timestamp_precision(precision);
660 let result = format_with_opts(&value, &opts);
661 assert_eq!(result, expected);
662 }
663
664 #[test]
665 fn test_format_timestamp_precision_with_offset() {
666 use crate::Timestamp;
667
668 let ts = Timestamp::from_unix_timestamp_nanos(1234567890123456789).unwrap();
669 let value = Value::Timestamp(ts);
670 let opts = Options::compact()
671 .with_timestamp_precision(TimestampPrecision::Milliseconds)
672 .with_use_zulu(false);
673 let result = format_with_opts(&value, &opts);
674 assert_eq!(result, "ts\"2009-02-13T23:31:30.123+00:00\"");
675 }
676
677 #[test]
678 fn test_format_timestamp_precision_padding() {
679 use crate::Timestamp;
680
681 let ts = Timestamp::from_unix_timestamp(1234567890).unwrap();
684 let value = Value::Timestamp(ts);
685 let opts = Options::compact().with_timestamp_precision(TimestampPrecision::Milliseconds);
686 let result = format_with_opts(&value, &opts);
687 assert_eq!(result, "ts\"2009-02-13T23:31:30.000Z\"");
688 }
689}