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("trace".to_string(), serde_json::json!({"duration_ms": 0}));
262 Value::Object(obj)
263}
264
265fn redact_secrets(value: &mut Value) {
270 match value {
271 Value::Object(map) => {
272 let keys: Vec<String> = map.keys().cloned().collect();
273 for key in keys {
274 if key.ends_with("_secret") || key.ends_with("_SECRET") {
275 match map.get(&key) {
276 Some(Value::Object(_)) | Some(Value::Array(_)) => {
277 }
279 _ => {
280 map.insert(key.clone(), Value::String("***".into()));
281 continue;
282 }
283 }
284 }
285 if let Some(v) = map.get_mut(&key) {
286 redact_secrets(v);
287 }
288 }
289 }
290 Value::Array(arr) => {
291 for v in arr {
292 redact_secrets(v);
293 }
294 }
295 _ => {}
296 }
297}
298
299fn apply_redaction_policy(value: &mut Value, redaction_policy: RedactionPolicy) {
300 match redaction_policy {
301 RedactionPolicy::RedactionTraceOnly => {
302 if let Value::Object(map) = value {
303 if let Some(trace) = map.get_mut("trace") {
304 redact_secrets(trace);
305 }
306 }
307 }
308 RedactionPolicy::RedactionNone => {}
309 }
310}
311
312fn strip_suffix_ci(key: &str, suffix_lower: &str) -> Option<String> {
318 if let Some(s) = key.strip_suffix(suffix_lower) {
319 return Some(s.to_string());
320 }
321 let suffix_upper: String = suffix_lower
322 .chars()
323 .map(|c| c.to_ascii_uppercase())
324 .collect();
325 if let Some(s) = key.strip_suffix(&suffix_upper) {
326 return Some(s.to_string());
327 }
328 None
329}
330
331fn try_strip_generic_cents(key: &str) -> Option<(String, String)> {
333 let code = extract_currency_code(key)?;
334 let suffix_len = code.len() + "_cents".len() + 1; let stripped = &key[..key.len() - suffix_len];
336 if stripped.is_empty() {
337 return None;
338 }
339 Some((stripped.to_string(), code.to_string()))
340}
341
342fn try_process_field(key: &str, value: &Value) -> Option<(String, String)> {
345 if let Some(stripped) = strip_suffix_ci(key, "_epoch_ms") {
347 return value.as_i64().map(|ms| (stripped, format_rfc3339_ms(ms)));
348 }
349 if let Some(stripped) = strip_suffix_ci(key, "_epoch_s") {
350 return value
351 .as_i64()
352 .map(|s| (stripped, format_rfc3339_ms(s * 1000)));
353 }
354 if let Some(stripped) = strip_suffix_ci(key, "_epoch_ns") {
355 return value
356 .as_i64()
357 .map(|ns| (stripped, format_rfc3339_ms(ns.div_euclid(1_000_000))));
358 }
359
360 if let Some(stripped) = strip_suffix_ci(key, "_usd_cents") {
362 return value
363 .as_u64()
364 .map(|n| (stripped, format!("${}.{:02}", n / 100, n % 100)));
365 }
366 if let Some(stripped) = strip_suffix_ci(key, "_eur_cents") {
367 return value
368 .as_u64()
369 .map(|n| (stripped, format!("€{}.{:02}", n / 100, n % 100)));
370 }
371 if let Some((stripped, code)) = try_strip_generic_cents(key) {
372 return value.as_u64().map(|n| {
373 (
374 stripped,
375 format!("{}.{:02} {}", n / 100, n % 100, code.to_uppercase()),
376 )
377 });
378 }
379
380 if let Some(stripped) = strip_suffix_ci(key, "_rfc3339") {
382 return value.as_str().map(|s| (stripped, s.to_string()));
383 }
384 if let Some(stripped) = strip_suffix_ci(key, "_minutes") {
385 return value
386 .is_number()
387 .then(|| (stripped, format!("{} minutes", number_str(value))));
388 }
389 if let Some(stripped) = strip_suffix_ci(key, "_hours") {
390 return value
391 .is_number()
392 .then(|| (stripped, format!("{} hours", number_str(value))));
393 }
394 if let Some(stripped) = strip_suffix_ci(key, "_days") {
395 return value
396 .is_number()
397 .then(|| (stripped, format!("{} days", number_str(value))));
398 }
399
400 if let Some(stripped) = strip_suffix_ci(key, "_msats") {
402 return value
403 .is_number()
404 .then(|| (stripped, format!("{}msats", number_str(value))));
405 }
406 if let Some(stripped) = strip_suffix_ci(key, "_sats") {
407 return value
408 .is_number()
409 .then(|| (stripped, format!("{}sats", number_str(value))));
410 }
411 if let Some(stripped) = strip_suffix_ci(key, "_bytes") {
412 return value.as_i64().map(|n| (stripped, format_bytes_human(n)));
413 }
414 if let Some(stripped) = strip_suffix_ci(key, "_percent") {
415 return value
416 .is_number()
417 .then(|| (stripped, format!("{}%", number_str(value))));
418 }
419 if let Some(stripped) = strip_suffix_ci(key, "_secret") {
420 return Some((stripped, "***".to_string()));
421 }
422
423 if let Some(stripped) = strip_suffix_ci(key, "_btc") {
425 return value
426 .is_number()
427 .then(|| (stripped, format!("{} BTC", number_str(value))));
428 }
429 if let Some(stripped) = strip_suffix_ci(key, "_jpy") {
430 return value
431 .as_u64()
432 .map(|n| (stripped, format!("¥{}", format_with_commas(n))));
433 }
434 if let Some(stripped) = strip_suffix_ci(key, "_ns") {
435 return value
436 .is_number()
437 .then(|| (stripped, format!("{}ns", number_str(value))));
438 }
439 if let Some(stripped) = strip_suffix_ci(key, "_us") {
440 return value
441 .is_number()
442 .then(|| (stripped, format!("{}μs", number_str(value))));
443 }
444 if let Some(stripped) = strip_suffix_ci(key, "_ms") {
445 return format_ms_value(value).map(|v| (stripped, v));
446 }
447 if let Some(stripped) = strip_suffix_ci(key, "_s") {
448 return value
449 .is_number()
450 .then(|| (stripped, format!("{}s", number_str(value))));
451 }
452
453 None
454}
455
456fn process_object_fields<'a>(
458 map: &'a serde_json::Map<String, Value>,
459) -> Vec<(String, &'a Value, Option<String>)> {
460 let mut entries: Vec<(String, &'a str, &'a Value, Option<String>)> = Vec::new();
461 for (key, value) in map {
462 match try_process_field(key, value) {
463 Some((stripped, formatted)) => {
464 entries.push((stripped, key.as_str(), value, Some(formatted)));
465 }
466 None => {
467 entries.push((key.clone(), key.as_str(), value, None));
468 }
469 }
470 }
471
472 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
474 for (stripped, _, _, _) in &entries {
475 *counts.entry(stripped.clone()).or_insert(0) += 1;
476 }
477
478 let mut result: Vec<(String, &'a Value, Option<String>)> = entries
480 .into_iter()
481 .map(|(stripped, original, value, formatted)| {
482 if counts.get(&stripped).copied().unwrap_or(0) > 1 && original != stripped.as_str() {
483 (original.to_string(), value, None)
484 } else {
485 (stripped, value, formatted)
486 }
487 })
488 .collect();
489
490 result.sort_by(|(a, _, _), (b, _, _)| a.encode_utf16().cmp(b.encode_utf16()));
491 result
492}
493
494fn number_str(value: &Value) -> String {
499 match value {
500 Value::Number(n) => n.to_string(),
501 _ => String::new(),
502 }
503}
504
505fn format_ms_as_seconds(ms: f64) -> String {
507 let formatted = format!("{:.3}", ms / 1000.0);
508 let trimmed = formatted.trim_end_matches('0');
509 if trimmed.ends_with('.') {
510 format!("{}0s", trimmed)
511 } else {
512 format!("{}s", trimmed)
513 }
514}
515
516fn format_ms_value(value: &Value) -> Option<String> {
518 let n = value.as_f64()?;
519 if n.abs() >= 1000.0 {
520 Some(format_ms_as_seconds(n))
521 } else if let Some(i) = value.as_i64() {
522 Some(format!("{}ms", i))
523 } else {
524 Some(format!("{}ms", number_str(value)))
525 }
526}
527
528fn format_rfc3339_ms(ms: i64) -> String {
530 use chrono::{DateTime, Utc};
531 let secs = ms.div_euclid(1000);
532 let nanos = (ms.rem_euclid(1000) * 1_000_000) as u32;
533 match DateTime::from_timestamp(secs, nanos) {
534 Some(dt) => dt
535 .with_timezone(&Utc)
536 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
537 None => ms.to_string(),
538 }
539}
540
541fn format_bytes_human(bytes: i64) -> String {
543 const KB: f64 = 1024.0;
544 const MB: f64 = KB * 1024.0;
545 const GB: f64 = MB * 1024.0;
546 const TB: f64 = GB * 1024.0;
547
548 let sign = if bytes < 0 { "-" } else { "" };
549 let b = (bytes as f64).abs();
550 if b >= TB {
551 format!("{sign}{:.1}TB", b / TB)
552 } else if b >= GB {
553 format!("{sign}{:.1}GB", b / GB)
554 } else if b >= MB {
555 format!("{sign}{:.1}MB", b / MB)
556 } else if b >= KB {
557 format!("{sign}{:.1}KB", b / KB)
558 } else {
559 format!("{bytes}B")
560 }
561}
562
563fn format_with_commas(n: u64) -> String {
565 let s = n.to_string();
566 let mut result = String::with_capacity(s.len() + s.len() / 3);
567 for (i, c) in s.chars().enumerate() {
568 if i > 0 && (s.len() - i).is_multiple_of(3) {
569 result.push(',');
570 }
571 result.push(c);
572 }
573 result
574}
575
576fn extract_currency_code(key: &str) -> Option<&str> {
578 let without_cents = key
579 .strip_suffix("_cents")
580 .or_else(|| key.strip_suffix("_CENTS"))?;
581 let last_underscore = without_cents.rfind('_')?;
582 let code = &without_cents[last_underscore + 1..];
583 if code.is_empty() {
584 return None;
585 }
586 Some(code)
587}
588
589fn render_yaml_processed(value: &Value, indent: usize, lines: &mut Vec<String>) {
594 let prefix = " ".repeat(indent);
595 match value {
596 Value::Object(map) => {
597 let processed = process_object_fields(map);
598 for (display_key, v, formatted) in processed {
599 if let Some(fv) = formatted {
600 lines.push(format!(
601 "{}{}: \"{}\"",
602 prefix,
603 display_key,
604 escape_yaml_str(&fv)
605 ));
606 } else {
607 match v {
608 Value::Object(inner) if !inner.is_empty() => {
609 lines.push(format!("{}{}:", prefix, display_key));
610 render_yaml_processed(v, indent + 1, lines);
611 }
612 Value::Object(_) => {
613 lines.push(format!("{}{}: {{}}", prefix, display_key));
614 }
615 Value::Array(arr) => {
616 if arr.is_empty() {
617 lines.push(format!("{}{}: []", prefix, display_key));
618 } else {
619 lines.push(format!("{}{}:", prefix, display_key));
620 for item in arr {
621 if item.is_object() {
622 lines.push(format!("{} -", prefix));
623 render_yaml_processed(item, indent + 2, lines);
624 } else {
625 lines.push(format!("{} - {}", prefix, yaml_scalar(item)));
626 }
627 }
628 }
629 }
630 _ => {
631 lines.push(format!("{}{}: {}", prefix, display_key, yaml_scalar(v)));
632 }
633 }
634 }
635 }
636 }
637 _ => {
638 lines.push(format!("{}{}", prefix, yaml_scalar(value)));
639 }
640 }
641}
642
643fn escape_yaml_str(s: &str) -> String {
644 s.replace('\\', "\\\\")
645 .replace('"', "\\\"")
646 .replace('\n', "\\n")
647 .replace('\r', "\\r")
648 .replace('\t', "\\t")
649}
650
651fn yaml_scalar(value: &Value) -> String {
652 match value {
653 Value::String(s) => {
654 format!("\"{}\"", escape_yaml_str(s))
655 }
656 Value::Null => "null".to_string(),
657 Value::Bool(b) => b.to_string(),
658 Value::Number(n) => n.to_string(),
659 other => format!("\"{}\"", other.to_string().replace('"', "\\\"")),
660 }
661}
662
663fn collect_plain_pairs(value: &Value, prefix: &str, pairs: &mut Vec<(String, String)>) {
668 if let Value::Object(map) = value {
669 let processed = process_object_fields(map);
670 for (display_key, v, formatted) in processed {
671 let full_key = if prefix.is_empty() {
672 display_key
673 } else {
674 format!("{}.{}", prefix, display_key)
675 };
676 if let Some(fv) = formatted {
677 pairs.push((full_key, fv));
678 } else {
679 match v {
680 Value::Object(_) => collect_plain_pairs(v, &full_key, pairs),
681 Value::Array(arr) => {
682 let joined = arr.iter().map(plain_scalar).collect::<Vec<_>>().join(",");
683 pairs.push((full_key, joined));
684 }
685 Value::Null => pairs.push((full_key, String::new())),
686 _ => pairs.push((full_key, plain_scalar(v))),
687 }
688 }
689 }
690 }
691}
692
693fn plain_scalar(value: &Value) -> String {
694 match value {
695 Value::String(s) => s.clone(),
696 Value::Null => "null".to_string(),
697 Value::Bool(b) => b.to_string(),
698 Value::Number(n) => n.to_string(),
699 other => other.to_string(),
700 }
701}
702
703#[cfg(test)]
704mod tests;