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 {
33 format_impl(value, &Options::default(), 0, false)
34}
35
36pub fn format_with_opts(value: &Value, opts: &Options) -> String {
38 format_impl(value, opts, 0, false)
39}
40
41fn format_impl(value: &Value, opts: &Options, depth: usize, inline: bool) -> String {
42 match value {
43 Value::Null => "null".to_string(),
44 Value::Bool(b) => b.to_string(),
45 Value::Int(i) => format_int(*i, opts),
46 Value::Float(f) => format_float(*f, opts),
47 Value::String(s) => {
48 let quote = match opts.quote_style {
49 QuoteStyle::Double => '"',
50 QuoteStyle::Single => '\'',
51 QuoteStyle::PreferDouble => {
52 if s.contains('"') && !s.contains('\'') {
53 '\''
54 } else {
55 '"'
56 }
57 }
58 };
59 format_string(s, quote, opts.escape_unicode)
60 }
61 Value::Binary(b) => format_binary(b, opts.binary_encoding),
62 Value::Timestamp(t) => format_timestamp(t, opts),
63 Value::List(items) => format_list(items, opts, depth, inline),
64 Value::Map(map) => format_map(map, opts, depth, inline),
65 }
66}
67
68fn format_int(i: i64, opts: &Options) -> String {
69 if opts.leading_plus && i >= 0 {
70 format!("+{}", i)
71 } else {
72 i.to_string()
73 }
74}
75
76fn format_float(f: f64, opts: &Options) -> String {
77 let base_string = if f.is_infinite() {
78 if f.is_sign_negative() {
79 "-inf".to_string()
80 } else {
81 "inf".to_string()
82 }
83 } else if f.is_nan() {
84 "nan".to_string()
85 } else if f.fract() == 0.0 && f.abs() < 1e15 {
86 format!("{:.1}", f)
88 } else {
89 f.to_string()
90 };
91
92 if opts.leading_plus && !f.is_nan() && !base_string.starts_with('-') {
94 format!("+{}", base_string)
95 } else {
96 base_string
97 }
98}
99
100const TIMESTAMP_FORMAT_SECONDS: &[format_description::FormatItem<'static>] = fd!(
101 "[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]"
102);
103
104const TIMESTAMP_FORMAT_MILLIS: &[format_description::FormatItem<'static>] = fd!(
105 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3][offset_hour sign:mandatory]:[offset_minute]"
106);
107
108const TIMESTAMP_FORMAT_MICROS: &[format_description::FormatItem<'static>] = fd!(
109 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6][offset_hour sign:mandatory]:[offset_minute]"
110);
111
112const TIMESTAMP_FORMAT_NANOS: &[format_description::FormatItem<'static>] = fd!(
113 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:9][offset_hour sign:mandatory]:[offset_minute]"
114);
115
116fn format_timestamp(t: &crate::Timestamp, opts: &Options) -> String {
117 let format: &[format_description::FormatItem<'_>] = match opts.timestamp_precision {
119 TimestampPrecision::Auto => {
120 let formatted = t
122 .format(&time::format_description::well_known::Rfc3339)
123 .unwrap_or_else(|_| t.to_string());
124
125 let final_str = if !opts.use_zulu && formatted.ends_with('Z') {
127 let mut s = formatted;
128 s.pop();
129 s.push_str("+00:00");
130 s
131 } else {
132 formatted
133 };
134 return format!("ts\"{}\"", final_str);
135 }
136 TimestampPrecision::Seconds => TIMESTAMP_FORMAT_SECONDS,
137 TimestampPrecision::Milliseconds => TIMESTAMP_FORMAT_MILLIS,
138 TimestampPrecision::Microseconds => TIMESTAMP_FORMAT_MICROS,
139 TimestampPrecision::Nanoseconds => TIMESTAMP_FORMAT_NANOS,
140 };
141
142 let formatted = t.format(format).unwrap_or_else(|_| t.to_string());
144 let final_str = if opts.use_zulu && formatted.ends_with("+00:00") {
145 let mut s = formatted;
146 s.truncate(s.len() - 6);
147 s.push('Z');
148 s
149 } else {
150 formatted
151 };
152
153 format!("ts\"{}\"", final_str)
154}
155
156fn format_string(s: &str, quote: char, escape_unicode: bool) -> String {
157 let mut result = String::with_capacity(s.len() + 2);
158 result.push(quote);
159
160 for ch in s.chars() {
161 match ch {
162 '"' if quote == '"' => result.push_str("\\\""),
163 '\'' if quote == '\'' => result.push_str("\\'"),
164 '\\' => result.push_str("\\\\"),
165 '/' => result.push_str("\\/"),
166 '\n' => result.push_str("\\n"),
167 '\t' => result.push_str("\\t"),
168 '\r' => result.push_str("\\r"),
169 '\x08' => result.push_str("\\b"),
170 '\x0C' => result.push_str("\\f"),
171 c if c.is_control() => {
172 use std::fmt::Write;
173 write!(&mut result, "\\u{:04x}", c as u32).unwrap();
174 }
175 c if escape_unicode && !c.is_ascii() => {
176 use std::fmt::Write;
177 let code = c as u32;
178 if code <= 0xFFFF {
179 write!(&mut result, "\\u{:04x}", code).unwrap();
181 } else {
182 let adjusted = code - 0x10000;
184 let high = 0xD800 + (adjusted >> 10);
185 let low = 0xDC00 + (adjusted & 0x3FF);
186 write!(&mut result, "\\u{:04x}\\u{:04x}", high, low).unwrap();
187 }
188 }
189 c => result.push(c),
190 }
191 }
192
193 result.push(quote);
194 result
195}
196
197fn format_binary(binary: &Binary, encoding: BinaryEncoding) -> String {
198 match encoding {
199 BinaryEncoding::Base64 => {
200 use base64::{Engine as _, engine::general_purpose};
201 let encoded = general_purpose::STANDARD.encode(&binary.0);
202 format!("b64\"{}\"", encoded)
203 }
204 BinaryEncoding::Hex => {
205 let hex: String = binary.0.iter().map(|b| format!("{:02x}", b)).collect();
206 format!("hex\"{}\"", hex)
207 }
208 }
209}
210
211fn format_list(items: &[Value], opts: &Options, depth: usize, inline: bool) -> String {
212 if items.is_empty() {
213 return "[]".to_string();
215 }
216
217 let indent = " ".repeat(depth);
218 let mut result = String::new();
219
220 for (i, item) in items.iter().enumerate() {
221 if i > 0 || !inline {
222 result.push_str(&indent);
223 }
224 result.push_str("- ");
225
226 match item {
228 Value::List(items) if !items.is_empty() => {
229 result.push('\n');
231 result.push_str(&format_impl(item, opts, depth + 1, false));
232 }
233 Value::Map(m) if !m.is_empty() => {
234 result.push('\n');
236 result.push_str(&format_impl(item, opts, depth + 1, false));
237 }
238 _ => {
239 result.push_str(&format_impl(item, opts, depth + 1, true));
241 result.push('\n');
242 }
243 }
244 }
245
246 result
247}
248
249fn format_map(map: &BTreeMap<String, Value>, opts: &Options, depth: usize, inline: bool) -> String {
250 if map.is_empty() {
251 return "{}".to_string();
253 }
254
255 let indent = " ".repeat(depth);
256 let mut result = String::new();
257
258 let entries: Vec<_> = if opts.sort_keys {
259 let mut sorted: Vec<_> = map.iter().collect();
260 sorted.sort_by_key(|(k, _)| *k);
261 sorted
262 } else {
263 map.iter().collect()
264 };
265
266 for (i, (key, value)) in entries.iter().enumerate() {
267 if i > 0 || !inline {
268 result.push_str(&indent);
269 }
270
271 if opts.unquoted_keys && can_be_unquoted(key) {
273 result.push_str(key);
274 } else {
275 let quote = match opts.quote_style {
276 QuoteStyle::Double => '"',
277 QuoteStyle::Single => '\'',
278 QuoteStyle::PreferDouble => {
279 if key.contains('"') && !key.contains('\'') {
280 '\''
281 } else {
282 '"'
283 }
284 }
285 };
286 result.push_str(&format_string(key, quote, opts.escape_unicode));
287 }
288
289 result.push(':');
290
291 match value {
293 Value::List(items) if !items.is_empty() => {
294 result.push('\n');
296 result.push_str(&format_impl(value, opts, depth + 1, false));
297 }
298 Value::Map(m) if !m.is_empty() => {
299 result.push('\n');
301 result.push_str(&format_impl(value, opts, depth + 1, false));
302 }
303 _ => {
304 result.push(' ');
306 result.push_str(&format_impl(value, opts, depth + 1, true));
307 result.push('\n');
308 }
309 }
310 }
311
312 result
313}
314
315fn can_be_unquoted(key: &str) -> bool {
316 if key.is_empty() {
317 return false;
318 }
319
320 if matches!(key, "null" | "true" | "false" | "inf" | "nan") {
322 return false;
323 }
324
325 let mut chars = key.chars();
326 let first = chars.next().unwrap();
327
328 if !first.is_ascii_alphabetic() && first != '_' {
330 return false;
331 }
332
333 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
335}