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