Skip to main content

agent_first_data/
formatting.rs

1use crate::cli::OutputFormat;
2use crate::redaction::{OutputOptions, PlainStyle};
3use serde_json::Value;
4
5/// Render a value as a string in the given format with the given options.
6///
7/// The single `value × format × options → String` entry point. JSON and YAML
8/// are structure-preserving and ignore [`PlainStyle`]; plain honors it. All
9/// three redact through `options.redaction` before rendering.
10pub fn render(value: &Value, format: OutputFormat, options: &OutputOptions) -> String {
11    match format {
12        OutputFormat::Json => serialize_json_output(&options.redaction.value(value)),
13        OutputFormat::Yaml => render_yaml(value, options),
14        OutputFormat::Plain => render_plain(value, options),
15    }
16}
17
18pub(crate) fn serialize_json_output(value: &Value) -> String {
19    match serde_json::to_string(value) {
20        Ok(s) => s,
21        Err(err) => serde_json::json!({
22            "error": "output_json_failed",
23            "detail": err.to_string(),
24        })
25        .to_string(),
26    }
27}
28
29/// Format as multi-line YAML with the given output options.
30///
31/// YAML output ignores [`PlainStyle`] and always preserves original keys and values after
32/// redaction, the same structure-preserving semantics as JSON.
33pub(crate) fn render_yaml(value: &Value, output_options: &OutputOptions) -> String {
34    let mut lines = vec!["---".to_string()];
35    let v = output_options.redaction.value(value);
36    render_yaml_raw(&v, 0, &mut lines);
37    lines.join("\n")
38}
39
40/// Format as single-line logfmt with the given output options.
41pub(crate) fn render_plain(value: &Value, output_options: &OutputOptions) -> String {
42    let mut pairs: Vec<(String, String)> = Vec::new();
43    let v = output_options.redaction.value(value);
44    match output_options.style {
45        PlainStyle::Readable => collect_plain_pairs(&v, "", &mut pairs),
46        PlainStyle::Raw => collect_plain_pairs_raw(&v, "", &mut pairs),
47    }
48    pairs.sort_by(|(a, _), (b, _)| a.encode_utf16().cmp(b.encode_utf16()));
49    pairs
50        .into_iter()
51        .map(|(k, v)| format!("{}={}", quote_logfmt_key(&k), quote_logfmt_value(&v)))
52        .collect::<Vec<_>>()
53        .join(" ")
54}
55
56// ═══════════════════════════════════════════
57// Suffix Processing
58// ═══════════════════════════════════════════
59
60/// Strip a suffix matching exact lowercase or exact uppercase only.
61fn strip_suffix_ci(key: &str, suffix_lower: &str) -> Option<String> {
62    if let Some(s) = key.strip_suffix(suffix_lower) {
63        return Some(s.to_string());
64    }
65    let suffix_upper: String = suffix_lower
66        .chars()
67        .map(|c| c.to_ascii_uppercase())
68        .collect();
69    if let Some(s) = key.strip_suffix(&suffix_upper) {
70        return Some(s.to_string());
71    }
72    None
73}
74
75/// Extract currency code from `_{code}_cents` / `_{CODE}_CENTS` pattern.
76fn try_strip_generic_cents(key: &str) -> Option<(String, String)> {
77    let code = extract_currency_code(key)?;
78    let suffix_len = code.len() + "_cents".len() + 1; // _{code}_cents
79    let stripped = &key[..key.len() - suffix_len];
80    if stripped.is_empty() {
81        return None;
82    }
83    Some((stripped.to_string(), code.to_string()))
84}
85
86/// Extract currency code from `_{code}_micro` / `_{CODE}_MICRO` pattern.
87fn try_strip_generic_micro(key: &str) -> Option<(String, String)> {
88    let code = extract_currency_code_micro(key)?;
89    let suffix_len = code.len() + "_micro".len() + 1; // _{code}_micro
90    let stripped = &key[..key.len() - suffix_len];
91    if stripped.is_empty() {
92        return None;
93    }
94    Some((stripped.to_string(), code.to_string()))
95}
96
97/// Try suffix-driven processing. Returns Some((stripped_key, formatted_value))
98/// when suffix matches and type is valid. None for no match or type mismatch.
99/// Accept an integer value, including an integral-valued float (`3.0` → `3`).
100/// Non-integral floats and out-of-range values return `None`. This keeps the
101/// four language implementations consistent: JS/TS cannot distinguish `3` from
102/// `3.0` after JSON parsing, so the value's integrality — not its lexical form —
103/// decides whether an integer-required suffix applies.
104fn as_int(value: &Value) -> Option<i64> {
105    if let Some(i) = value.as_i64() {
106        return Some(i);
107    }
108    let f = value.as_f64()?;
109    if f.is_finite() && f.fract() == 0.0 && (i64::MIN as f64..=i64::MAX as f64).contains(&f) {
110        return Some(f as i64);
111    }
112    None
113}
114
115/// Like [`as_int`] but for non-negative integers (rejects negatives).
116fn as_uint(value: &Value) -> Option<u64> {
117    if let Some(u) = value.as_u64() {
118        return Some(u);
119    }
120    let f = value.as_f64()?;
121    if f.is_finite() && f.fract() == 0.0 && (0.0..=u64::MAX as f64).contains(&f) {
122        return Some(f as u64);
123    }
124    None
125}
126
127fn integer_text(value: &Value) -> Option<String> {
128    match value {
129        Value::Number(_) => as_int(value).map(|n| n.to_string()),
130        Value::String(s) if is_decimal_integer_string(s) => Some(s.clone()),
131        _ => None,
132    }
133}
134
135fn is_decimal_integer_string(s: &str) -> bool {
136    let digits = s.strip_prefix('-').unwrap_or(s);
137    !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit())
138}
139
140fn epoch_ns_to_ms(value: &Value) -> Option<i64> {
141    let ns = match value {
142        Value::Number(_) => i128::from(as_int(value)?),
143        Value::String(s) if is_decimal_integer_string(s) => s.parse::<i128>().ok()?,
144        _ => return None,
145    };
146    ns.div_euclid(1_000_000).try_into().ok()
147}
148
149fn try_process_field(key: &str, value: &Value) -> Option<(String, String)> {
150    // Group 1: compound timestamp suffixes
151    if let Some(stripped) = strip_suffix_ci(key, "_epoch_ms") {
152        return as_int(value)
153            .and_then(|ms| format_rfc3339_ms(ms).map(|formatted| (stripped, formatted)));
154    }
155    if let Some(stripped) = strip_suffix_ci(key, "_epoch_s") {
156        return as_int(value)
157            .and_then(|s| s.checked_mul(1000))
158            .and_then(|ms| format_rfc3339_ms(ms).map(|formatted| (stripped, formatted)));
159    }
160    if let Some(stripped) = strip_suffix_ci(key, "_epoch_ns") {
161        return epoch_ns_to_ms(value)
162            .and_then(|ms| format_rfc3339_ms(ms).map(|formatted| (stripped, formatted)));
163    }
164
165    // Group 2: compound currency suffixes
166    if let Some(stripped) = strip_suffix_ci(key, "_usd_cents") {
167        return as_uint(value).map(|n| (stripped, format!("${}.{:02}", n / 100, n % 100)));
168    }
169    if let Some(stripped) = strip_suffix_ci(key, "_eur_cents") {
170        return as_uint(value).map(|n| (stripped, format!("€{}.{:02}", n / 100, n % 100)));
171    }
172    if let Some((stripped, code)) = try_strip_generic_cents(key) {
173        return as_uint(value).map(|n| {
174            (
175                stripped,
176                format!("{}.{:02} {}", n / 100, n % 100, code.to_uppercase()),
177            )
178        });
179    }
180    if let Some((stripped, code)) = try_strip_generic_micro(key) {
181        return as_uint(value).map(|n| {
182            (
183                stripped,
184                format!(
185                    "{}.{:06} {}",
186                    n / 1_000_000,
187                    n % 1_000_000,
188                    code.to_uppercase()
189                ),
190            )
191        });
192    }
193
194    // Group 3: multi-char suffixes
195    if let Some(stripped) = strip_suffix_ci(key, "_rfc3339") {
196        return value.as_str().map(|s| (stripped, s.to_string()));
197    }
198    if let Some(stripped) = strip_suffix_ci(key, "_minutes") {
199        return value
200            .is_number()
201            .then(|| (stripped, format!("{} minutes", number_str(value))));
202    }
203    if let Some(stripped) = strip_suffix_ci(key, "_hours") {
204        return value
205            .is_number()
206            .then(|| (stripped, format!("{} hours", number_str(value))));
207    }
208    if let Some(stripped) = strip_suffix_ci(key, "_days") {
209        return value
210            .is_number()
211            .then(|| (stripped, format!("{} days", number_str(value))));
212    }
213
214    // Group 4: single-unit suffixes
215    if let Some(stripped) = strip_suffix_ci(key, "_msats") {
216        return integer_text(value).map(|n| (stripped, format!("{n}msats")));
217    }
218    if let Some(stripped) = strip_suffix_ci(key, "_sats") {
219        return integer_text(value).map(|n| (stripped, format!("{n}sats")));
220    }
221    if let Some(stripped) = strip_suffix_ci(key, "_bytes") {
222        return as_uint(value).map(|n| (stripped, format_bytes_human(n)));
223    }
224    if let Some(stripped) = strip_suffix_ci(key, "_percent") {
225        return value
226            .is_number()
227            .then(|| (stripped, format!("{}%", number_str(value))));
228    }
229    // Group 5: short suffixes (last to avoid false positives)
230    if let Some(stripped) = strip_suffix_ci(key, "_jpy") {
231        return as_uint(value).map(|n| (stripped, format!("¥{}", format_with_commas(n))));
232    }
233    if let Some(stripped) = strip_suffix_ci(key, "_ns") {
234        return value
235            .is_number()
236            .then(|| (stripped, format!("{}ns", number_str(value))));
237    }
238    if let Some(stripped) = strip_suffix_ci(key, "_us") {
239        return value
240            .is_number()
241            .then(|| (stripped, format!("{}μs", number_str(value))));
242    }
243    if let Some(stripped) = strip_suffix_ci(key, "_ms") {
244        return format_ms_value(value).map(|v| (stripped, v));
245    }
246    if let Some(stripped) = strip_suffix_ci(key, "_s") {
247        return value
248            .is_number()
249            .then(|| (stripped, format!("{}s", number_str(value))));
250    }
251
252    None
253}
254
255/// Process object fields: strip keys, format values, detect collisions.
256fn process_object_fields<'a>(
257    map: &'a serde_json::Map<String, Value>,
258) -> Vec<(String, &'a Value, Option<String>)> {
259    let mut entries: Vec<(String, &'a str, &'a Value, Option<String>)> = Vec::new();
260    for (key, value) in map {
261        if let Some(stripped) = strip_suffix_ci(key, "_secret") {
262            entries.push((stripped, key.as_str(), value, None));
263            continue;
264        }
265        match try_process_field(key, value) {
266            Some((stripped, formatted)) => {
267                entries.push((stripped, key.as_str(), value, Some(formatted)));
268            }
269            None => {
270                entries.push((key.clone(), key.as_str(), value, None));
271            }
272        }
273    }
274
275    // Detect collisions
276    let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
277    for (stripped, _, _, _) in &entries {
278        *counts.entry(stripped.clone()).or_insert(0) += 1;
279    }
280
281    // Resolve collisions: revert both key and formatted value
282    let mut result: Vec<(String, &'a Value, Option<String>)> = entries
283        .into_iter()
284        .map(|(stripped, original, value, formatted)| {
285            if counts.get(&stripped).copied().unwrap_or(0) > 1 && original != stripped.as_str() {
286                (original.to_string(), value, None)
287            } else {
288                (stripped, value, formatted)
289            }
290        })
291        .collect();
292
293    result.sort_by(|(a, _, _), (b, _, _)| a.encode_utf16().cmp(b.encode_utf16()));
294    result
295}
296
297// ═══════════════════════════════════════════
298// Formatting Helpers
299// ═══════════════════════════════════════════
300
301fn number_str(value: &Value) -> String {
302    match value {
303        Value::Number(n) => format_number(n),
304        _ => String::new(),
305    }
306}
307
308/// Render a JSON number canonically for YAML/plain output: an integral-valued
309/// float drops its trailing `.0` so `3.0` and `3` both render as `3`. This
310/// matches Go (`strconv.FormatFloat(_, 'f', -1, 64)`), TypeScript
311/// (`Number.prototype.toString`), and Python (`int(v)` for integral floats),
312/// keeping the four implementations byte-identical.
313fn format_number(n: &serde_json::Number) -> String {
314    if n.is_f64()
315        && let Some(f) = n.as_f64()
316        && f.is_finite()
317        && f.fract() == 0.0
318        && f.abs() < 1e21
319    {
320        return format!("{f:.0}");
321    }
322    normalize_exponent(&n.to_string())
323}
324
325fn normalize_exponent(s: &str) -> String {
326    let Some(e) = s.find(['e', 'E']) else {
327        return s.to_string();
328    };
329    let mantissa = &s[..e];
330    let mut exp = &s[e + 1..];
331    let mut sign = "";
332    if exp.starts_with(['+', '-']) {
333        sign = &exp[..1];
334        exp = &exp[1..];
335    }
336    let exp = exp.trim_start_matches('0');
337    let exp = if exp.is_empty() { "0" } else { exp };
338    format!("{mantissa}e{sign}{exp}")
339}
340
341/// Format ms as seconds: 3 decimal places, trim trailing zeros, min 1 decimal.
342fn format_ms_as_seconds(ms: f64) -> String {
343    let formatted = format!("{:.3}", ms / 1000.0);
344    let trimmed = formatted.trim_end_matches('0');
345    if trimmed.ends_with('.') {
346        format!("{}0s", trimmed)
347    } else {
348        format!("{}s", trimmed)
349    }
350}
351
352/// Format `_ms` value: < 1000 → `{n}ms`, ≥ 1000 → seconds.
353fn format_ms_value(value: &Value) -> Option<String> {
354    let n = value.as_f64()?;
355    if n.abs() >= 1000.0 {
356        Some(format_ms_as_seconds(n))
357    } else if let Some(i) = value.as_i64() {
358        Some(format!("{}ms", i))
359    } else {
360        Some(format!("{}ms", number_str(value)))
361    }
362}
363
364/// Convert unix milliseconds (signed) to RFC 3339 with UTC timezone.
365const MIN_RFC3339_MS: i64 = -62135596800000;
366const MAX_RFC3339_MS: i64 = 253402300799999;
367
368fn format_rfc3339_ms(ms: i64) -> Option<String> {
369    use chrono::{DateTime, Utc};
370    if !(MIN_RFC3339_MS..=MAX_RFC3339_MS).contains(&ms) {
371        return None;
372    }
373    let secs = ms.div_euclid(1000);
374    let nanos = (ms.rem_euclid(1000) * 1_000_000) as u32;
375    DateTime::from_timestamp(secs, nanos).map(|dt| {
376        dt.with_timezone(&Utc)
377            .to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
378    })
379}
380
381/// Format non-negative bytes as human-readable binary size.
382pub(crate) fn format_bytes_human(bytes: u64) -> String {
383    const KIB: f64 = 1024.0;
384    const MIB: f64 = KIB * 1024.0;
385    const GIB: f64 = MIB * 1024.0;
386    const TIB: f64 = GIB * 1024.0;
387
388    let b = bytes as f64;
389    if b >= TIB {
390        format!("{:.1}TiB", b / TIB)
391    } else if b >= GIB {
392        format!("{:.1}GiB", b / GIB)
393    } else if b >= MIB {
394        format!("{:.1}MiB", b / MIB)
395    } else if b >= KIB {
396        format!("{:.1}KiB", b / KIB)
397    } else {
398        format!("{bytes}B")
399    }
400}
401
402/// Format a number with thousands separators.
403pub(crate) fn format_with_commas(n: u64) -> String {
404    let s = n.to_string();
405    let mut result = String::with_capacity(s.len() + s.len() / 3);
406    for (i, c) in s.chars().enumerate() {
407        if i > 0 && (s.len() - i).is_multiple_of(3) {
408            result.push(',');
409        }
410        result.push(c);
411    }
412    result
413}
414
415/// Extract currency code from a `_{code}_cents` / `_{CODE}_CENTS` suffix.
416pub(crate) fn extract_currency_code(key: &str) -> Option<&str> {
417    let without_cents = key
418        .strip_suffix("_cents")
419        .or_else(|| key.strip_suffix("_CENTS"))?;
420    extract_currency_code_from_stem(without_cents)
421}
422
423/// Extract currency code from a `_{code}_micro` / `_{CODE}_MICRO` suffix.
424fn extract_currency_code_micro(key: &str) -> Option<&str> {
425    let without_micro = key
426        .strip_suffix("_micro")
427        .or_else(|| key.strip_suffix("_MICRO"))?;
428    extract_currency_code_from_stem(without_micro)
429}
430
431fn extract_currency_code_from_stem(stem: &str) -> Option<&str> {
432    let last_underscore = stem.rfind('_')?;
433    let code = &stem[last_underscore + 1..];
434    if code.is_empty()
435        || !(3..=4).contains(&code.len())
436        || !code.bytes().all(|b| b.is_ascii_alphabetic())
437    {
438        return None;
439    }
440    Some(code)
441}
442
443// ═══════════════════════════════════════════
444// YAML Rendering (structure-preserving)
445// ═══════════════════════════════════════════
446
447fn render_yaml_raw(value: &Value, indent: usize, lines: &mut Vec<String>) {
448    let prefix = "  ".repeat(indent);
449    match value {
450        Value::Object(map) => {
451            for key in sorted_value_keys(map) {
452                render_yaml_field_raw(&prefix, &key, &map[&key], indent, lines);
453            }
454        }
455        Value::Array(arr) => {
456            render_yaml_array_raw(arr, indent, lines);
457        }
458        _ => {
459            lines.push(format!("{}{}", prefix, yaml_scalar(value)));
460        }
461    }
462}
463
464fn render_yaml_field_raw(
465    prefix: &str,
466    key: &str,
467    value: &Value,
468    indent: usize,
469    lines: &mut Vec<String>,
470) {
471    match value {
472        Value::Object(inner) if !inner.is_empty() => {
473            lines.push(format!("{}{}:", prefix, yaml_key(key)));
474            render_yaml_raw(value, indent + 1, lines);
475        }
476        Value::Object(_) => {
477            lines.push(format!("{}{}: {{}}", prefix, yaml_key(key)));
478        }
479        Value::Array(arr) => {
480            if arr.is_empty() {
481                lines.push(format!("{}{}: []", prefix, yaml_key(key)));
482            } else {
483                lines.push(format!("{}{}:", prefix, yaml_key(key)));
484                render_yaml_array_raw(arr, indent + 1, lines);
485            }
486        }
487        _ => {
488            lines.push(format!(
489                "{}{}: {}",
490                prefix,
491                yaml_key(key),
492                yaml_scalar(value)
493            ));
494        }
495    }
496}
497
498fn render_yaml_array_raw(arr: &[Value], indent: usize, lines: &mut Vec<String>) {
499    let prefix = "  ".repeat(indent);
500    for item in arr {
501        match item {
502            Value::Object(inner) if !inner.is_empty() => {
503                lines.push(format!("{}-", prefix));
504                render_yaml_raw(item, indent + 1, lines);
505            }
506            Value::Array(nested) if !nested.is_empty() => {
507                lines.push(format!("{}-", prefix));
508                render_yaml_array_raw(nested, indent + 1, lines);
509            }
510            Value::Object(_) => {
511                lines.push(format!("{}- {{}}", prefix));
512            }
513            Value::Array(_) => {
514                lines.push(format!("{}- []", prefix));
515            }
516            _ => {
517                lines.push(format!("{}- {}", prefix, yaml_scalar(item)));
518            }
519        }
520    }
521}
522
523fn escape_yaml_str(s: &str) -> String {
524    s.replace('\\', "\\\\")
525        .replace('"', "\\\"")
526        .replace('\n', "\\n")
527        .replace('\r', "\\r")
528        .replace('\t', "\\t")
529        .replace('\x0c', "\\f")
530        .replace('\x0b', "\\v")
531}
532
533fn yaml_key(key: &str) -> String {
534    if is_safe_key(key) {
535        key.to_string()
536    } else {
537        format!("\"{}\"", escape_yaml_str(key))
538    }
539}
540
541fn quote_logfmt_key(key: &str) -> String {
542    if is_safe_key(key) {
543        key.to_string()
544    } else {
545        quote_logfmt_value(key)
546    }
547}
548
549fn is_safe_key(key: &str) -> bool {
550    !key.is_empty()
551        && key
552            .bytes()
553            .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.'))
554}
555
556fn yaml_scalar(value: &Value) -> String {
557    match value {
558        Value::String(s) => format!("\"{}\"", escape_yaml_str(s)),
559        Value::Null => "null".to_string(),
560        Value::Bool(b) => b.to_string(),
561        Value::Number(n) => format_number(n),
562        Value::Object(_) | Value::Array(_) => {
563            format!("\"{}\"", escape_yaml_str(&canonical_json(value)))
564        }
565    }
566}
567
568// ═══════════════════════════════════════════
569// Plain Rendering (logfmt)
570// ═══════════════════════════════════════════
571
572fn collect_plain_pairs(value: &Value, prefix: &str, pairs: &mut Vec<(String, String)>) {
573    if let Value::Object(map) = value {
574        let processed = process_object_fields(map);
575        for (display_key, v, formatted) in processed {
576            let full_key = if prefix.is_empty() {
577                display_key
578            } else {
579                format!("{}.{}", prefix, display_key)
580            };
581            if let Some(fv) = formatted {
582                pairs.push((full_key, fv));
583            } else {
584                match v {
585                    Value::Object(_) => collect_plain_pairs(v, &full_key, pairs),
586                    Value::Array(arr) => {
587                        let joined = arr.iter().map(plain_scalar).collect::<Vec<_>>().join(",");
588                        pairs.push((full_key, joined));
589                    }
590                    Value::Null => pairs.push((full_key, String::new())),
591                    _ => pairs.push((full_key, plain_scalar(v))),
592                }
593            }
594        }
595    }
596}
597
598fn collect_plain_pairs_raw(value: &Value, prefix: &str, pairs: &mut Vec<(String, String)>) {
599    if let Value::Object(map) = value {
600        for key in sorted_value_keys(map) {
601            let v = &map[&key];
602            let full_key = if prefix.is_empty() {
603                key.clone()
604            } else {
605                format!("{}.{}", prefix, key)
606            };
607            match v {
608                Value::Object(_) => collect_plain_pairs_raw(v, &full_key, pairs),
609                Value::Array(arr) => {
610                    let joined = arr.iter().map(plain_scalar).collect::<Vec<_>>().join(",");
611                    pairs.push((full_key, joined));
612                }
613                Value::Null => pairs.push((full_key, String::new())),
614                _ => pairs.push((full_key, plain_scalar(v))),
615            }
616        }
617    }
618}
619
620fn plain_scalar(value: &Value) -> String {
621    match value {
622        Value::String(s) => s.clone(),
623        Value::Null => "null".to_string(),
624        Value::Bool(b) => b.to_string(),
625        Value::Number(n) => format_number(n),
626        Value::Object(_) | Value::Array(_) => canonical_json(value),
627    }
628}
629
630fn quote_logfmt_value(value: &str) -> String {
631    if value.is_empty() {
632        return String::new();
633    }
634    if !value
635        .chars()
636        .any(|c| c.is_whitespace() || matches!(c, '=' | '"' | '\\'))
637    {
638        return value.to_string();
639    }
640    let escaped = value
641        .replace('\\', "\\\\")
642        .replace('"', "\\\"")
643        .replace('\n', "\\n")
644        .replace('\r', "\\r")
645        .replace('\t', "\\t")
646        .replace('\x0c', "\\f")
647        .replace('\x0b', "\\v");
648    format!("\"{}\"", escaped)
649}
650
651fn canonical_json(value: &Value) -> String {
652    serde_json::to_string(&sort_json_value(value))
653        .unwrap_or_else(|_| "<unsupported:json>".to_string())
654}
655
656fn sort_json_value(value: &Value) -> Value {
657    match value {
658        Value::Object(map) => {
659            let mut out = serde_json::Map::new();
660            for key in sorted_value_keys(map) {
661                if let Some(v) = map.get(&key) {
662                    out.insert(key, sort_json_value(v));
663                }
664            }
665            Value::Object(out)
666        }
667        Value::Array(arr) => Value::Array(arr.iter().map(sort_json_value).collect()),
668        _ => value.clone(),
669    }
670}
671
672fn sorted_value_keys(map: &serde_json::Map<String, Value>) -> Vec<String> {
673    let mut keys: Vec<String> = map.keys().cloned().collect();
674    keys.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16()));
675    keys
676}