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
49pub fn output_json(value: &Value) -> String {
55 let mut v = value.clone();
56 redact_secrets(&mut v);
57 serde_json::to_string(&v).unwrap_or_default()
58}
59
60pub fn output_yaml(value: &Value) -> String {
62 let mut lines = vec!["---".to_string()];
63 render_yaml_processed(value, 0, &mut lines);
64 lines.join("\n")
65}
66
67pub fn output_plain(value: &Value) -> String {
69 let mut pairs: Vec<(String, String)> = Vec::new();
70 collect_plain_pairs(value, "", &mut pairs);
71 pairs.sort_by(|(a, _), (b, _)| a.encode_utf16().cmp(b.encode_utf16()));
72 pairs
73 .into_iter()
74 .map(|(k, v)| {
75 if v.contains(' ') {
76 format!("{}=\"{}\"", k, v)
77 } else {
78 format!("{}={}", k, v)
79 }
80 })
81 .collect::<Vec<_>>()
82 .join(" ")
83}
84
85pub fn internal_redact_secrets(value: &mut Value) {
91 redact_secrets(value);
92}
93
94pub fn parse_size(s: &str) -> Option<u64> {
100 let s = s.trim();
101 if s.is_empty() {
102 return None;
103 }
104 let last = *s.as_bytes().last()?;
105 let (num_str, mult) = match last {
106 b'B' | b'b' => (&s[..s.len() - 1], 1u64),
107 b'K' | b'k' => (&s[..s.len() - 1], 1024),
108 b'M' | b'm' => (&s[..s.len() - 1], 1024 * 1024),
109 b'G' | b'g' => (&s[..s.len() - 1], 1024 * 1024 * 1024),
110 b'T' | b't' => (&s[..s.len() - 1], 1024u64 * 1024 * 1024 * 1024),
111 b'0'..=b'9' | b'.' => (s, 1),
112 _ => return None,
113 };
114 if num_str.is_empty() {
115 return None;
116 }
117 if let Ok(n) = num_str.parse::<u64>() {
118 return n.checked_mul(mult);
119 }
120 let f: f64 = num_str.parse().ok()?;
121 if f < 0.0 || f.is_nan() || f.is_infinite() {
122 return None;
123 }
124 let result = f * mult as f64;
125 if result > u64::MAX as f64 {
126 return None;
127 }
128 Some(result as u64)
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum OutputFormat {
138 Json,
139 Yaml,
140 Plain,
141}
142
143pub fn cli_parse_output(s: &str) -> Result<OutputFormat, String> {
153 match s {
154 "json" => Ok(OutputFormat::Json),
155 "yaml" => Ok(OutputFormat::Yaml),
156 "plain" => Ok(OutputFormat::Plain),
157 _ => Err(format!(
158 "invalid --output format '{s}': expected json, yaml, or plain"
159 )),
160 }
161}
162
163pub fn cli_parse_log_filters<S: AsRef<str>>(entries: &[S]) -> Vec<String> {
173 let mut out: Vec<String> = Vec::new();
174 for entry in entries {
175 let s = entry.as_ref().trim().to_ascii_lowercase();
176 if !s.is_empty() && !out.contains(&s) {
177 out.push(s);
178 }
179 }
180 out
181}
182
183pub fn cli_output(value: &Value, format: OutputFormat) -> String {
194 match format {
195 OutputFormat::Json => output_json(value),
196 OutputFormat::Yaml => output_yaml(value),
197 OutputFormat::Plain => output_plain(value),
198 }
199}
200
201pub fn build_cli_error(message: &str) -> Value {
213 serde_json::json!({
214 "code": "error",
215 "error_code": "invalid_request",
216 "error": message,
217 "retryable": false,
218 "trace": {"duration_ms": 0}
219 })
220}
221
222fn redact_secrets(value: &mut Value) {
227 match value {
228 Value::Object(map) => {
229 let keys: Vec<String> = map.keys().cloned().collect();
230 for key in keys {
231 if key.ends_with("_secret") || key.ends_with("_SECRET") {
232 match map.get(&key) {
233 Some(Value::Object(_)) | Some(Value::Array(_)) => {
234 }
236 _ => {
237 map.insert(key.clone(), Value::String("***".into()));
238 continue;
239 }
240 }
241 }
242 if let Some(v) = map.get_mut(&key) {
243 redact_secrets(v);
244 }
245 }
246 }
247 Value::Array(arr) => {
248 for v in arr {
249 redact_secrets(v);
250 }
251 }
252 _ => {}
253 }
254}
255
256fn strip_suffix_ci(key: &str, suffix_lower: &str) -> Option<String> {
262 if let Some(s) = key.strip_suffix(suffix_lower) {
263 return Some(s.to_string());
264 }
265 let suffix_upper: String = suffix_lower.chars().map(|c| c.to_ascii_uppercase()).collect();
266 if let Some(s) = key.strip_suffix(&suffix_upper) {
267 return Some(s.to_string());
268 }
269 None
270}
271
272fn try_strip_generic_cents(key: &str) -> Option<(String, String)> {
274 let code = extract_currency_code(key)?;
275 let suffix_len = code.len() + "_cents".len() + 1; let stripped = &key[..key.len() - suffix_len];
277 if stripped.is_empty() {
278 return None;
279 }
280 Some((stripped.to_string(), code.to_string()))
281}
282
283fn try_process_field(key: &str, value: &Value) -> Option<(String, String)> {
286 if let Some(stripped) = strip_suffix_ci(key, "_epoch_ms") {
288 return value.as_i64().map(|ms| (stripped, format_rfc3339_ms(ms)));
289 }
290 if let Some(stripped) = strip_suffix_ci(key, "_epoch_s") {
291 return value.as_i64().map(|s| (stripped, format_rfc3339_ms(s * 1000)));
292 }
293 if let Some(stripped) = strip_suffix_ci(key, "_epoch_ns") {
294 return value
295 .as_i64()
296 .map(|ns| (stripped, format_rfc3339_ms(ns.div_euclid(1_000_000))));
297 }
298
299 if let Some(stripped) = strip_suffix_ci(key, "_usd_cents") {
301 return value
302 .as_u64()
303 .map(|n| (stripped, format!("${}.{:02}", n / 100, n % 100)));
304 }
305 if let Some(stripped) = strip_suffix_ci(key, "_eur_cents") {
306 return value
307 .as_u64()
308 .map(|n| (stripped, format!("€{}.{:02}", n / 100, n % 100)));
309 }
310 if let Some((stripped, code)) = try_strip_generic_cents(key) {
311 return value.as_u64().map(|n| {
312 (
313 stripped,
314 format!("{}.{:02} {}", n / 100, n % 100, code.to_uppercase()),
315 )
316 });
317 }
318
319 if let Some(stripped) = strip_suffix_ci(key, "_rfc3339") {
321 return value.as_str().map(|s| (stripped, s.to_string()));
322 }
323 if let Some(stripped) = strip_suffix_ci(key, "_minutes") {
324 return value
325 .is_number()
326 .then(|| (stripped, format!("{} minutes", number_str(value))));
327 }
328 if let Some(stripped) = strip_suffix_ci(key, "_hours") {
329 return value
330 .is_number()
331 .then(|| (stripped, format!("{} hours", number_str(value))));
332 }
333 if let Some(stripped) = strip_suffix_ci(key, "_days") {
334 return value
335 .is_number()
336 .then(|| (stripped, format!("{} days", number_str(value))));
337 }
338
339 if let Some(stripped) = strip_suffix_ci(key, "_msats") {
341 return value
342 .is_number()
343 .then(|| (stripped, format!("{}msats", number_str(value))));
344 }
345 if let Some(stripped) = strip_suffix_ci(key, "_sats") {
346 return value
347 .is_number()
348 .then(|| (stripped, format!("{}sats", number_str(value))));
349 }
350 if let Some(stripped) = strip_suffix_ci(key, "_bytes") {
351 return value.as_i64().map(|n| (stripped, format_bytes_human(n)));
352 }
353 if let Some(stripped) = strip_suffix_ci(key, "_percent") {
354 return value
355 .is_number()
356 .then(|| (stripped, format!("{}%", number_str(value))));
357 }
358 if let Some(stripped) = strip_suffix_ci(key, "_secret") {
359 return Some((stripped, "***".to_string()));
360 }
361
362 if let Some(stripped) = strip_suffix_ci(key, "_btc") {
364 return value
365 .is_number()
366 .then(|| (stripped, format!("{} BTC", number_str(value))));
367 }
368 if let Some(stripped) = strip_suffix_ci(key, "_jpy") {
369 return value
370 .as_u64()
371 .map(|n| (stripped, format!("¥{}", format_with_commas(n))));
372 }
373 if let Some(stripped) = strip_suffix_ci(key, "_ns") {
374 return value
375 .is_number()
376 .then(|| (stripped, format!("{}ns", number_str(value))));
377 }
378 if let Some(stripped) = strip_suffix_ci(key, "_us") {
379 return value
380 .is_number()
381 .then(|| (stripped, format!("{}μs", number_str(value))));
382 }
383 if let Some(stripped) = strip_suffix_ci(key, "_ms") {
384 return format_ms_value(value).map(|v| (stripped, v));
385 }
386 if let Some(stripped) = strip_suffix_ci(key, "_s") {
387 return value
388 .is_number()
389 .then(|| (stripped, format!("{}s", number_str(value))));
390 }
391
392 None
393}
394
395fn process_object_fields<'a>(
397 map: &'a serde_json::Map<String, Value>,
398) -> Vec<(String, &'a Value, Option<String>)> {
399 let mut entries: Vec<(String, &'a str, &'a Value, Option<String>)> = Vec::new();
400 for (key, value) in map {
401 match try_process_field(key, value) {
402 Some((stripped, formatted)) => {
403 entries.push((stripped, key.as_str(), value, Some(formatted)));
404 }
405 None => {
406 entries.push((key.clone(), key.as_str(), value, None));
407 }
408 }
409 }
410
411 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
413 for (stripped, _, _, _) in &entries {
414 *counts.entry(stripped.clone()).or_insert(0) += 1;
415 }
416
417 let mut result: Vec<(String, &'a Value, Option<String>)> = entries
419 .into_iter()
420 .map(|(stripped, original, value, formatted)| {
421 if counts.get(&stripped).copied().unwrap_or(0) > 1
422 && original != stripped.as_str()
423 {
424 (original.to_string(), value, None)
425 } else {
426 (stripped, value, formatted)
427 }
428 })
429 .collect();
430
431 result.sort_by(|(a, _, _), (b, _, _)| a.encode_utf16().cmp(b.encode_utf16()));
432 result
433}
434
435fn number_str(value: &Value) -> String {
440 match value {
441 Value::Number(n) => n.to_string(),
442 _ => String::new(),
443 }
444}
445
446fn format_ms_as_seconds(ms: f64) -> String {
448 let formatted = format!("{:.3}", ms / 1000.0);
449 let trimmed = formatted.trim_end_matches('0');
450 if trimmed.ends_with('.') {
451 format!("{}0s", trimmed)
452 } else {
453 format!("{}s", trimmed)
454 }
455}
456
457fn format_ms_value(value: &Value) -> Option<String> {
459 let n = value.as_f64()?;
460 if n.abs() >= 1000.0 {
461 Some(format_ms_as_seconds(n))
462 } else if let Some(i) = value.as_i64() {
463 Some(format!("{}ms", i))
464 } else {
465 Some(format!("{}ms", number_str(value)))
466 }
467}
468
469fn format_rfc3339_ms(ms: i64) -> String {
471 use chrono::{DateTime, Utc};
472 let secs = ms.div_euclid(1000);
473 let nanos = (ms.rem_euclid(1000) * 1_000_000) as u32;
474 match DateTime::from_timestamp(secs, nanos) {
475 Some(dt) => dt
476 .with_timezone(&Utc)
477 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
478 None => ms.to_string(),
479 }
480}
481
482fn format_bytes_human(bytes: i64) -> String {
484 const KB: f64 = 1024.0;
485 const MB: f64 = KB * 1024.0;
486 const GB: f64 = MB * 1024.0;
487 const TB: f64 = GB * 1024.0;
488
489 let sign = if bytes < 0 { "-" } else { "" };
490 let b = (bytes as f64).abs();
491 if b >= TB {
492 format!("{sign}{:.1}TB", b / TB)
493 } else if b >= GB {
494 format!("{sign}{:.1}GB", b / GB)
495 } else if b >= MB {
496 format!("{sign}{:.1}MB", b / MB)
497 } else if b >= KB {
498 format!("{sign}{:.1}KB", b / KB)
499 } else {
500 format!("{bytes}B")
501 }
502}
503
504fn format_with_commas(n: u64) -> String {
506 let s = n.to_string();
507 let mut result = String::with_capacity(s.len() + s.len() / 3);
508 for (i, c) in s.chars().enumerate() {
509 if i > 0 && (s.len() - i).is_multiple_of(3) {
510 result.push(',');
511 }
512 result.push(c);
513 }
514 result
515}
516
517fn extract_currency_code(key: &str) -> Option<&str> {
519 let without_cents = key
520 .strip_suffix("_cents")
521 .or_else(|| key.strip_suffix("_CENTS"))?;
522 let last_underscore = without_cents.rfind('_')?;
523 let code = &without_cents[last_underscore + 1..];
524 if code.is_empty() {
525 return None;
526 }
527 Some(code)
528}
529
530fn render_yaml_processed(value: &Value, indent: usize, lines: &mut Vec<String>) {
535 let prefix = " ".repeat(indent);
536 match value {
537 Value::Object(map) => {
538 let processed = process_object_fields(map);
539 for (display_key, v, formatted) in processed {
540 if let Some(fv) = formatted {
541 lines.push(format!(
542 "{}{}: \"{}\"",
543 prefix,
544 display_key,
545 escape_yaml_str(&fv)
546 ));
547 } else {
548 match v {
549 Value::Object(inner) if !inner.is_empty() => {
550 lines.push(format!("{}{}:", prefix, display_key));
551 render_yaml_processed(v, indent + 1, lines);
552 }
553 Value::Object(_) => {
554 lines.push(format!("{}{}: {{}}", prefix, display_key));
555 }
556 Value::Array(arr) => {
557 if arr.is_empty() {
558 lines.push(format!("{}{}: []", prefix, display_key));
559 } else {
560 lines.push(format!("{}{}:", prefix, display_key));
561 for item in arr {
562 if item.is_object() {
563 lines.push(format!("{} -", prefix));
564 render_yaml_processed(item, indent + 2, lines);
565 } else {
566 lines.push(format!(
567 "{} - {}",
568 prefix,
569 yaml_scalar(item)
570 ));
571 }
572 }
573 }
574 }
575 _ => {
576 lines.push(format!(
577 "{}{}: {}",
578 prefix,
579 display_key,
580 yaml_scalar(v)
581 ));
582 }
583 }
584 }
585 }
586 }
587 _ => {
588 lines.push(format!("{}{}", prefix, yaml_scalar(value)));
589 }
590 }
591}
592
593fn escape_yaml_str(s: &str) -> String {
594 s.replace('\\', "\\\\")
595 .replace('"', "\\\"")
596 .replace('\n', "\\n")
597 .replace('\r', "\\r")
598 .replace('\t', "\\t")
599}
600
601fn yaml_scalar(value: &Value) -> String {
602 match value {
603 Value::String(s) => {
604 format!("\"{}\"", escape_yaml_str(s))
605 }
606 Value::Null => "null".to_string(),
607 Value::Bool(b) => b.to_string(),
608 Value::Number(n) => n.to_string(),
609 other => format!("\"{}\"", other.to_string().replace('"', "\\\"")),
610 }
611}
612
613fn collect_plain_pairs(value: &Value, prefix: &str, pairs: &mut Vec<(String, String)>) {
618 if let Value::Object(map) = value {
619 let processed = process_object_fields(map);
620 for (display_key, v, formatted) in processed {
621 let full_key = if prefix.is_empty() {
622 display_key
623 } else {
624 format!("{}.{}", prefix, display_key)
625 };
626 if let Some(fv) = formatted {
627 pairs.push((full_key, fv));
628 } else {
629 match v {
630 Value::Object(_) => collect_plain_pairs(v, &full_key, pairs),
631 Value::Array(arr) => {
632 let joined = arr.iter().map(plain_scalar).collect::<Vec<_>>().join(",");
633 pairs.push((full_key, joined));
634 }
635 Value::Null => pairs.push((full_key, String::new())),
636 _ => pairs.push((full_key, plain_scalar(v))),
637 }
638 }
639 }
640 }
641}
642
643fn plain_scalar(value: &Value) -> String {
644 match value {
645 Value::String(s) => s.clone(),
646 Value::Null => "null".to_string(),
647 Value::Bool(b) => b.to_string(),
648 Value::Number(n) => n.to_string(),
649 other => other.to_string(),
650 }
651}
652
653#[cfg(test)]
654mod tests;