1#[cfg(feature = "tracing")]
12pub mod afdata_tracing;
13
14use serde_json::Value;
15
16pub fn build_json_ok(result: Value, trace: Option<Value>) -> Value {
22 match trace {
23 Some(t) => serde_json::json!({"code": "ok", "result": result, "trace": t}),
24 None => serde_json::json!({"code": "ok", "result": result}),
25 }
26}
27
28pub fn build_json_error(message: &str, hint: Option<&str>, trace: Option<Value>) -> Value {
30 let mut obj = serde_json::Map::new();
31 obj.insert("code".to_string(), Value::String("error".to_string()));
32 obj.insert("error".to_string(), Value::String(message.to_string()));
33 if let Some(h) = hint {
34 obj.insert("hint".to_string(), Value::String(h.to_string()));
35 }
36 if let Some(t) = trace {
37 obj.insert("trace".to_string(), t);
38 }
39 Value::Object(obj)
40}
41
42pub fn build_json(code: &str, fields: Value, trace: Option<Value>) -> Value {
44 let mut obj = match fields {
45 Value::Object(map) => map,
46 _ => serde_json::Map::new(),
47 };
48 obj.insert("code".to_string(), Value::String(code.to_string()));
49 if let Some(t) = trace {
50 obj.insert("trace".to_string(), t);
51 }
52 Value::Object(obj)
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum RedactionPolicy {
62 RedactionTraceOnly,
64 RedactionNone,
66}
67
68pub fn output_json(value: &Value) -> String {
70 let mut v = value.clone();
71 redact_secrets(&mut v);
72 serialize_json_output(&v)
73}
74
75pub fn output_json_with(value: &Value, redaction_policy: RedactionPolicy) -> String {
77 let mut v = value.clone();
78 apply_redaction_policy(&mut v, redaction_policy);
79 serialize_json_output(&v)
80}
81
82fn serialize_json_output(value: &Value) -> String {
83 match serde_json::to_string(value) {
84 Ok(s) => s,
85 Err(err) => serde_json::json!({
86 "error": "output_json_failed",
87 "detail": err.to_string(),
88 })
89 .to_string(),
90 }
91}
92
93pub fn output_yaml(value: &Value) -> String {
95 let mut lines = vec!["---".to_string()];
96 render_yaml_processed(value, 0, &mut lines);
97 lines.join("\n")
98}
99
100pub fn output_plain(value: &Value) -> String {
102 let mut pairs: Vec<(String, String)> = Vec::new();
103 collect_plain_pairs(value, "", &mut pairs);
104 pairs.sort_by(|(a, _), (b, _)| a.encode_utf16().cmp(b.encode_utf16()));
105 pairs
106 .into_iter()
107 .map(|(k, v)| {
108 if v.contains(' ') {
109 format!("{}=\"{}\"", k, v)
110 } else {
111 format!("{}={}", k, v)
112 }
113 })
114 .collect::<Vec<_>>()
115 .join(" ")
116}
117
118pub fn internal_redact_secrets(value: &mut Value) {
124 redact_secrets(value);
125}
126
127pub fn parse_size(s: &str) -> Option<u64> {
133 let s = s.trim();
134 if s.is_empty() {
135 return None;
136 }
137 let last = *s.as_bytes().last()?;
138 let (num_str, mult) = match last {
139 b'B' | b'b' => (&s[..s.len() - 1], 1u64),
140 b'K' | b'k' => (&s[..s.len() - 1], 1024),
141 b'M' | b'm' => (&s[..s.len() - 1], 1024 * 1024),
142 b'G' | b'g' => (&s[..s.len() - 1], 1024 * 1024 * 1024),
143 b'T' | b't' => (&s[..s.len() - 1], 1024u64 * 1024 * 1024 * 1024),
144 b'0'..=b'9' | b'.' => (s, 1),
145 _ => return None,
146 };
147 if num_str.is_empty() {
148 return None;
149 }
150 if let Ok(n) = num_str.parse::<u64>() {
151 return n.checked_mul(mult);
152 }
153 if !num_str.contains('.') && !num_str.contains('e') && !num_str.contains('E') {
155 return None;
156 }
157 let f: f64 = num_str.parse().ok()?;
158 if f < 0.0 || f.is_nan() || f.is_infinite() {
159 return None;
160 }
161 let result = f * mult as f64;
162 if result >= u64::MAX as f64 {
163 return None;
164 }
165 Some(result as u64)
166}
167
168#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174pub enum OutputFormat {
175 Json,
176 Yaml,
177 Plain,
178}
179
180pub fn cli_parse_output(s: &str) -> Result<OutputFormat, String> {
190 match s {
191 "json" => Ok(OutputFormat::Json),
192 "yaml" => Ok(OutputFormat::Yaml),
193 "plain" => Ok(OutputFormat::Plain),
194 _ => Err(format!(
195 "invalid --output format '{s}': expected json, yaml, or plain"
196 )),
197 }
198}
199
200pub fn cli_parse_log_filters<S: AsRef<str>>(entries: &[S]) -> Vec<String> {
210 let mut out: Vec<String> = Vec::new();
211 for entry in entries {
212 let s = entry.as_ref().trim().to_ascii_lowercase();
213 if !s.is_empty() && !out.contains(&s) {
214 out.push(s);
215 }
216 }
217 out
218}
219
220pub fn cli_output(value: &Value, format: OutputFormat) -> String {
231 match format {
232 OutputFormat::Json => output_json(value),
233 OutputFormat::Yaml => output_yaml(value),
234 OutputFormat::Plain => output_plain(value),
235 }
236}
237
238pub fn build_cli_error(message: &str, hint: Option<&str>) -> Value {
250 let mut obj = serde_json::Map::new();
251 obj.insert("code".to_string(), Value::String("error".to_string()));
252 obj.insert(
253 "error_code".to_string(),
254 Value::String("invalid_request".to_string()),
255 );
256 obj.insert("error".to_string(), Value::String(message.to_string()));
257 if let Some(h) = hint {
258 obj.insert("hint".to_string(), Value::String(h.to_string()));
259 }
260 obj.insert("retryable".to_string(), Value::Bool(false));
261 obj.insert(
262 "trace".to_string(),
263 serde_json::json!({"duration_ms": 0}),
264 );
265 Value::Object(obj)
266}
267
268fn redact_secrets(value: &mut Value) {
273 match value {
274 Value::Object(map) => {
275 let keys: Vec<String> = map.keys().cloned().collect();
276 for key in keys {
277 if key.ends_with("_secret") || key.ends_with("_SECRET") {
278 match map.get(&key) {
279 Some(Value::Object(_)) | Some(Value::Array(_)) => {
280 }
282 _ => {
283 map.insert(key.clone(), Value::String("***".into()));
284 continue;
285 }
286 }
287 }
288 if let Some(v) = map.get_mut(&key) {
289 redact_secrets(v);
290 }
291 }
292 }
293 Value::Array(arr) => {
294 for v in arr {
295 redact_secrets(v);
296 }
297 }
298 _ => {}
299 }
300}
301
302fn apply_redaction_policy(value: &mut Value, redaction_policy: RedactionPolicy) {
303 match redaction_policy {
304 RedactionPolicy::RedactionTraceOnly => {
305 if let Value::Object(map) = value {
306 if let Some(trace) = map.get_mut("trace") {
307 redact_secrets(trace);
308 }
309 }
310 }
311 RedactionPolicy::RedactionNone => {}
312 }
313}
314
315fn strip_suffix_ci(key: &str, suffix_lower: &str) -> Option<String> {
321 if let Some(s) = key.strip_suffix(suffix_lower) {
322 return Some(s.to_string());
323 }
324 let suffix_upper: String = suffix_lower
325 .chars()
326 .map(|c| c.to_ascii_uppercase())
327 .collect();
328 if let Some(s) = key.strip_suffix(&suffix_upper) {
329 return Some(s.to_string());
330 }
331 None
332}
333
334fn try_strip_generic_cents(key: &str) -> Option<(String, String)> {
336 let code = extract_currency_code(key)?;
337 let suffix_len = code.len() + "_cents".len() + 1; let stripped = &key[..key.len() - suffix_len];
339 if stripped.is_empty() {
340 return None;
341 }
342 Some((stripped.to_string(), code.to_string()))
343}
344
345fn try_process_field(key: &str, value: &Value) -> Option<(String, String)> {
348 if let Some(stripped) = strip_suffix_ci(key, "_epoch_ms") {
350 return value.as_i64().map(|ms| (stripped, format_rfc3339_ms(ms)));
351 }
352 if let Some(stripped) = strip_suffix_ci(key, "_epoch_s") {
353 return value
354 .as_i64()
355 .map(|s| (stripped, format_rfc3339_ms(s * 1000)));
356 }
357 if let Some(stripped) = strip_suffix_ci(key, "_epoch_ns") {
358 return value
359 .as_i64()
360 .map(|ns| (stripped, format_rfc3339_ms(ns.div_euclid(1_000_000))));
361 }
362
363 if let Some(stripped) = strip_suffix_ci(key, "_usd_cents") {
365 return value
366 .as_u64()
367 .map(|n| (stripped, format!("${}.{:02}", n / 100, n % 100)));
368 }
369 if let Some(stripped) = strip_suffix_ci(key, "_eur_cents") {
370 return value
371 .as_u64()
372 .map(|n| (stripped, format!("€{}.{:02}", n / 100, n % 100)));
373 }
374 if let Some((stripped, code)) = try_strip_generic_cents(key) {
375 return value.as_u64().map(|n| {
376 (
377 stripped,
378 format!("{}.{:02} {}", n / 100, n % 100, code.to_uppercase()),
379 )
380 });
381 }
382
383 if let Some(stripped) = strip_suffix_ci(key, "_rfc3339") {
385 return value.as_str().map(|s| (stripped, s.to_string()));
386 }
387 if let Some(stripped) = strip_suffix_ci(key, "_minutes") {
388 return value
389 .is_number()
390 .then(|| (stripped, format!("{} minutes", number_str(value))));
391 }
392 if let Some(stripped) = strip_suffix_ci(key, "_hours") {
393 return value
394 .is_number()
395 .then(|| (stripped, format!("{} hours", number_str(value))));
396 }
397 if let Some(stripped) = strip_suffix_ci(key, "_days") {
398 return value
399 .is_number()
400 .then(|| (stripped, format!("{} days", number_str(value))));
401 }
402
403 if let Some(stripped) = strip_suffix_ci(key, "_msats") {
405 return value
406 .is_number()
407 .then(|| (stripped, format!("{}msats", number_str(value))));
408 }
409 if let Some(stripped) = strip_suffix_ci(key, "_sats") {
410 return value
411 .is_number()
412 .then(|| (stripped, format!("{}sats", number_str(value))));
413 }
414 if let Some(stripped) = strip_suffix_ci(key, "_bytes") {
415 return value.as_i64().map(|n| (stripped, format_bytes_human(n)));
416 }
417 if let Some(stripped) = strip_suffix_ci(key, "_percent") {
418 return value
419 .is_number()
420 .then(|| (stripped, format!("{}%", number_str(value))));
421 }
422 if let Some(stripped) = strip_suffix_ci(key, "_secret") {
423 return Some((stripped, "***".to_string()));
424 }
425
426 if let Some(stripped) = strip_suffix_ci(key, "_btc") {
428 return value
429 .is_number()
430 .then(|| (stripped, format!("{} BTC", number_str(value))));
431 }
432 if let Some(stripped) = strip_suffix_ci(key, "_jpy") {
433 return value
434 .as_u64()
435 .map(|n| (stripped, format!("¥{}", format_with_commas(n))));
436 }
437 if let Some(stripped) = strip_suffix_ci(key, "_ns") {
438 return value
439 .is_number()
440 .then(|| (stripped, format!("{}ns", number_str(value))));
441 }
442 if let Some(stripped) = strip_suffix_ci(key, "_us") {
443 return value
444 .is_number()
445 .then(|| (stripped, format!("{}μs", number_str(value))));
446 }
447 if let Some(stripped) = strip_suffix_ci(key, "_ms") {
448 return format_ms_value(value).map(|v| (stripped, v));
449 }
450 if let Some(stripped) = strip_suffix_ci(key, "_s") {
451 return value
452 .is_number()
453 .then(|| (stripped, format!("{}s", number_str(value))));
454 }
455
456 None
457}
458
459fn process_object_fields<'a>(
461 map: &'a serde_json::Map<String, Value>,
462) -> Vec<(String, &'a Value, Option<String>)> {
463 let mut entries: Vec<(String, &'a str, &'a Value, Option<String>)> = Vec::new();
464 for (key, value) in map {
465 match try_process_field(key, value) {
466 Some((stripped, formatted)) => {
467 entries.push((stripped, key.as_str(), value, Some(formatted)));
468 }
469 None => {
470 entries.push((key.clone(), key.as_str(), value, None));
471 }
472 }
473 }
474
475 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
477 for (stripped, _, _, _) in &entries {
478 *counts.entry(stripped.clone()).or_insert(0) += 1;
479 }
480
481 let mut result: Vec<(String, &'a Value, Option<String>)> = entries
483 .into_iter()
484 .map(|(stripped, original, value, formatted)| {
485 if counts.get(&stripped).copied().unwrap_or(0) > 1 && original != stripped.as_str() {
486 (original.to_string(), value, None)
487 } else {
488 (stripped, value, formatted)
489 }
490 })
491 .collect();
492
493 result.sort_by(|(a, _, _), (b, _, _)| a.encode_utf16().cmp(b.encode_utf16()));
494 result
495}
496
497fn number_str(value: &Value) -> String {
502 match value {
503 Value::Number(n) => n.to_string(),
504 _ => String::new(),
505 }
506}
507
508fn format_ms_as_seconds(ms: f64) -> String {
510 let formatted = format!("{:.3}", ms / 1000.0);
511 let trimmed = formatted.trim_end_matches('0');
512 if trimmed.ends_with('.') {
513 format!("{}0s", trimmed)
514 } else {
515 format!("{}s", trimmed)
516 }
517}
518
519fn format_ms_value(value: &Value) -> Option<String> {
521 let n = value.as_f64()?;
522 if n.abs() >= 1000.0 {
523 Some(format_ms_as_seconds(n))
524 } else if let Some(i) = value.as_i64() {
525 Some(format!("{}ms", i))
526 } else {
527 Some(format!("{}ms", number_str(value)))
528 }
529}
530
531fn format_rfc3339_ms(ms: i64) -> String {
533 use chrono::{DateTime, Utc};
534 let secs = ms.div_euclid(1000);
535 let nanos = (ms.rem_euclid(1000) * 1_000_000) as u32;
536 match DateTime::from_timestamp(secs, nanos) {
537 Some(dt) => dt
538 .with_timezone(&Utc)
539 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
540 None => ms.to_string(),
541 }
542}
543
544fn format_bytes_human(bytes: i64) -> String {
546 const KB: f64 = 1024.0;
547 const MB: f64 = KB * 1024.0;
548 const GB: f64 = MB * 1024.0;
549 const TB: f64 = GB * 1024.0;
550
551 let sign = if bytes < 0 { "-" } else { "" };
552 let b = (bytes as f64).abs();
553 if b >= TB {
554 format!("{sign}{:.1}TB", b / TB)
555 } else if b >= GB {
556 format!("{sign}{:.1}GB", b / GB)
557 } else if b >= MB {
558 format!("{sign}{:.1}MB", b / MB)
559 } else if b >= KB {
560 format!("{sign}{:.1}KB", b / KB)
561 } else {
562 format!("{bytes}B")
563 }
564}
565
566fn format_with_commas(n: u64) -> String {
568 let s = n.to_string();
569 let mut result = String::with_capacity(s.len() + s.len() / 3);
570 for (i, c) in s.chars().enumerate() {
571 if i > 0 && (s.len() - i).is_multiple_of(3) {
572 result.push(',');
573 }
574 result.push(c);
575 }
576 result
577}
578
579fn extract_currency_code(key: &str) -> Option<&str> {
581 let without_cents = key
582 .strip_suffix("_cents")
583 .or_else(|| key.strip_suffix("_CENTS"))?;
584 let last_underscore = without_cents.rfind('_')?;
585 let code = &without_cents[last_underscore + 1..];
586 if code.is_empty() {
587 return None;
588 }
589 Some(code)
590}
591
592fn render_yaml_processed(value: &Value, indent: usize, lines: &mut Vec<String>) {
597 let prefix = " ".repeat(indent);
598 match value {
599 Value::Object(map) => {
600 let processed = process_object_fields(map);
601 for (display_key, v, formatted) in processed {
602 if let Some(fv) = formatted {
603 lines.push(format!(
604 "{}{}: \"{}\"",
605 prefix,
606 display_key,
607 escape_yaml_str(&fv)
608 ));
609 } else {
610 match v {
611 Value::Object(inner) if !inner.is_empty() => {
612 lines.push(format!("{}{}:", prefix, display_key));
613 render_yaml_processed(v, indent + 1, lines);
614 }
615 Value::Object(_) => {
616 lines.push(format!("{}{}: {{}}", prefix, display_key));
617 }
618 Value::Array(arr) => {
619 if arr.is_empty() {
620 lines.push(format!("{}{}: []", prefix, display_key));
621 } else {
622 lines.push(format!("{}{}:", prefix, display_key));
623 for item in arr {
624 if item.is_object() {
625 lines.push(format!("{} -", prefix));
626 render_yaml_processed(item, indent + 2, lines);
627 } else {
628 lines.push(format!("{} - {}", prefix, yaml_scalar(item)));
629 }
630 }
631 }
632 }
633 _ => {
634 lines.push(format!("{}{}: {}", prefix, display_key, yaml_scalar(v)));
635 }
636 }
637 }
638 }
639 }
640 _ => {
641 lines.push(format!("{}{}", prefix, yaml_scalar(value)));
642 }
643 }
644}
645
646fn escape_yaml_str(s: &str) -> String {
647 s.replace('\\', "\\\\")
648 .replace('"', "\\\"")
649 .replace('\n', "\\n")
650 .replace('\r', "\\r")
651 .replace('\t', "\\t")
652}
653
654fn yaml_scalar(value: &Value) -> String {
655 match value {
656 Value::String(s) => {
657 format!("\"{}\"", escape_yaml_str(s))
658 }
659 Value::Null => "null".to_string(),
660 Value::Bool(b) => b.to_string(),
661 Value::Number(n) => n.to_string(),
662 other => format!("\"{}\"", other.to_string().replace('"', "\\\"")),
663 }
664}
665
666fn collect_plain_pairs(value: &Value, prefix: &str, pairs: &mut Vec<(String, String)>) {
671 if let Value::Object(map) = value {
672 let processed = process_object_fields(map);
673 for (display_key, v, formatted) in processed {
674 let full_key = if prefix.is_empty() {
675 display_key
676 } else {
677 format!("{}.{}", prefix, display_key)
678 };
679 if let Some(fv) = formatted {
680 pairs.push((full_key, fv));
681 } else {
682 match v {
683 Value::Object(_) => collect_plain_pairs(v, &full_key, pairs),
684 Value::Array(arr) => {
685 let joined = arr.iter().map(plain_scalar).collect::<Vec<_>>().join(",");
686 pairs.push((full_key, joined));
687 }
688 Value::Null => pairs.push((full_key, String::new())),
689 _ => pairs.push((full_key, plain_scalar(v))),
690 }
691 }
692 }
693 }
694}
695
696fn plain_scalar(value: &Value) -> String {
697 match value {
698 Value::String(s) => s.clone(),
699 Value::Null => "null".to_string(),
700 Value::Bool(b) => b.to_string(),
701 Value::Number(n) => n.to_string(),
702 other => other.to_string(),
703 }
704}
705
706#[cfg(test)]
707mod tests;