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 let f: f64 = num_str.parse().ok()?;
154 if f < 0.0 || f.is_nan() || f.is_infinite() {
155 return None;
156 }
157 let result = f * mult as f64;
158 if result > u64::MAX as f64 {
159 return None;
160 }
161 Some(result as u64)
162}
163
164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170pub enum OutputFormat {
171 Json,
172 Yaml,
173 Plain,
174}
175
176pub fn cli_parse_output(s: &str) -> Result<OutputFormat, String> {
186 match s {
187 "json" => Ok(OutputFormat::Json),
188 "yaml" => Ok(OutputFormat::Yaml),
189 "plain" => Ok(OutputFormat::Plain),
190 _ => Err(format!(
191 "invalid --output format '{s}': expected json, yaml, or plain"
192 )),
193 }
194}
195
196pub fn cli_parse_log_filters<S: AsRef<str>>(entries: &[S]) -> Vec<String> {
206 let mut out: Vec<String> = Vec::new();
207 for entry in entries {
208 let s = entry.as_ref().trim().to_ascii_lowercase();
209 if !s.is_empty() && !out.contains(&s) {
210 out.push(s);
211 }
212 }
213 out
214}
215
216pub fn cli_output(value: &Value, format: OutputFormat) -> String {
227 match format {
228 OutputFormat::Json => output_json(value),
229 OutputFormat::Yaml => output_yaml(value),
230 OutputFormat::Plain => output_plain(value),
231 }
232}
233
234pub fn build_cli_error(message: &str, hint: Option<&str>) -> Value {
246 let mut obj = serde_json::Map::new();
247 obj.insert("code".to_string(), Value::String("error".to_string()));
248 obj.insert(
249 "error_code".to_string(),
250 Value::String("invalid_request".to_string()),
251 );
252 obj.insert("error".to_string(), Value::String(message.to_string()));
253 if let Some(h) = hint {
254 obj.insert("hint".to_string(), Value::String(h.to_string()));
255 }
256 obj.insert("retryable".to_string(), Value::Bool(false));
257 obj.insert(
258 "trace".to_string(),
259 serde_json::json!({"duration_ms": 0}),
260 );
261 Value::Object(obj)
262}
263
264fn redact_secrets(value: &mut Value) {
269 match value {
270 Value::Object(map) => {
271 let keys: Vec<String> = map.keys().cloned().collect();
272 for key in keys {
273 if key.ends_with("_secret") || key.ends_with("_SECRET") {
274 match map.get(&key) {
275 Some(Value::Object(_)) | Some(Value::Array(_)) => {
276 }
278 _ => {
279 map.insert(key.clone(), Value::String("***".into()));
280 continue;
281 }
282 }
283 }
284 if let Some(v) = map.get_mut(&key) {
285 redact_secrets(v);
286 }
287 }
288 }
289 Value::Array(arr) => {
290 for v in arr {
291 redact_secrets(v);
292 }
293 }
294 _ => {}
295 }
296}
297
298fn apply_redaction_policy(value: &mut Value, redaction_policy: RedactionPolicy) {
299 match redaction_policy {
300 RedactionPolicy::RedactionTraceOnly => {
301 if let Value::Object(map) = value {
302 if let Some(trace) = map.get_mut("trace") {
303 redact_secrets(trace);
304 }
305 }
306 }
307 RedactionPolicy::RedactionNone => {}
308 }
309}
310
311fn strip_suffix_ci(key: &str, suffix_lower: &str) -> Option<String> {
317 if let Some(s) = key.strip_suffix(suffix_lower) {
318 return Some(s.to_string());
319 }
320 let suffix_upper: String = suffix_lower
321 .chars()
322 .map(|c| c.to_ascii_uppercase())
323 .collect();
324 if let Some(s) = key.strip_suffix(&suffix_upper) {
325 return Some(s.to_string());
326 }
327 None
328}
329
330fn try_strip_generic_cents(key: &str) -> Option<(String, String)> {
332 let code = extract_currency_code(key)?;
333 let suffix_len = code.len() + "_cents".len() + 1; let stripped = &key[..key.len() - suffix_len];
335 if stripped.is_empty() {
336 return None;
337 }
338 Some((stripped.to_string(), code.to_string()))
339}
340
341fn try_process_field(key: &str, value: &Value) -> Option<(String, String)> {
344 if let Some(stripped) = strip_suffix_ci(key, "_epoch_ms") {
346 return value.as_i64().map(|ms| (stripped, format_rfc3339_ms(ms)));
347 }
348 if let Some(stripped) = strip_suffix_ci(key, "_epoch_s") {
349 return value
350 .as_i64()
351 .map(|s| (stripped, format_rfc3339_ms(s * 1000)));
352 }
353 if let Some(stripped) = strip_suffix_ci(key, "_epoch_ns") {
354 return value
355 .as_i64()
356 .map(|ns| (stripped, format_rfc3339_ms(ns.div_euclid(1_000_000))));
357 }
358
359 if let Some(stripped) = strip_suffix_ci(key, "_usd_cents") {
361 return value
362 .as_u64()
363 .map(|n| (stripped, format!("${}.{:02}", n / 100, n % 100)));
364 }
365 if let Some(stripped) = strip_suffix_ci(key, "_eur_cents") {
366 return value
367 .as_u64()
368 .map(|n| (stripped, format!("€{}.{:02}", n / 100, n % 100)));
369 }
370 if let Some((stripped, code)) = try_strip_generic_cents(key) {
371 return value.as_u64().map(|n| {
372 (
373 stripped,
374 format!("{}.{:02} {}", n / 100, n % 100, code.to_uppercase()),
375 )
376 });
377 }
378
379 if let Some(stripped) = strip_suffix_ci(key, "_rfc3339") {
381 return value.as_str().map(|s| (stripped, s.to_string()));
382 }
383 if let Some(stripped) = strip_suffix_ci(key, "_minutes") {
384 return value
385 .is_number()
386 .then(|| (stripped, format!("{} minutes", number_str(value))));
387 }
388 if let Some(stripped) = strip_suffix_ci(key, "_hours") {
389 return value
390 .is_number()
391 .then(|| (stripped, format!("{} hours", number_str(value))));
392 }
393 if let Some(stripped) = strip_suffix_ci(key, "_days") {
394 return value
395 .is_number()
396 .then(|| (stripped, format!("{} days", number_str(value))));
397 }
398
399 if let Some(stripped) = strip_suffix_ci(key, "_msats") {
401 return value
402 .is_number()
403 .then(|| (stripped, format!("{}msats", number_str(value))));
404 }
405 if let Some(stripped) = strip_suffix_ci(key, "_sats") {
406 return value
407 .is_number()
408 .then(|| (stripped, format!("{}sats", number_str(value))));
409 }
410 if let Some(stripped) = strip_suffix_ci(key, "_bytes") {
411 return value.as_i64().map(|n| (stripped, format_bytes_human(n)));
412 }
413 if let Some(stripped) = strip_suffix_ci(key, "_percent") {
414 return value
415 .is_number()
416 .then(|| (stripped, format!("{}%", number_str(value))));
417 }
418 if let Some(stripped) = strip_suffix_ci(key, "_secret") {
419 return Some((stripped, "***".to_string()));
420 }
421
422 if let Some(stripped) = strip_suffix_ci(key, "_btc") {
424 return value
425 .is_number()
426 .then(|| (stripped, format!("{} BTC", number_str(value))));
427 }
428 if let Some(stripped) = strip_suffix_ci(key, "_jpy") {
429 return value
430 .as_u64()
431 .map(|n| (stripped, format!("¥{}", format_with_commas(n))));
432 }
433 if let Some(stripped) = strip_suffix_ci(key, "_ns") {
434 return value
435 .is_number()
436 .then(|| (stripped, format!("{}ns", number_str(value))));
437 }
438 if let Some(stripped) = strip_suffix_ci(key, "_us") {
439 return value
440 .is_number()
441 .then(|| (stripped, format!("{}μs", number_str(value))));
442 }
443 if let Some(stripped) = strip_suffix_ci(key, "_ms") {
444 return format_ms_value(value).map(|v| (stripped, v));
445 }
446 if let Some(stripped) = strip_suffix_ci(key, "_s") {
447 return value
448 .is_number()
449 .then(|| (stripped, format!("{}s", number_str(value))));
450 }
451
452 None
453}
454
455fn process_object_fields<'a>(
457 map: &'a serde_json::Map<String, Value>,
458) -> Vec<(String, &'a Value, Option<String>)> {
459 let mut entries: Vec<(String, &'a str, &'a Value, Option<String>)> = Vec::new();
460 for (key, value) in map {
461 match try_process_field(key, value) {
462 Some((stripped, formatted)) => {
463 entries.push((stripped, key.as_str(), value, Some(formatted)));
464 }
465 None => {
466 entries.push((key.clone(), key.as_str(), value, None));
467 }
468 }
469 }
470
471 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
473 for (stripped, _, _, _) in &entries {
474 *counts.entry(stripped.clone()).or_insert(0) += 1;
475 }
476
477 let mut result: Vec<(String, &'a Value, Option<String>)> = entries
479 .into_iter()
480 .map(|(stripped, original, value, formatted)| {
481 if counts.get(&stripped).copied().unwrap_or(0) > 1 && original != stripped.as_str() {
482 (original.to_string(), value, None)
483 } else {
484 (stripped, value, formatted)
485 }
486 })
487 .collect();
488
489 result.sort_by(|(a, _, _), (b, _, _)| a.encode_utf16().cmp(b.encode_utf16()));
490 result
491}
492
493fn number_str(value: &Value) -> String {
498 match value {
499 Value::Number(n) => n.to_string(),
500 _ => String::new(),
501 }
502}
503
504fn format_ms_as_seconds(ms: f64) -> String {
506 let formatted = format!("{:.3}", ms / 1000.0);
507 let trimmed = formatted.trim_end_matches('0');
508 if trimmed.ends_with('.') {
509 format!("{}0s", trimmed)
510 } else {
511 format!("{}s", trimmed)
512 }
513}
514
515fn format_ms_value(value: &Value) -> Option<String> {
517 let n = value.as_f64()?;
518 if n.abs() >= 1000.0 {
519 Some(format_ms_as_seconds(n))
520 } else if let Some(i) = value.as_i64() {
521 Some(format!("{}ms", i))
522 } else {
523 Some(format!("{}ms", number_str(value)))
524 }
525}
526
527fn format_rfc3339_ms(ms: i64) -> String {
529 use chrono::{DateTime, Utc};
530 let secs = ms.div_euclid(1000);
531 let nanos = (ms.rem_euclid(1000) * 1_000_000) as u32;
532 match DateTime::from_timestamp(secs, nanos) {
533 Some(dt) => dt
534 .with_timezone(&Utc)
535 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
536 None => ms.to_string(),
537 }
538}
539
540fn format_bytes_human(bytes: i64) -> String {
542 const KB: f64 = 1024.0;
543 const MB: f64 = KB * 1024.0;
544 const GB: f64 = MB * 1024.0;
545 const TB: f64 = GB * 1024.0;
546
547 let sign = if bytes < 0 { "-" } else { "" };
548 let b = (bytes as f64).abs();
549 if b >= TB {
550 format!("{sign}{:.1}TB", b / TB)
551 } else if b >= GB {
552 format!("{sign}{:.1}GB", b / GB)
553 } else if b >= MB {
554 format!("{sign}{:.1}MB", b / MB)
555 } else if b >= KB {
556 format!("{sign}{:.1}KB", b / KB)
557 } else {
558 format!("{bytes}B")
559 }
560}
561
562fn format_with_commas(n: u64) -> String {
564 let s = n.to_string();
565 let mut result = String::with_capacity(s.len() + s.len() / 3);
566 for (i, c) in s.chars().enumerate() {
567 if i > 0 && (s.len() - i).is_multiple_of(3) {
568 result.push(',');
569 }
570 result.push(c);
571 }
572 result
573}
574
575fn extract_currency_code(key: &str) -> Option<&str> {
577 let without_cents = key
578 .strip_suffix("_cents")
579 .or_else(|| key.strip_suffix("_CENTS"))?;
580 let last_underscore = without_cents.rfind('_')?;
581 let code = &without_cents[last_underscore + 1..];
582 if code.is_empty() {
583 return None;
584 }
585 Some(code)
586}
587
588fn render_yaml_processed(value: &Value, indent: usize, lines: &mut Vec<String>) {
593 let prefix = " ".repeat(indent);
594 match value {
595 Value::Object(map) => {
596 let processed = process_object_fields(map);
597 for (display_key, v, formatted) in processed {
598 if let Some(fv) = formatted {
599 lines.push(format!(
600 "{}{}: \"{}\"",
601 prefix,
602 display_key,
603 escape_yaml_str(&fv)
604 ));
605 } else {
606 match v {
607 Value::Object(inner) if !inner.is_empty() => {
608 lines.push(format!("{}{}:", prefix, display_key));
609 render_yaml_processed(v, indent + 1, lines);
610 }
611 Value::Object(_) => {
612 lines.push(format!("{}{}: {{}}", prefix, display_key));
613 }
614 Value::Array(arr) => {
615 if arr.is_empty() {
616 lines.push(format!("{}{}: []", prefix, display_key));
617 } else {
618 lines.push(format!("{}{}:", prefix, display_key));
619 for item in arr {
620 if item.is_object() {
621 lines.push(format!("{} -", prefix));
622 render_yaml_processed(item, indent + 2, lines);
623 } else {
624 lines.push(format!("{} - {}", prefix, yaml_scalar(item)));
625 }
626 }
627 }
628 }
629 _ => {
630 lines.push(format!("{}{}: {}", prefix, display_key, yaml_scalar(v)));
631 }
632 }
633 }
634 }
635 }
636 _ => {
637 lines.push(format!("{}{}", prefix, yaml_scalar(value)));
638 }
639 }
640}
641
642fn escape_yaml_str(s: &str) -> String {
643 s.replace('\\', "\\\\")
644 .replace('"', "\\\"")
645 .replace('\n', "\\n")
646 .replace('\r', "\\r")
647 .replace('\t', "\\t")
648}
649
650fn yaml_scalar(value: &Value) -> String {
651 match value {
652 Value::String(s) => {
653 format!("\"{}\"", escape_yaml_str(s))
654 }
655 Value::Null => "null".to_string(),
656 Value::Bool(b) => b.to_string(),
657 Value::Number(n) => n.to_string(),
658 other => format!("\"{}\"", other.to_string().replace('"', "\\\"")),
659 }
660}
661
662fn collect_plain_pairs(value: &Value, prefix: &str, pairs: &mut Vec<(String, String)>) {
667 if let Value::Object(map) = value {
668 let processed = process_object_fields(map);
669 for (display_key, v, formatted) in processed {
670 let full_key = if prefix.is_empty() {
671 display_key
672 } else {
673 format!("{}.{}", prefix, display_key)
674 };
675 if let Some(fv) = formatted {
676 pairs.push((full_key, fv));
677 } else {
678 match v {
679 Value::Object(_) => collect_plain_pairs(v, &full_key, pairs),
680 Value::Array(arr) => {
681 let joined = arr.iter().map(plain_scalar).collect::<Vec<_>>().join(",");
682 pairs.push((full_key, joined));
683 }
684 Value::Null => pairs.push((full_key, String::new())),
685 _ => pairs.push((full_key, plain_scalar(v))),
686 }
687 }
688 }
689 }
690}
691
692fn plain_scalar(value: &Value) -> String {
693 match value {
694 Value::String(s) => s.clone(),
695 Value::Null => "null".to_string(),
696 Value::Bool(b) => b.to_string(),
697 Value::Number(n) => n.to_string(),
698 other => other.to_string(),
699 }
700}
701
702#[cfg(test)]
703mod tests;