1#[cfg(feature = "tracing")]
6pub mod afdata_tracing;
7
8use serde_json::Value;
9
10pub fn build_json_ok(result: Value, trace: Option<Value>) -> Value {
16 match trace {
17 Some(t) => serde_json::json!({"code": "ok", "result": result, "trace": t}),
18 None => serde_json::json!({"code": "ok", "result": result}),
19 }
20}
21
22pub fn build_json_error(message: &str, trace: Option<Value>) -> Value {
24 match trace {
25 Some(t) => serde_json::json!({"code": "error", "error": message, "trace": t}),
26 None => serde_json::json!({"code": "error", "error": message}),
27 }
28}
29
30pub fn build_json(code: &str, fields: Value, trace: Option<Value>) -> Value {
32 let mut obj = match fields {
33 Value::Object(map) => map,
34 _ => serde_json::Map::new(),
35 };
36 obj.insert("code".to_string(), Value::String(code.to_string()));
37 if let Some(t) = trace {
38 obj.insert("trace".to_string(), t);
39 }
40 Value::Object(obj)
41}
42
43pub fn output_json(value: &Value) -> String {
49 let mut v = value.clone();
50 redact_secrets(&mut v);
51 serde_json::to_string(&v).unwrap_or_default()
52}
53
54pub fn output_yaml(value: &Value) -> String {
56 let mut lines = vec!["---".to_string()];
57 render_yaml_processed(value, 0, &mut lines);
58 lines.join("\n")
59}
60
61pub fn output_plain(value: &Value) -> String {
63 let mut pairs: Vec<(String, String)> = Vec::new();
64 collect_plain_pairs(value, "", &mut pairs);
65 pairs.sort_by(|(a, _), (b, _)| a.encode_utf16().cmp(b.encode_utf16()));
66 pairs
67 .into_iter()
68 .map(|(k, v)| {
69 if v.contains(' ') {
70 format!("{}=\"{}\"", k, v)
71 } else {
72 format!("{}={}", k, v)
73 }
74 })
75 .collect::<Vec<_>>()
76 .join(" ")
77}
78
79pub fn internal_redact_secrets(value: &mut Value) {
85 redact_secrets(value);
86}
87
88pub fn parse_size(s: &str) -> Option<u64> {
94 let s = s.trim();
95 if s.is_empty() {
96 return None;
97 }
98 let last = *s.as_bytes().last()?;
99 let (num_str, mult) = match last {
100 b'B' | b'b' => (&s[..s.len() - 1], 1u64),
101 b'K' | b'k' => (&s[..s.len() - 1], 1024),
102 b'M' | b'm' => (&s[..s.len() - 1], 1024 * 1024),
103 b'G' | b'g' => (&s[..s.len() - 1], 1024 * 1024 * 1024),
104 b'T' | b't' => (&s[..s.len() - 1], 1024u64 * 1024 * 1024 * 1024),
105 b'0'..=b'9' | b'.' => (s, 1),
106 _ => return None,
107 };
108 if num_str.is_empty() {
109 return None;
110 }
111 if let Ok(n) = num_str.parse::<u64>() {
112 return n.checked_mul(mult);
113 }
114 let f: f64 = num_str.parse().ok()?;
115 if f < 0.0 || f.is_nan() || f.is_infinite() {
116 return None;
117 }
118 let result = f * mult as f64;
119 if result > u64::MAX as f64 {
120 return None;
121 }
122 Some(result as u64)
123}
124
125fn redact_secrets(value: &mut Value) {
130 match value {
131 Value::Object(map) => {
132 let keys: Vec<String> = map.keys().cloned().collect();
133 for key in keys {
134 if key.ends_with("_secret") || key.ends_with("_SECRET") {
135 match map.get(&key) {
136 Some(Value::Object(_)) | Some(Value::Array(_)) => {
137 }
139 _ => {
140 map.insert(key.clone(), Value::String("***".into()));
141 continue;
142 }
143 }
144 }
145 if let Some(v) = map.get_mut(&key) {
146 redact_secrets(v);
147 }
148 }
149 }
150 Value::Array(arr) => {
151 for v in arr {
152 redact_secrets(v);
153 }
154 }
155 _ => {}
156 }
157}
158
159fn strip_suffix_ci(key: &str, suffix_lower: &str) -> Option<String> {
165 if let Some(s) = key.strip_suffix(suffix_lower) {
166 return Some(s.to_string());
167 }
168 let suffix_upper: String = suffix_lower.chars().map(|c| c.to_ascii_uppercase()).collect();
169 if let Some(s) = key.strip_suffix(&suffix_upper) {
170 return Some(s.to_string());
171 }
172 None
173}
174
175fn try_strip_generic_cents(key: &str) -> Option<(String, String)> {
177 let code = extract_currency_code(key)?;
178 let suffix_len = code.len() + "_cents".len() + 1; let stripped = &key[..key.len() - suffix_len];
180 if stripped.is_empty() {
181 return None;
182 }
183 Some((stripped.to_string(), code.to_string()))
184}
185
186fn try_process_field(key: &str, value: &Value) -> Option<(String, String)> {
189 if let Some(stripped) = strip_suffix_ci(key, "_epoch_ms") {
191 return value.as_i64().map(|ms| (stripped, format_rfc3339_ms(ms)));
192 }
193 if let Some(stripped) = strip_suffix_ci(key, "_epoch_s") {
194 return value.as_i64().map(|s| (stripped, format_rfc3339_ms(s * 1000)));
195 }
196 if let Some(stripped) = strip_suffix_ci(key, "_epoch_ns") {
197 return value
198 .as_i64()
199 .map(|ns| (stripped, format_rfc3339_ms(ns.div_euclid(1_000_000))));
200 }
201
202 if let Some(stripped) = strip_suffix_ci(key, "_usd_cents") {
204 return value
205 .as_u64()
206 .map(|n| (stripped, format!("${}.{:02}", n / 100, n % 100)));
207 }
208 if let Some(stripped) = strip_suffix_ci(key, "_eur_cents") {
209 return value
210 .as_u64()
211 .map(|n| (stripped, format!("€{}.{:02}", n / 100, n % 100)));
212 }
213 if let Some((stripped, code)) = try_strip_generic_cents(key) {
214 return value.as_u64().map(|n| {
215 (
216 stripped,
217 format!("{}.{:02} {}", n / 100, n % 100, code.to_uppercase()),
218 )
219 });
220 }
221
222 if let Some(stripped) = strip_suffix_ci(key, "_rfc3339") {
224 return value.as_str().map(|s| (stripped, s.to_string()));
225 }
226 if let Some(stripped) = strip_suffix_ci(key, "_minutes") {
227 return value
228 .is_number()
229 .then(|| (stripped, format!("{} minutes", number_str(value))));
230 }
231 if let Some(stripped) = strip_suffix_ci(key, "_hours") {
232 return value
233 .is_number()
234 .then(|| (stripped, format!("{} hours", number_str(value))));
235 }
236 if let Some(stripped) = strip_suffix_ci(key, "_days") {
237 return value
238 .is_number()
239 .then(|| (stripped, format!("{} days", number_str(value))));
240 }
241
242 if let Some(stripped) = strip_suffix_ci(key, "_msats") {
244 return value
245 .is_number()
246 .then(|| (stripped, format!("{}msats", number_str(value))));
247 }
248 if let Some(stripped) = strip_suffix_ci(key, "_sats") {
249 return value
250 .is_number()
251 .then(|| (stripped, format!("{}sats", number_str(value))));
252 }
253 if let Some(stripped) = strip_suffix_ci(key, "_bytes") {
254 return value.as_i64().map(|n| (stripped, format_bytes_human(n)));
255 }
256 if let Some(stripped) = strip_suffix_ci(key, "_percent") {
257 return value
258 .is_number()
259 .then(|| (stripped, format!("{}%", number_str(value))));
260 }
261 if let Some(stripped) = strip_suffix_ci(key, "_secret") {
262 return Some((stripped, "***".to_string()));
263 }
264
265 if let Some(stripped) = strip_suffix_ci(key, "_btc") {
267 return value
268 .is_number()
269 .then(|| (stripped, format!("{} BTC", number_str(value))));
270 }
271 if let Some(stripped) = strip_suffix_ci(key, "_jpy") {
272 return value
273 .as_u64()
274 .map(|n| (stripped, format!("¥{}", format_with_commas(n))));
275 }
276 if let Some(stripped) = strip_suffix_ci(key, "_ns") {
277 return value
278 .is_number()
279 .then(|| (stripped, format!("{}ns", number_str(value))));
280 }
281 if let Some(stripped) = strip_suffix_ci(key, "_us") {
282 return value
283 .is_number()
284 .then(|| (stripped, format!("{}μs", number_str(value))));
285 }
286 if let Some(stripped) = strip_suffix_ci(key, "_ms") {
287 return format_ms_value(value).map(|v| (stripped, v));
288 }
289 if let Some(stripped) = strip_suffix_ci(key, "_s") {
290 return value
291 .is_number()
292 .then(|| (stripped, format!("{}s", number_str(value))));
293 }
294
295 None
296}
297
298fn process_object_fields<'a>(
300 map: &'a serde_json::Map<String, Value>,
301) -> Vec<(String, &'a Value, Option<String>)> {
302 let mut entries: Vec<(String, &'a str, &'a Value, Option<String>)> = Vec::new();
303 for (key, value) in map {
304 match try_process_field(key, value) {
305 Some((stripped, formatted)) => {
306 entries.push((stripped, key.as_str(), value, Some(formatted)));
307 }
308 None => {
309 entries.push((key.clone(), key.as_str(), value, None));
310 }
311 }
312 }
313
314 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
316 for (stripped, _, _, _) in &entries {
317 *counts.entry(stripped.clone()).or_insert(0) += 1;
318 }
319
320 let mut result: Vec<(String, &'a Value, Option<String>)> = entries
322 .into_iter()
323 .map(|(stripped, original, value, formatted)| {
324 if counts.get(&stripped).copied().unwrap_or(0) > 1
325 && original != stripped.as_str()
326 {
327 (original.to_string(), value, None)
328 } else {
329 (stripped, value, formatted)
330 }
331 })
332 .collect();
333
334 result.sort_by(|(a, _, _), (b, _, _)| a.encode_utf16().cmp(b.encode_utf16()));
335 result
336}
337
338fn number_str(value: &Value) -> String {
343 match value {
344 Value::Number(n) => n.to_string(),
345 _ => String::new(),
346 }
347}
348
349fn format_ms_as_seconds(ms: f64) -> String {
351 let formatted = format!("{:.3}", ms / 1000.0);
352 let trimmed = formatted.trim_end_matches('0');
353 if trimmed.ends_with('.') {
354 format!("{}0s", trimmed)
355 } else {
356 format!("{}s", trimmed)
357 }
358}
359
360fn format_ms_value(value: &Value) -> Option<String> {
362 let n = value.as_f64()?;
363 if n.abs() >= 1000.0 {
364 Some(format_ms_as_seconds(n))
365 } else if let Some(i) = value.as_i64() {
366 Some(format!("{}ms", i))
367 } else {
368 Some(format!("{}ms", number_str(value)))
369 }
370}
371
372fn format_rfc3339_ms(ms: i64) -> String {
374 use chrono::{DateTime, Utc};
375 let secs = ms.div_euclid(1000);
376 let nanos = (ms.rem_euclid(1000) * 1_000_000) as u32;
377 match DateTime::from_timestamp(secs, nanos) {
378 Some(dt) => dt
379 .with_timezone(&Utc)
380 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
381 None => ms.to_string(),
382 }
383}
384
385fn format_bytes_human(bytes: i64) -> String {
387 const KB: f64 = 1024.0;
388 const MB: f64 = KB * 1024.0;
389 const GB: f64 = MB * 1024.0;
390 const TB: f64 = GB * 1024.0;
391
392 let sign = if bytes < 0 { "-" } else { "" };
393 let b = (bytes as f64).abs();
394 if b >= TB {
395 format!("{sign}{:.1}TB", b / TB)
396 } else if b >= GB {
397 format!("{sign}{:.1}GB", b / GB)
398 } else if b >= MB {
399 format!("{sign}{:.1}MB", b / MB)
400 } else if b >= KB {
401 format!("{sign}{:.1}KB", b / KB)
402 } else {
403 format!("{bytes}B")
404 }
405}
406
407fn format_with_commas(n: u64) -> String {
409 let s = n.to_string();
410 let mut result = String::with_capacity(s.len() + s.len() / 3);
411 for (i, c) in s.chars().enumerate() {
412 if i > 0 && (s.len() - i).is_multiple_of(3) {
413 result.push(',');
414 }
415 result.push(c);
416 }
417 result
418}
419
420fn extract_currency_code(key: &str) -> Option<&str> {
422 let without_cents = key
423 .strip_suffix("_cents")
424 .or_else(|| key.strip_suffix("_CENTS"))?;
425 let last_underscore = without_cents.rfind('_')?;
426 let code = &without_cents[last_underscore + 1..];
427 if code.is_empty() {
428 return None;
429 }
430 Some(code)
431}
432
433fn render_yaml_processed(value: &Value, indent: usize, lines: &mut Vec<String>) {
438 let prefix = " ".repeat(indent);
439 match value {
440 Value::Object(map) => {
441 let processed = process_object_fields(map);
442 for (display_key, v, formatted) in processed {
443 if let Some(fv) = formatted {
444 lines.push(format!(
445 "{}{}: \"{}\"",
446 prefix,
447 display_key,
448 escape_yaml_str(&fv)
449 ));
450 } else {
451 match v {
452 Value::Object(inner) if !inner.is_empty() => {
453 lines.push(format!("{}{}:", prefix, display_key));
454 render_yaml_processed(v, indent + 1, lines);
455 }
456 Value::Object(_) => {
457 lines.push(format!("{}{}: {{}}", prefix, display_key));
458 }
459 Value::Array(arr) => {
460 if arr.is_empty() {
461 lines.push(format!("{}{}: []", prefix, display_key));
462 } else {
463 lines.push(format!("{}{}:", prefix, display_key));
464 for item in arr {
465 if item.is_object() {
466 lines.push(format!("{} -", prefix));
467 render_yaml_processed(item, indent + 2, lines);
468 } else {
469 lines.push(format!(
470 "{} - {}",
471 prefix,
472 yaml_scalar(item)
473 ));
474 }
475 }
476 }
477 }
478 _ => {
479 lines.push(format!(
480 "{}{}: {}",
481 prefix,
482 display_key,
483 yaml_scalar(v)
484 ));
485 }
486 }
487 }
488 }
489 }
490 _ => {
491 lines.push(format!("{}{}", prefix, yaml_scalar(value)));
492 }
493 }
494}
495
496fn escape_yaml_str(s: &str) -> String {
497 s.replace('\\', "\\\\")
498 .replace('"', "\\\"")
499 .replace('\n', "\\n")
500 .replace('\r', "\\r")
501 .replace('\t', "\\t")
502}
503
504fn yaml_scalar(value: &Value) -> String {
505 match value {
506 Value::String(s) => {
507 format!("\"{}\"", escape_yaml_str(s))
508 }
509 Value::Null => "null".to_string(),
510 Value::Bool(b) => b.to_string(),
511 Value::Number(n) => n.to_string(),
512 other => format!("\"{}\"", other.to_string().replace('"', "\\\"")),
513 }
514}
515
516fn collect_plain_pairs(value: &Value, prefix: &str, pairs: &mut Vec<(String, String)>) {
521 if let Value::Object(map) = value {
522 let processed = process_object_fields(map);
523 for (display_key, v, formatted) in processed {
524 let full_key = if prefix.is_empty() {
525 display_key
526 } else {
527 format!("{}.{}", prefix, display_key)
528 };
529 if let Some(fv) = formatted {
530 pairs.push((full_key, fv));
531 } else {
532 match v {
533 Value::Object(_) => collect_plain_pairs(v, &full_key, pairs),
534 Value::Array(arr) => {
535 let joined = arr.iter().map(plain_scalar).collect::<Vec<_>>().join(",");
536 pairs.push((full_key, joined));
537 }
538 Value::Null => pairs.push((full_key, String::new())),
539 _ => pairs.push((full_key, plain_scalar(v))),
540 }
541 }
542 }
543 }
544}
545
546fn plain_scalar(value: &Value) -> String {
547 match value {
548 Value::String(s) => s.clone(),
549 Value::Null => "null".to_string(),
550 Value::Bool(b) => b.to_string(),
551 Value::Number(n) => n.to_string(),
552 other => other.to_string(),
553 }
554}
555
556#[cfg(test)]
557mod tests;