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, trace: Option<Value>) -> Value {
30 match trace {
31 Some(t) => serde_json::json!({"code": "error", "error": message, "trace": t}),
32 None => serde_json::json!({"code": "error", "error": message}),
33 }
34}
35
36pub fn build_json(code: &str, fields: Value, trace: Option<Value>) -> Value {
38 let mut obj = match fields {
39 Value::Object(map) => map,
40 _ => serde_json::Map::new(),
41 };
42 obj.insert("code".to_string(), Value::String(code.to_string()));
43 if let Some(t) = trace {
44 obj.insert("trace".to_string(), t);
45 }
46 Value::Object(obj)
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum RedactionPolicy {
56 RedactionTraceOnly,
58 RedactionNone,
60}
61
62pub fn output_json(value: &Value) -> String {
64 let mut v = value.clone();
65 redact_secrets(&mut v);
66 serialize_json_output(&v)
67}
68
69pub fn output_json_with(value: &Value, redaction_policy: RedactionPolicy) -> String {
71 let mut v = value.clone();
72 apply_redaction_policy(&mut v, redaction_policy);
73 serialize_json_output(&v)
74}
75
76fn serialize_json_output(value: &Value) -> String {
77 match serde_json::to_string(value) {
78 Ok(s) => s,
79 Err(err) => serde_json::json!({
80 "error": "output_json_failed",
81 "detail": err.to_string(),
82 })
83 .to_string(),
84 }
85}
86
87pub fn output_yaml(value: &Value) -> String {
89 let mut lines = vec!["---".to_string()];
90 render_yaml_processed(value, 0, &mut lines);
91 lines.join("\n")
92}
93
94pub fn output_plain(value: &Value) -> String {
96 let mut pairs: Vec<(String, String)> = Vec::new();
97 collect_plain_pairs(value, "", &mut pairs);
98 pairs.sort_by(|(a, _), (b, _)| a.encode_utf16().cmp(b.encode_utf16()));
99 pairs
100 .into_iter()
101 .map(|(k, v)| {
102 if v.contains(' ') {
103 format!("{}=\"{}\"", k, v)
104 } else {
105 format!("{}={}", k, v)
106 }
107 })
108 .collect::<Vec<_>>()
109 .join(" ")
110}
111
112pub fn internal_redact_secrets(value: &mut Value) {
118 redact_secrets(value);
119}
120
121pub fn parse_size(s: &str) -> Option<u64> {
127 let s = s.trim();
128 if s.is_empty() {
129 return None;
130 }
131 let last = *s.as_bytes().last()?;
132 let (num_str, mult) = match last {
133 b'B' | b'b' => (&s[..s.len() - 1], 1u64),
134 b'K' | b'k' => (&s[..s.len() - 1], 1024),
135 b'M' | b'm' => (&s[..s.len() - 1], 1024 * 1024),
136 b'G' | b'g' => (&s[..s.len() - 1], 1024 * 1024 * 1024),
137 b'T' | b't' => (&s[..s.len() - 1], 1024u64 * 1024 * 1024 * 1024),
138 b'0'..=b'9' | b'.' => (s, 1),
139 _ => return None,
140 };
141 if num_str.is_empty() {
142 return None;
143 }
144 if let Ok(n) = num_str.parse::<u64>() {
145 return n.checked_mul(mult);
146 }
147 let f: f64 = num_str.parse().ok()?;
148 if f < 0.0 || f.is_nan() || f.is_infinite() {
149 return None;
150 }
151 let result = f * mult as f64;
152 if result > u64::MAX as f64 {
153 return None;
154 }
155 Some(result as u64)
156}
157
158#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164pub enum OutputFormat {
165 Json,
166 Yaml,
167 Plain,
168}
169
170pub fn cli_parse_output(s: &str) -> Result<OutputFormat, String> {
180 match s {
181 "json" => Ok(OutputFormat::Json),
182 "yaml" => Ok(OutputFormat::Yaml),
183 "plain" => Ok(OutputFormat::Plain),
184 _ => Err(format!(
185 "invalid --output format '{s}': expected json, yaml, or plain"
186 )),
187 }
188}
189
190pub fn cli_parse_log_filters<S: AsRef<str>>(entries: &[S]) -> Vec<String> {
200 let mut out: Vec<String> = Vec::new();
201 for entry in entries {
202 let s = entry.as_ref().trim().to_ascii_lowercase();
203 if !s.is_empty() && !out.contains(&s) {
204 out.push(s);
205 }
206 }
207 out
208}
209
210pub fn cli_output(value: &Value, format: OutputFormat) -> String {
221 match format {
222 OutputFormat::Json => output_json(value),
223 OutputFormat::Yaml => output_yaml(value),
224 OutputFormat::Plain => output_plain(value),
225 }
226}
227
228pub fn build_cli_error(message: &str) -> Value {
240 serde_json::json!({
241 "code": "error",
242 "error_code": "invalid_request",
243 "error": message,
244 "retryable": false,
245 "trace": {"duration_ms": 0}
246 })
247}
248
249fn redact_secrets(value: &mut Value) {
254 match value {
255 Value::Object(map) => {
256 let keys: Vec<String> = map.keys().cloned().collect();
257 for key in keys {
258 if key.ends_with("_secret") || key.ends_with("_SECRET") {
259 match map.get(&key) {
260 Some(Value::Object(_)) | Some(Value::Array(_)) => {
261 }
263 _ => {
264 map.insert(key.clone(), Value::String("***".into()));
265 continue;
266 }
267 }
268 }
269 if let Some(v) = map.get_mut(&key) {
270 redact_secrets(v);
271 }
272 }
273 }
274 Value::Array(arr) => {
275 for v in arr {
276 redact_secrets(v);
277 }
278 }
279 _ => {}
280 }
281}
282
283fn apply_redaction_policy(value: &mut Value, redaction_policy: RedactionPolicy) {
284 match redaction_policy {
285 RedactionPolicy::RedactionTraceOnly => {
286 if let Value::Object(map) = value {
287 if let Some(trace) = map.get_mut("trace") {
288 redact_secrets(trace);
289 }
290 }
291 }
292 RedactionPolicy::RedactionNone => {}
293 }
294}
295
296fn strip_suffix_ci(key: &str, suffix_lower: &str) -> Option<String> {
302 if let Some(s) = key.strip_suffix(suffix_lower) {
303 return Some(s.to_string());
304 }
305 let suffix_upper: String = suffix_lower
306 .chars()
307 .map(|c| c.to_ascii_uppercase())
308 .collect();
309 if let Some(s) = key.strip_suffix(&suffix_upper) {
310 return Some(s.to_string());
311 }
312 None
313}
314
315fn try_strip_generic_cents(key: &str) -> Option<(String, String)> {
317 let code = extract_currency_code(key)?;
318 let suffix_len = code.len() + "_cents".len() + 1; let stripped = &key[..key.len() - suffix_len];
320 if stripped.is_empty() {
321 return None;
322 }
323 Some((stripped.to_string(), code.to_string()))
324}
325
326fn try_process_field(key: &str, value: &Value) -> Option<(String, String)> {
329 if let Some(stripped) = strip_suffix_ci(key, "_epoch_ms") {
331 return value.as_i64().map(|ms| (stripped, format_rfc3339_ms(ms)));
332 }
333 if let Some(stripped) = strip_suffix_ci(key, "_epoch_s") {
334 return value
335 .as_i64()
336 .map(|s| (stripped, format_rfc3339_ms(s * 1000)));
337 }
338 if let Some(stripped) = strip_suffix_ci(key, "_epoch_ns") {
339 return value
340 .as_i64()
341 .map(|ns| (stripped, format_rfc3339_ms(ns.div_euclid(1_000_000))));
342 }
343
344 if let Some(stripped) = strip_suffix_ci(key, "_usd_cents") {
346 return value
347 .as_u64()
348 .map(|n| (stripped, format!("${}.{:02}", n / 100, n % 100)));
349 }
350 if let Some(stripped) = strip_suffix_ci(key, "_eur_cents") {
351 return value
352 .as_u64()
353 .map(|n| (stripped, format!("€{}.{:02}", n / 100, n % 100)));
354 }
355 if let Some((stripped, code)) = try_strip_generic_cents(key) {
356 return value.as_u64().map(|n| {
357 (
358 stripped,
359 format!("{}.{:02} {}", n / 100, n % 100, code.to_uppercase()),
360 )
361 });
362 }
363
364 if let Some(stripped) = strip_suffix_ci(key, "_rfc3339") {
366 return value.as_str().map(|s| (stripped, s.to_string()));
367 }
368 if let Some(stripped) = strip_suffix_ci(key, "_minutes") {
369 return value
370 .is_number()
371 .then(|| (stripped, format!("{} minutes", number_str(value))));
372 }
373 if let Some(stripped) = strip_suffix_ci(key, "_hours") {
374 return value
375 .is_number()
376 .then(|| (stripped, format!("{} hours", number_str(value))));
377 }
378 if let Some(stripped) = strip_suffix_ci(key, "_days") {
379 return value
380 .is_number()
381 .then(|| (stripped, format!("{} days", number_str(value))));
382 }
383
384 if let Some(stripped) = strip_suffix_ci(key, "_msats") {
386 return value
387 .is_number()
388 .then(|| (stripped, format!("{}msats", number_str(value))));
389 }
390 if let Some(stripped) = strip_suffix_ci(key, "_sats") {
391 return value
392 .is_number()
393 .then(|| (stripped, format!("{}sats", number_str(value))));
394 }
395 if let Some(stripped) = strip_suffix_ci(key, "_bytes") {
396 return value.as_i64().map(|n| (stripped, format_bytes_human(n)));
397 }
398 if let Some(stripped) = strip_suffix_ci(key, "_percent") {
399 return value
400 .is_number()
401 .then(|| (stripped, format!("{}%", number_str(value))));
402 }
403 if let Some(stripped) = strip_suffix_ci(key, "_secret") {
404 return Some((stripped, "***".to_string()));
405 }
406
407 if let Some(stripped) = strip_suffix_ci(key, "_btc") {
409 return value
410 .is_number()
411 .then(|| (stripped, format!("{} BTC", number_str(value))));
412 }
413 if let Some(stripped) = strip_suffix_ci(key, "_jpy") {
414 return value
415 .as_u64()
416 .map(|n| (stripped, format!("¥{}", format_with_commas(n))));
417 }
418 if let Some(stripped) = strip_suffix_ci(key, "_ns") {
419 return value
420 .is_number()
421 .then(|| (stripped, format!("{}ns", number_str(value))));
422 }
423 if let Some(stripped) = strip_suffix_ci(key, "_us") {
424 return value
425 .is_number()
426 .then(|| (stripped, format!("{}μs", number_str(value))));
427 }
428 if let Some(stripped) = strip_suffix_ci(key, "_ms") {
429 return format_ms_value(value).map(|v| (stripped, v));
430 }
431 if let Some(stripped) = strip_suffix_ci(key, "_s") {
432 return value
433 .is_number()
434 .then(|| (stripped, format!("{}s", number_str(value))));
435 }
436
437 None
438}
439
440fn process_object_fields<'a>(
442 map: &'a serde_json::Map<String, Value>,
443) -> Vec<(String, &'a Value, Option<String>)> {
444 let mut entries: Vec<(String, &'a str, &'a Value, Option<String>)> = Vec::new();
445 for (key, value) in map {
446 match try_process_field(key, value) {
447 Some((stripped, formatted)) => {
448 entries.push((stripped, key.as_str(), value, Some(formatted)));
449 }
450 None => {
451 entries.push((key.clone(), key.as_str(), value, None));
452 }
453 }
454 }
455
456 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
458 for (stripped, _, _, _) in &entries {
459 *counts.entry(stripped.clone()).or_insert(0) += 1;
460 }
461
462 let mut result: Vec<(String, &'a Value, Option<String>)> = entries
464 .into_iter()
465 .map(|(stripped, original, value, formatted)| {
466 if counts.get(&stripped).copied().unwrap_or(0) > 1 && original != stripped.as_str() {
467 (original.to_string(), value, None)
468 } else {
469 (stripped, value, formatted)
470 }
471 })
472 .collect();
473
474 result.sort_by(|(a, _, _), (b, _, _)| a.encode_utf16().cmp(b.encode_utf16()));
475 result
476}
477
478fn number_str(value: &Value) -> String {
483 match value {
484 Value::Number(n) => n.to_string(),
485 _ => String::new(),
486 }
487}
488
489fn format_ms_as_seconds(ms: f64) -> String {
491 let formatted = format!("{:.3}", ms / 1000.0);
492 let trimmed = formatted.trim_end_matches('0');
493 if trimmed.ends_with('.') {
494 format!("{}0s", trimmed)
495 } else {
496 format!("{}s", trimmed)
497 }
498}
499
500fn format_ms_value(value: &Value) -> Option<String> {
502 let n = value.as_f64()?;
503 if n.abs() >= 1000.0 {
504 Some(format_ms_as_seconds(n))
505 } else if let Some(i) = value.as_i64() {
506 Some(format!("{}ms", i))
507 } else {
508 Some(format!("{}ms", number_str(value)))
509 }
510}
511
512fn format_rfc3339_ms(ms: i64) -> String {
514 use chrono::{DateTime, Utc};
515 let secs = ms.div_euclid(1000);
516 let nanos = (ms.rem_euclid(1000) * 1_000_000) as u32;
517 match DateTime::from_timestamp(secs, nanos) {
518 Some(dt) => dt
519 .with_timezone(&Utc)
520 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
521 None => ms.to_string(),
522 }
523}
524
525fn format_bytes_human(bytes: i64) -> String {
527 const KB: f64 = 1024.0;
528 const MB: f64 = KB * 1024.0;
529 const GB: f64 = MB * 1024.0;
530 const TB: f64 = GB * 1024.0;
531
532 let sign = if bytes < 0 { "-" } else { "" };
533 let b = (bytes as f64).abs();
534 if b >= TB {
535 format!("{sign}{:.1}TB", b / TB)
536 } else if b >= GB {
537 format!("{sign}{:.1}GB", b / GB)
538 } else if b >= MB {
539 format!("{sign}{:.1}MB", b / MB)
540 } else if b >= KB {
541 format!("{sign}{:.1}KB", b / KB)
542 } else {
543 format!("{bytes}B")
544 }
545}
546
547fn format_with_commas(n: u64) -> String {
549 let s = n.to_string();
550 let mut result = String::with_capacity(s.len() + s.len() / 3);
551 for (i, c) in s.chars().enumerate() {
552 if i > 0 && (s.len() - i).is_multiple_of(3) {
553 result.push(',');
554 }
555 result.push(c);
556 }
557 result
558}
559
560fn extract_currency_code(key: &str) -> Option<&str> {
562 let without_cents = key
563 .strip_suffix("_cents")
564 .or_else(|| key.strip_suffix("_CENTS"))?;
565 let last_underscore = without_cents.rfind('_')?;
566 let code = &without_cents[last_underscore + 1..];
567 if code.is_empty() {
568 return None;
569 }
570 Some(code)
571}
572
573fn render_yaml_processed(value: &Value, indent: usize, lines: &mut Vec<String>) {
578 let prefix = " ".repeat(indent);
579 match value {
580 Value::Object(map) => {
581 let processed = process_object_fields(map);
582 for (display_key, v, formatted) in processed {
583 if let Some(fv) = formatted {
584 lines.push(format!(
585 "{}{}: \"{}\"",
586 prefix,
587 display_key,
588 escape_yaml_str(&fv)
589 ));
590 } else {
591 match v {
592 Value::Object(inner) if !inner.is_empty() => {
593 lines.push(format!("{}{}:", prefix, display_key));
594 render_yaml_processed(v, indent + 1, lines);
595 }
596 Value::Object(_) => {
597 lines.push(format!("{}{}: {{}}", prefix, display_key));
598 }
599 Value::Array(arr) => {
600 if arr.is_empty() {
601 lines.push(format!("{}{}: []", prefix, display_key));
602 } else {
603 lines.push(format!("{}{}:", prefix, display_key));
604 for item in arr {
605 if item.is_object() {
606 lines.push(format!("{} -", prefix));
607 render_yaml_processed(item, indent + 2, lines);
608 } else {
609 lines.push(format!("{} - {}", prefix, yaml_scalar(item)));
610 }
611 }
612 }
613 }
614 _ => {
615 lines.push(format!("{}{}: {}", prefix, display_key, yaml_scalar(v)));
616 }
617 }
618 }
619 }
620 }
621 _ => {
622 lines.push(format!("{}{}", prefix, yaml_scalar(value)));
623 }
624 }
625}
626
627fn escape_yaml_str(s: &str) -> String {
628 s.replace('\\', "\\\\")
629 .replace('"', "\\\"")
630 .replace('\n', "\\n")
631 .replace('\r', "\\r")
632 .replace('\t', "\\t")
633}
634
635fn yaml_scalar(value: &Value) -> String {
636 match value {
637 Value::String(s) => {
638 format!("\"{}\"", escape_yaml_str(s))
639 }
640 Value::Null => "null".to_string(),
641 Value::Bool(b) => b.to_string(),
642 Value::Number(n) => n.to_string(),
643 other => format!("\"{}\"", other.to_string().replace('"', "\\\"")),
644 }
645}
646
647fn collect_plain_pairs(value: &Value, prefix: &str, pairs: &mut Vec<(String, String)>) {
652 if let Value::Object(map) = value {
653 let processed = process_object_fields(map);
654 for (display_key, v, formatted) in processed {
655 let full_key = if prefix.is_empty() {
656 display_key
657 } else {
658 format!("{}.{}", prefix, display_key)
659 };
660 if let Some(fv) = formatted {
661 pairs.push((full_key, fv));
662 } else {
663 match v {
664 Value::Object(_) => collect_plain_pairs(v, &full_key, pairs),
665 Value::Array(arr) => {
666 let joined = arr.iter().map(plain_scalar).collect::<Vec<_>>().join(",");
667 pairs.push((full_key, joined));
668 }
669 Value::Null => pairs.push((full_key, String::new())),
670 _ => pairs.push((full_key, plain_scalar(v))),
671 }
672 }
673 }
674 }
675}
676
677fn plain_scalar(value: &Value) -> String {
678 match value {
679 Value::String(s) => s.clone(),
680 Value::Null => "null".to_string(),
681 Value::Bool(b) => b.to_string(),
682 Value::Number(n) => n.to_string(),
683 other => other.to_string(),
684 }
685}
686
687#[cfg(test)]
688mod tests;