#[cfg(feature = "tracing")]
pub mod afdata_tracing;
#[cfg(feature = "skill-admin")]
pub mod skill;
use serde_json::Value;
use std::collections::HashSet;
pub fn build_json_ok(result: Value, trace: Option<Value>) -> Value {
match trace {
Some(t) => serde_json::json!({"code": "ok", "result": result, "trace": t}),
None => serde_json::json!({"code": "ok", "result": result}),
}
}
pub fn build_json_error(message: &str, hint: Option<&str>, trace: Option<Value>) -> Value {
let mut obj = serde_json::Map::new();
obj.insert("code".to_string(), Value::String("error".to_string()));
obj.insert("error".to_string(), Value::String(message.to_string()));
if let Some(h) = hint {
obj.insert("hint".to_string(), Value::String(h.to_string()));
}
if let Some(t) = trace {
obj.insert("trace".to_string(), t);
}
Value::Object(obj)
}
pub fn build_json(code: &str, fields: Value, trace: Option<Value>) -> Value {
let mut obj = match fields {
Value::Object(map) => map,
_ => serde_json::Map::new(),
};
obj.insert("code".to_string(), Value::String(code.to_string()));
if let Some(t) = trace {
obj.insert("trace".to_string(), t);
}
Value::Object(obj)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RedactionPolicy {
RedactionTraceOnly,
RedactionNone,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RedactionOptions {
pub policy: Option<RedactionPolicy>,
pub secret_names: Vec<String>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OutputStyle {
#[default]
Readable,
Raw,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct OutputOptions {
pub redaction: RedactionOptions,
pub style: OutputStyle,
}
pub fn output_json(value: &Value) -> String {
serialize_json_output(&redacted_value(value))
}
pub fn output_json_with(value: &Value, redaction_policy: RedactionPolicy) -> String {
serialize_json_output(&redacted_value_with(value, redaction_policy))
}
pub fn output_json_with_options(value: &Value, output_options: &OutputOptions) -> String {
serialize_json_output(&redacted_value_with_options(
value,
&output_options.redaction,
))
}
fn serialize_json_output(value: &Value) -> String {
match serde_json::to_string(value) {
Ok(s) => s,
Err(err) => serde_json::json!({
"error": "output_json_failed",
"detail": err.to_string(),
})
.to_string(),
}
}
pub fn output_yaml(value: &Value) -> String {
output_yaml_with_options(value, &OutputOptions::default())
}
pub fn output_yaml_with_options(value: &Value, output_options: &OutputOptions) -> String {
let mut lines = vec!["---".to_string()];
let v = redacted_value_with_options(value, &output_options.redaction);
match output_options.style {
OutputStyle::Readable => render_yaml_processed(&v, 0, &mut lines),
OutputStyle::Raw => render_yaml_raw(&v, 0, &mut lines),
}
lines.join("\n")
}
pub fn output_plain(value: &Value) -> String {
output_plain_with_options(value, &OutputOptions::default())
}
pub fn output_plain_with_options(value: &Value, output_options: &OutputOptions) -> String {
let mut pairs: Vec<(String, String)> = Vec::new();
let v = redacted_value_with_options(value, &output_options.redaction);
match output_options.style {
OutputStyle::Readable => collect_plain_pairs(&v, "", &mut pairs),
OutputStyle::Raw => collect_plain_pairs_raw(&v, "", &mut pairs),
}
pairs.sort_by(|(a, _), (b, _)| a.encode_utf16().cmp(b.encode_utf16()));
pairs
.into_iter()
.map(|(k, v)| format!("{}={}", quote_logfmt_key(&k), quote_logfmt_value(&v)))
.collect::<Vec<_>>()
.join(" ")
}
pub fn redact_secrets_in_place(value: &mut Value) {
redact_secrets(value);
}
pub fn redact_secrets_in_place_with_options(
value: &mut Value,
redaction_options: &RedactionOptions,
) {
apply_redaction_options(value, redaction_options);
}
pub fn redacted_value(value: &Value) -> Value {
let mut v = value.clone();
redact_secrets(&mut v);
v
}
pub fn redacted_value_with(value: &Value, redaction_policy: RedactionPolicy) -> Value {
let mut v = value.clone();
apply_redaction_policy(&mut v, redaction_policy);
v
}
pub fn redacted_value_with_options(value: &Value, redaction_options: &RedactionOptions) -> Value {
let mut v = value.clone();
apply_redaction_options(&mut v, redaction_options);
v
}
pub fn redact_url_secrets(url: &str) -> String {
redact_url_secrets_with_options(url, &RedactionOptions::default())
}
pub fn redact_url_secrets_with_options(url: &str, redaction_options: &RedactionOptions) -> String {
let context = RedactionContext::from_options(redaction_options);
redact_url_in_str(url, &context).unwrap_or_else(|| url.to_string())
}
pub fn parse_size(s: &str) -> Option<u64> {
const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
let s = s.trim();
if s.is_empty() {
return None;
}
let last = *s.as_bytes().last()?;
let (num_str, mult) = match last {
b'B' | b'b' => (&s[..s.len() - 1], 1u64),
b'K' | b'k' => (&s[..s.len() - 1], 1024),
b'M' | b'm' => (&s[..s.len() - 1], 1024 * 1024),
b'G' | b'g' => (&s[..s.len() - 1], 1024 * 1024 * 1024),
b'T' | b't' => (&s[..s.len() - 1], 1024u64 * 1024 * 1024 * 1024),
b'0'..=b'9' | b'.' => (s, 1),
_ => return None,
};
if num_str.is_empty() || !is_decimal_number(num_str) {
return None;
}
if let Ok(n) = num_str.parse::<u64>() {
let result = n.checked_mul(mult)?;
return (result <= MAX_SAFE_INTEGER).then_some(result);
}
if !num_str.contains('.') && !num_str.contains('e') && !num_str.contains('E') {
return None;
}
let f: f64 = num_str.parse().ok()?;
if f < 0.0 || f.is_nan() || f.is_infinite() {
return None;
}
let result = f * mult as f64;
if result > MAX_SAFE_INTEGER as f64 {
return None;
}
Some(result as u64)
}
fn is_decimal_number(s: &str) -> bool {
let bytes = s.as_bytes();
let mut i = 0;
let mut digits = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
digits += 1;
}
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
digits += 1;
}
}
if digits == 0 {
return false;
}
if i < bytes.len() && matches!(bytes[i], b'e' | b'E') {
i += 1;
if i < bytes.len() && matches!(bytes[i], b'+' | b'-') {
i += 1;
}
let exp_start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if i == exp_start {
return false;
}
}
i == bytes.len()
}
pub fn normalize_utc_offset(s: &str) -> Option<String> {
let s = s.trim();
if s.eq_ignore_ascii_case("utc") || s.eq_ignore_ascii_case("z") {
return Some("UTC".to_string());
}
let sign = match s.as_bytes().first()? {
b'+' => '+',
b'-' => '-',
_ => return None,
};
let body = &s[1..];
let (hours, minutes) = parse_utc_offset_body(body)?;
if hours > 23 || minutes > 59 {
return None;
}
if hours == 0 && minutes == 0 {
return Some("UTC".to_string());
}
Some(format!("{sign}{hours:02}:{minutes:02}"))
}
pub fn is_valid_rfc3339_date(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
return false;
}
let Some(year) = parse_ascii_u16_bytes(&bytes[0..4]) else {
return false;
};
let Some(month) = parse_ascii_u8_bytes(&bytes[5..7]) else {
return false;
};
let Some(day) = parse_ascii_u8_bytes(&bytes[8..10]) else {
return false;
};
(1..=12).contains(&month) && (1..=days_in_month(year, month)).contains(&day)
}
pub fn is_valid_rfc3339_time(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.len() < 8 || bytes[2] != b':' || bytes[5] != b':' {
return false;
}
let Some(hour) = parse_ascii_u8_bytes(&bytes[0..2]) else {
return false;
};
let Some(minute) = parse_ascii_u8_bytes(&bytes[3..5]) else {
return false;
};
let Some(second) = parse_ascii_u8_bytes(&bytes[6..8]) else {
return false;
};
if hour > 23 || minute > 59 || second > 59 {
return false;
}
if bytes.len() == 8 {
return true;
}
bytes[8] == b'.' && bytes.len() > 9 && bytes[9..].iter().all(u8::is_ascii_digit)
}
fn parse_utc_offset_body(body: &str) -> Option<(u8, u8)> {
if body.is_empty() {
return None;
}
if let Some((hours, minutes)) = body.split_once(':') {
if hours.is_empty() || hours.len() > 2 || minutes.len() != 2 {
return None;
}
return Some((parse_ascii_u8(hours)?, parse_ascii_u8(minutes)?));
}
if !body.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
match body.len() {
1 | 2 => Some((parse_ascii_u8(body)?, 0)),
4 => Some((parse_ascii_u8(&body[..2])?, parse_ascii_u8(&body[2..])?)),
_ => None,
}
}
fn parse_ascii_u8(s: &str) -> Option<u8> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse().ok()
}
fn parse_ascii_u8_bytes(bytes: &[u8]) -> Option<u8> {
let n = parse_ascii_u16_bytes(bytes)?;
u8::try_from(n).ok()
}
fn parse_ascii_u16_bytes(bytes: &[u8]) -> Option<u16> {
if bytes.is_empty() || !bytes.iter().all(u8::is_ascii_digit) {
return None;
}
let mut value = 0u16;
for byte in bytes {
value = value.checked_mul(10)?;
value = value.checked_add(u16::from(byte - b'0'))?;
}
Some(value)
}
fn days_in_month(year: u16, month: u8) -> u8 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap_year(year) => 29,
2 => 28,
_ => 0,
}
}
fn is_leap_year(year: u16) -> bool {
let year = u32::from(year);
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OutputFormat {
Json,
Yaml,
Plain,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VersionConfig {
pub default_output: Option<OutputFormat>,
pub output_flag: Option<&'static str>,
pub output_short: Option<char>,
pub allow_output_format: bool,
}
impl VersionConfig {
pub const fn new(default_output: Option<OutputFormat>) -> Self {
Self {
default_output,
output_flag: None,
output_short: None,
allow_output_format: false,
}
}
pub const fn agent_cli_default() -> Self {
Self {
default_output: Some(OutputFormat::Json),
output_flag: Some("--output"),
output_short: None,
allow_output_format: true,
}
}
pub const fn conventional_default() -> Self {
Self {
default_output: None,
output_flag: Some("--output"),
output_short: None,
allow_output_format: true,
}
}
pub const fn with_default_output(mut self, default_output: Option<OutputFormat>) -> Self {
self.default_output = default_output;
self
}
pub const fn with_output_flag(mut self, flag: Option<&'static str>) -> Self {
self.output_flag = flag;
self
}
pub const fn with_output_short(mut self, flag: Option<char>) -> Self {
self.output_short = flag;
self
}
pub const fn with_output_format_override(mut self, enabled: bool) -> Self {
self.allow_output_format = enabled;
self
}
}
pub fn cli_parse_output(s: &str) -> Result<OutputFormat, String> {
match s {
"json" => Ok(OutputFormat::Json),
"yaml" => Ok(OutputFormat::Yaml),
"plain" => Ok(OutputFormat::Plain),
_ => Err(format!(
"invalid --output format '{s}': expected json, yaml, or plain"
)),
}
}
pub fn cli_parse_log_filters<S: AsRef<str>>(entries: &[S]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for entry in entries {
let s = entry.as_ref().trim().to_ascii_lowercase();
if !s.is_empty() && !out.contains(&s) {
out.push(s);
}
}
out
}
pub fn cli_output(value: &Value, format: OutputFormat) -> String {
match format {
OutputFormat::Json => output_json(value),
OutputFormat::Yaml => output_yaml(value),
OutputFormat::Plain => output_plain(value),
}
}
pub fn cli_output_with_options(
value: &Value,
format: OutputFormat,
output_options: &OutputOptions,
) -> String {
match format {
OutputFormat::Json => output_json_with_options(value, output_options),
OutputFormat::Yaml => output_yaml_with_options(value, output_options),
OutputFormat::Plain => output_plain_with_options(value, output_options),
}
}
pub fn build_cli_version(version: &str) -> Value {
build_json("version", serde_json::json!({ "version": version }), None)
}
pub fn cli_render_version(name: &str, version: &str, format: Option<OutputFormat>) -> String {
let mut rendered = match format {
Some(format) => cli_output(&build_cli_version(version), format),
None => format!("{name} {version}"),
};
while rendered.ends_with('\n') {
rendered.pop();
}
rendered.push('\n');
rendered
}
pub fn cli_handle_version_or_continue(
raw_args: &[String],
name: &str,
version: &str,
config: &VersionConfig,
) -> Result<Option<String>, Value> {
let parsed = parse_version_request(raw_args, config);
if !parsed.version_requested {
return Ok(None);
}
if let Some(error) = parsed.output_error {
return Err(build_cli_error(
&error,
Some("valid version output formats: json, yaml, plain"),
));
}
let format = if config.allow_output_format {
parsed.output_format.or(config.default_output)
} else {
config.default_output
};
Ok(Some(cli_render_version(name, version, format)))
}
struct ParsedVersionRequest {
version_requested: bool,
output_format: Option<OutputFormat>,
output_error: Option<String>,
}
fn parse_version_request(raw_args: &[String], config: &VersionConfig) -> ParsedVersionRequest {
let args = raw_args.get(1..).unwrap_or(&[]);
let mut version_requested = false;
let mut output_format = None;
let mut output_error = None;
let output_flag = config.output_flag.map(normalize_long_flag);
let mut i = 0usize;
while i < args.len() {
let arg = args[i].as_str();
if arg == "--" {
break;
}
let (flag_name, inline_value) = split_flag(arg);
if matches!(arg, "--version" | "-V") {
version_requested = true;
i += 1;
continue;
}
if config.allow_output_format
&& version_output_flag_matches(flag_name, output_flag, config.output_short)
{
let value = inline_value.or_else(|| {
args.get(i + 1)
.map(String::as_str)
.filter(|next| !next.starts_with('-'))
});
if let Some(value) = value {
match cli_parse_output(value) {
Ok(format) => output_format = Some(format),
Err(err) => output_error = Some(err),
}
} else {
output_error = Some(format!(
"missing value for --{}: expected json, yaml, or plain",
output_flag.unwrap_or("output")
));
}
i += if inline_value.is_some() || value.is_none() {
1
} else {
2
};
continue;
}
i += 1;
}
ParsedVersionRequest {
version_requested,
output_format,
output_error,
}
}
fn version_output_flag_matches(
flag_name: Option<&str>,
output_flag: Option<&str>,
output_short: Option<char>,
) -> bool {
let Some(seen) = flag_name else {
return false;
};
output_flag.is_some_and(|expected| seen == expected)
|| output_short.is_some_and(|short| {
let mut chars = seen.chars();
chars.next().is_some_and(|seen_short| seen_short == short) && chars.next().is_none()
})
}
pub fn build_cli_error(message: &str, hint: Option<&str>) -> Value {
let mut obj = serde_json::Map::new();
obj.insert("code".to_string(), Value::String("error".to_string()));
obj.insert("error".to_string(), Value::String(message.to_string()));
if let Some(h) = hint {
obj.insert("hint".to_string(), Value::String(h.to_string()));
}
Value::Object(obj)
}
#[cfg(feature = "cli-help")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HelpScope {
OneLevel,
Recursive,
}
#[cfg(feature = "cli-help")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HelpFormat {
Plain,
Markdown,
Json,
Yaml,
}
#[cfg(feature = "cli-help")]
impl HelpFormat {
fn parse(s: &str) -> Option<Self> {
match s {
"plain" => Some(Self::Plain),
"markdown" => Some(Self::Markdown),
"json" => Some(Self::Json),
"yaml" => Some(Self::Yaml),
_ => None,
}
}
}
#[cfg(feature = "cli-help")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HelpOptions {
pub scope: HelpScope,
pub format: HelpFormat,
}
#[cfg(feature = "cli-help")]
impl HelpOptions {
pub const fn one_level_plain() -> Self {
Self {
scope: HelpScope::OneLevel,
format: HelpFormat::Plain,
}
}
pub const fn recursive_plain() -> Self {
Self {
scope: HelpScope::Recursive,
format: HelpFormat::Plain,
}
}
}
#[cfg(feature = "cli-help")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HelpConfig {
pub default_scope: HelpScope,
pub default_format: HelpFormat,
pub recursive_flag: Option<&'static str>,
pub output_flag: Option<&'static str>,
pub allow_output_format: bool,
}
#[cfg(feature = "cli-help")]
impl HelpConfig {
pub const fn new(default_scope: HelpScope, default_format: HelpFormat) -> Self {
Self {
default_scope,
default_format,
recursive_flag: None,
output_flag: None,
allow_output_format: false,
}
}
pub const fn human_cli_default() -> Self {
Self {
default_scope: HelpScope::OneLevel,
default_format: HelpFormat::Plain,
recursive_flag: None,
output_flag: Some("--output"),
allow_output_format: true,
}
}
pub const fn agent_cli_default() -> Self {
Self {
default_scope: HelpScope::Recursive,
default_format: HelpFormat::Plain,
recursive_flag: None,
output_flag: Some("--output"),
allow_output_format: true,
}
}
pub const fn with_default_scope(mut self, scope: HelpScope) -> Self {
self.default_scope = scope;
self
}
pub const fn with_default_format(mut self, format: HelpFormat) -> Self {
self.default_format = format;
self
}
pub const fn with_recursive_flag(mut self, flag: Option<&'static str>) -> Self {
self.recursive_flag = flag;
self
}
pub const fn with_output_flag(mut self, flag: Option<&'static str>) -> Self {
self.output_flag = flag;
self
}
pub const fn with_output_format_override(mut self, enabled: bool) -> Self {
self.allow_output_format = enabled;
self
}
}
#[cfg(feature = "cli-help")]
pub fn cli_render_help_with_options(
cmd: &clap::Command,
subcommand_path: &[&str],
options: &HelpOptions,
) -> String {
let target = walk_to_subcommand(cmd, subcommand_path);
let mut rendered = match options.format {
HelpFormat::Plain => match options.scope {
HelpScope::OneLevel => render_help_one_level_plain(target),
HelpScope::Recursive => {
let mut buf = String::new();
render_help_recursive_plain(target, &[], &mut buf);
buf
}
},
HelpFormat::Markdown => render_help_markdown(cmd, subcommand_path, options.scope),
HelpFormat::Json => {
serialize_json_output(&build_help_schema(cmd, subcommand_path, options.scope))
}
HelpFormat::Yaml => output_yaml_with_options(
&build_help_schema(cmd, subcommand_path, options.scope),
&OutputOptions {
redaction: RedactionOptions {
policy: Some(RedactionPolicy::RedactionNone),
secret_names: Vec::new(),
},
style: OutputStyle::Raw,
},
),
};
while rendered.ends_with('\n') {
rendered.pop();
}
rendered.push('\n');
rendered
}
#[cfg(feature = "cli-help")]
pub fn cli_render_help(cmd: &clap::Command, subcommand_path: &[&str]) -> String {
cli_render_help_with_options(cmd, subcommand_path, &HelpOptions::recursive_plain())
}
#[cfg(feature = "cli-help-markdown")]
pub fn cli_render_help_markdown(cmd: &clap::Command, subcommand_path: &[&str]) -> String {
cli_render_help_with_options(
cmd,
subcommand_path,
&HelpOptions {
scope: HelpScope::Recursive,
format: HelpFormat::Markdown,
},
)
}
#[cfg(feature = "cli-help")]
pub fn cli_handle_help_or_continue(
raw_args: &[String],
cmd: &clap::Command,
config: &HelpConfig,
) -> Result<Option<String>, Value> {
let parsed = parse_help_request(raw_args, cmd, config);
if !parsed.help_requested {
return Ok(None);
}
if let Some(error) = parsed.output_error {
return Err(build_cli_error(
&error,
Some("valid help output formats: plain, markdown, json, yaml"),
));
}
let (scope, format) = resolve_help_options(&parsed, config);
let path: Vec<&str> = parsed.subcommand_path.iter().map(String::as_str).collect();
Ok(Some(cli_render_help_with_options(
cmd,
&path,
&HelpOptions { scope, format },
)))
}
#[cfg(feature = "cli-help")]
fn resolve_help_options(
parsed: &ParsedHelpRequest,
config: &HelpConfig,
) -> (HelpScope, HelpFormat) {
let scope = if parsed.recursive_requested {
HelpScope::Recursive
} else {
config.default_scope
};
let format = if config.allow_output_format {
parsed.output_format.unwrap_or(config.default_format)
} else {
config.default_format
};
(scope, format)
}
#[cfg(feature = "cli-help")]
fn walk_to_subcommand<'a>(cmd: &'a clap::Command, path: &[&str]) -> &'a clap::Command {
let mut current = cmd;
for name in path {
current = current.find_subcommand(name).unwrap_or(current);
}
current
}
#[cfg(feature = "cli-help")]
fn walk_to_subcommand_with_names<'a>(
cmd: &'a clap::Command,
path: &[&str],
) -> (&'a clap::Command, Vec<String>) {
let mut current = cmd;
let mut names = vec![cmd.get_name().to_string()];
for name in path {
if let Some(next) = current.find_subcommand(name) {
current = next;
names.push(next.get_name().to_string());
} else {
break;
}
}
(current, names)
}
#[cfg(feature = "cli-help")]
fn render_help_one_level_plain(cmd: &clap::Command) -> String {
enriched_help_command(cmd).render_long_help().to_string()
}
#[cfg(feature = "cli-help")]
fn enriched_help_command(cmd: &clap::Command) -> clap::Command {
let cmd = cmd.clone();
let description = if visible_subcommands(&cmd).next().is_some() {
HELP_FLAG_WITH_SUBCOMMANDS
} else {
HELP_FLAG_LEAF
};
cmd.disable_help_flag(true).arg(
clap::Arg::new("help")
.short('h')
.long("help")
.help(description)
.long_help(description)
.action(clap::ArgAction::Help),
)
}
#[cfg(feature = "cli-help")]
const HELP_FLAG_WITH_SUBCOMMANDS: &str =
"Print help. Add --recursive to expand every nested subcommand; \
add --output json|yaml|markdown to render this help in another format.";
#[cfg(feature = "cli-help")]
const HELP_FLAG_LEAF: &str =
"Print help. Add --output json|yaml|markdown to render this help in another format.";
#[cfg(feature = "cli-help")]
fn render_help_recursive_plain(cmd: &clap::Command, parent_path: &[&str], buf: &mut String) {
use std::fmt::Write;
let mut cmd_path = parent_path.to_vec();
cmd_path.push(cmd.get_name());
let path_str = cmd_path.join(" ");
if !buf.is_empty() {
let _ = writeln!(buf);
let _ = writeln!(buf, "{}", "═".repeat(60));
}
if let Some(about) = cmd.get_about() {
let _ = writeln!(buf, "{path_str} — {about}");
} else {
let _ = writeln!(buf, "{path_str}");
}
let _ = writeln!(buf);
let is_target = parent_path.is_empty();
let styled = if is_target {
enriched_help_command(cmd).render_long_help()
} else {
cmd.clone().render_long_help()
};
let help_text = styled.to_string();
let _ = write!(buf, "{help_text}");
for sub in cmd.get_subcommands() {
if sub.get_name() == "help" || sub.is_hide_set() {
continue; }
render_help_recursive_plain(sub, &cmd_path, buf);
}
}
#[cfg(feature = "cli-help")]
fn render_help_markdown(cmd: &clap::Command, subcommand_path: &[&str], scope: HelpScope) -> String {
let (target, names) = walk_to_subcommand_with_names(cmd, subcommand_path);
let mut buf = String::new();
render_markdown_command(target, &names, &mut buf, 1, true);
if matches!(scope, HelpScope::Recursive) {
render_markdown_descendants(target, &names, &mut buf, 2);
}
buf
}
#[cfg(feature = "cli-help")]
fn render_markdown_descendants(
cmd: &clap::Command,
parent_names: &[String],
buf: &mut String,
level: usize,
) {
for sub in cmd.get_subcommands() {
if sub.get_name() == "help" || sub.is_hide_set() {
continue;
}
let mut names = parent_names.to_vec();
names.push(sub.get_name().to_string());
render_markdown_command(sub, &names, buf, level, false);
render_markdown_descendants(sub, &names, buf, level.saturating_add(1));
}
}
#[cfg(feature = "cli-help")]
fn render_markdown_command(
cmd: &clap::Command,
names: &[String],
buf: &mut String,
level: usize,
enrich: bool,
) {
use std::fmt::Write;
if !buf.is_empty() {
let _ = writeln!(buf);
}
let heading_level = "#".repeat(level.max(1));
let path = names.join(" ");
if let Some(about) = cmd.get_about() {
let _ = writeln!(buf, "{heading_level} {path} - {about}");
} else {
let _ = writeln!(buf, "{heading_level} {path}");
}
if let Some(long_about) = cmd.get_long_about() {
let _ = writeln!(buf);
let _ = writeln!(buf, "{long_about}");
}
let _ = writeln!(buf);
let _ = writeln!(buf, "```text");
let help = if enrich {
enriched_help_command(cmd).render_long_help()
} else {
cmd.clone().render_long_help()
};
write_trimmed_help(buf, &help.to_string());
if !buf.ends_with('\n') {
let _ = writeln!(buf);
}
let _ = writeln!(buf, "```");
}
#[cfg(feature = "cli-help")]
fn write_trimmed_help(buf: &mut String, help: &str) {
use std::fmt::Write;
for line in help.lines() {
let _ = writeln!(buf, "{}", line.trim_end());
}
}
#[cfg(feature = "cli-help")]
struct ParsedHelpRequest {
help_requested: bool,
recursive_requested: bool,
output_format: Option<HelpFormat>,
output_error: Option<String>,
subcommand_path: Vec<String>,
}
#[cfg(feature = "cli-help")]
fn parse_help_request(
raw_args: &[String],
cmd: &clap::Command,
config: &HelpConfig,
) -> ParsedHelpRequest {
let args = match raw_args.first() {
Some(first) if first.starts_with('-') || cmd.find_subcommand(first).is_some() => raw_args,
_ => raw_args.get(1..).unwrap_or(&[]),
};
let mut help_requested = false;
let mut recursive_requested = false;
let mut output_format = None;
let mut output_error = None;
let mut subcommand_path = Vec::new();
let mut current = cmd;
let output_flag = config.output_flag.map(normalize_long_flag);
let recursive_flag = config.recursive_flag.map(normalize_long_flag);
let mut i = 0usize;
while i < args.len() {
let arg = args[i].as_str();
if arg == "--" {
break;
}
let (flag_name, inline_value) = split_flag(arg);
if matches!(arg, "--help" | "-h") {
help_requested = true;
i += 1;
continue;
}
if arg == "--recursive"
|| flag_name
.zip(recursive_flag)
.is_some_and(|(seen, expected)| seen == expected)
{
recursive_requested = true;
i += 1;
continue;
}
if config.allow_output_format
&& flag_name
.zip(output_flag)
.is_some_and(|(seen, expected)| seen == expected)
{
let value = inline_value.or_else(|| {
args.get(i + 1)
.map(String::as_str)
.filter(|next| !next.starts_with('-'))
});
if let Some(value) = value {
match HelpFormat::parse(value) {
Some(format) => output_format = Some(format),
None => {
output_error = Some(format!(
"invalid --{} format '{}': expected plain, json, yaml, or markdown",
output_flag.unwrap_or("output"),
value
));
}
}
} else {
output_error = Some(format!(
"missing value for --{}: expected plain, json, yaml, or markdown",
output_flag.unwrap_or("output")
));
}
i += if inline_value.is_some() || value.is_none() {
1
} else {
2
};
continue;
}
if arg.starts_with('-') {
i += if inline_value.is_none() && flag_takes_value(current, arg) {
2
} else {
1
};
continue;
}
if let Some(sub) = current.find_subcommand(arg) {
if sub.get_name() != "help" && !sub.is_hide_set() {
subcommand_path.push(sub.get_name().to_string());
current = sub;
}
}
i += 1;
}
ParsedHelpRequest {
help_requested,
recursive_requested,
output_format,
output_error,
subcommand_path,
}
}
fn normalize_long_flag(flag: &str) -> &str {
flag.trim_start_matches('-')
}
fn split_flag(arg: &str) -> (Option<&str>, Option<&str>) {
if let Some(stripped) = arg.strip_prefix("--") {
if let Some((name, value)) = stripped.split_once('=') {
(Some(name), Some(value))
} else {
(Some(stripped), None)
}
} else if let Some(stripped) = arg.strip_prefix('-') {
(Some(stripped), None)
} else {
(None, None)
}
}
#[cfg(feature = "cli-help")]
fn flag_takes_value(cmd: &clap::Command, raw_flag: &str) -> bool {
let Some(flag) = raw_flag.strip_prefix('-') else {
return false;
};
let name = flag.trim_start_matches('-');
cmd.get_arguments().any(|arg| {
let long_matches = arg.get_long().is_some_and(|long| long == name);
let short_matches =
name.len() == 1 && arg.get_short().is_some_and(|short| name.starts_with(short));
(long_matches || short_matches)
&& matches!(
arg.get_action(),
clap::ArgAction::Set | clap::ArgAction::Append
)
})
}
#[cfg(feature = "cli-help")]
fn build_help_schema(cmd: &clap::Command, subcommand_path: &[&str], scope: HelpScope) -> Value {
let (target, names) = walk_to_subcommand_with_names(cmd, subcommand_path);
let mut schema = command_schema(target, &names, matches!(scope, HelpScope::Recursive), true);
if let Value::Object(map) = &mut schema {
map.insert("code".to_string(), Value::String("help".to_string()));
map.insert(
"scope".to_string(),
Value::String(help_scope_tag(scope).to_string()),
);
}
schema
}
#[cfg(feature = "cli-help")]
fn help_scope_tag(scope: HelpScope) -> &'static str {
match scope {
HelpScope::OneLevel => "one_level",
HelpScope::Recursive => "recursive",
}
}
#[cfg(feature = "cli-help")]
fn command_schema(cmd: &clap::Command, names: &[String], recursive: bool, enrich: bool) -> Value {
let subcommands: Vec<Value> = visible_subcommands(cmd)
.map(|sub| {
let mut child_names = names.to_vec();
child_names.push(sub.get_name().to_string());
if recursive {
command_schema(sub, &child_names, true, false)
} else {
command_summary_schema(sub, &child_names)
}
})
.collect();
serde_json::json!({
"name": cmd.get_name(),
"command_path": names.join(" "),
"path": names,
"about": styled_to_value(cmd.get_about()),
"long_about": styled_to_value(cmd.get_long_about()),
"usage": cmd.clone().render_usage().to_string(),
"arguments": command_arguments_schema(cmd, enrich),
"subcommands": subcommands,
})
}
#[cfg(feature = "cli-help")]
fn command_summary_schema(cmd: &clap::Command, names: &[String]) -> Value {
serde_json::json!({
"name": cmd.get_name(),
"command_path": names.join(" "),
"path": names,
"about": styled_to_value(cmd.get_about()),
"long_about": styled_to_value(cmd.get_long_about()),
"usage": Value::Null,
"arguments": [],
"subcommands": [],
})
}
#[cfg(feature = "cli-help")]
fn visible_subcommands(cmd: &clap::Command) -> impl Iterator<Item = &clap::Command> {
cmd.get_subcommands()
.filter(|sub| sub.get_name() != "help" && !sub.is_hide_set())
}
#[cfg(feature = "cli-help")]
fn command_arguments_schema(cmd: &clap::Command, enrich: bool) -> Vec<Value> {
let owned = enrich.then(|| enriched_help_command(cmd));
let source = owned.as_ref().unwrap_or(cmd);
source
.get_arguments()
.filter(|arg| !arg.is_hide_set())
.map(argument_schema)
.collect()
}
#[cfg(feature = "cli-help")]
fn argument_schema(arg: &clap::Arg) -> Value {
let value_names: Vec<String> = arg
.get_value_names()
.map(|names| names.iter().map(ToString::to_string).collect())
.unwrap_or_default();
let default_values: Vec<String> = arg
.get_default_values()
.iter()
.map(|value| value.to_string_lossy().to_string())
.collect();
serde_json::json!({
"id": arg.get_id().to_string(),
"kind": if arg.get_long().is_some() || arg.get_short().is_some() { "option" } else { "argument" },
"long": arg.get_long(),
"short": arg.get_short().map(|c| c.to_string()),
"help": styled_to_value(arg.get_help()),
"long_help": styled_to_value(arg.get_long_help()),
"required": arg.is_required_set(),
"action": format!("{:?}", arg.get_action()),
"value_names": value_names,
"default_values": default_values,
})
}
#[cfg(feature = "cli-help")]
fn styled_to_value(value: Option<&clap::builder::StyledStr>) -> Value {
value.map_or(Value::Null, |s| Value::String(s.to_string()))
}
#[derive(Default)]
struct RedactionContext {
secret_names: HashSet<String>,
}
impl RedactionContext {
fn from_options(redaction_options: &RedactionOptions) -> Self {
let secret_names = redaction_options.secret_names.iter().cloned().collect();
Self { secret_names }
}
fn is_secret_key(&self, key: &str) -> bool {
key_has_secret_suffix(key) || self.secret_names.contains(key)
}
}
fn key_has_secret_suffix(key: &str) -> bool {
key.ends_with("_secret") || key.ends_with("_SECRET")
}
fn key_has_url_suffix(key: &str) -> bool {
key.ends_with("_url") || key.ends_with("_URL")
}
const MAX_DEPTH: usize = 256;
fn redact_secrets(value: &mut Value) {
let context = RedactionContext::default();
redact_secrets_with_context(value, &context);
}
fn redact_secrets_with_context(value: &mut Value, context: &RedactionContext) {
redact_secrets_with_context_depth(value, context, 0);
}
fn redact_secrets_with_context_depth(value: &mut Value, context: &RedactionContext, depth: usize) {
if depth >= MAX_DEPTH {
*value = Value::String("***".into());
return;
}
match value {
Value::Object(map) => {
let keys: Vec<String> = map.keys().cloned().collect();
for key in keys {
if context.is_secret_key(&key) {
map.insert(key, Value::String("***".into()));
} else if key_has_url_suffix(&key) {
if let Some(Value::String(s)) = map.get_mut(&key) {
*s = redact_url_field_value(s, context);
} else if let Some(v) = map.get_mut(&key) {
redact_secrets_with_context_depth(v, context, depth + 1);
}
} else if let Some(v) = map.get_mut(&key) {
redact_secrets_with_context_depth(v, context, depth + 1);
}
}
}
Value::Array(arr) => {
for v in arr {
redact_secrets_with_context_depth(v, context, depth + 1);
}
}
_ => {}
}
}
fn redact_url_in_str(s: &str, context: &RedactionContext) -> Option<String> {
if !s.contains("://") || !is_single_url(s) {
return None;
}
let scheme_sep = s.find("://")?;
let scheme = &s[..scheme_sep];
let rest = &s[scheme_sep + 3..];
let auth_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
let authority = &rest[..auth_end];
let remainder = &rest[auth_end..];
let new_authority = redact_userinfo_password(authority);
let new_remainder = match remainder.find('?') {
Some(q) => {
let (path, q_onwards) = remainder.split_at(q);
let query_body = &q_onwards[1..];
let (query, fragment) = match query_body.find('#') {
Some(h) => (&query_body[..h], &query_body[h..]),
None => (query_body, ""),
};
format!("{path}?{}{fragment}", redact_query(query, context))
}
None => remainder.to_string(),
};
Some(format!("{scheme}://{new_authority}{new_remainder}"))
}
fn redact_url_field_value(s: &str, context: &RedactionContext) -> String {
if let Some(redacted) = redact_url_in_str(s, context) {
return redacted;
}
let trimmed = s.trim();
if trimmed != s {
if let Some(redacted) = redact_url_in_str(trimmed, context) {
return redacted;
}
}
if s.chars().any(char::is_whitespace) || s.contains('@') {
return "***".to_string();
}
s.to_string()
}
fn redact_userinfo_password(authority: &str) -> String {
let Some(at) = authority.rfind('@') else {
return authority.to_string();
};
let userinfo = &authority[..at];
match userinfo.find(':') {
Some(colon) => format!("{}:***{}", &authority[..colon], &authority[at..]),
None => authority.to_string(),
}
}
fn redact_query(query: &str, context: &RedactionContext) -> String {
query
.split('&')
.map(|segment| {
let Some(eq) = segment.find('=') else {
return segment.to_string();
};
let raw_key = &segment[..eq];
let name = url::form_urlencoded::parse(segment.as_bytes())
.next()
.map(|(k, _)| k.into_owned())
.unwrap_or_default();
if context.is_secret_key(&name) {
format!("{raw_key}=***")
} else {
segment.to_string()
}
})
.collect::<Vec<_>>()
.join("&")
}
fn is_single_url(s: &str) -> bool {
if s.bytes().any(|b| b.is_ascii_whitespace()) {
return false;
}
let bytes = s.as_bytes();
if !bytes.first().is_some_and(|b| b.is_ascii_alphabetic()) {
return false;
}
let mut i = 1;
while i < bytes.len() {
let c = bytes[i];
if c.is_ascii_alphanumeric() || matches!(c, b'+' | b'-' | b'.') {
i += 1;
} else {
break;
}
}
s[i..].starts_with("://")
}
fn apply_redaction_policy(value: &mut Value, redaction_policy: RedactionPolicy) {
let context = RedactionContext::default();
apply_redaction_policy_with_context(value, Some(redaction_policy), &context);
}
fn apply_redaction_options(value: &mut Value, redaction_options: &RedactionOptions) {
let context = RedactionContext::from_options(redaction_options);
apply_redaction_policy_with_context(value, redaction_options.policy, &context);
}
fn apply_redaction_policy_with_context(
value: &mut Value,
redaction_policy: Option<RedactionPolicy>,
context: &RedactionContext,
) {
match redaction_policy {
Some(RedactionPolicy::RedactionTraceOnly) => {
if let Value::Object(map) = value {
if let Some(trace) = map.get_mut("trace") {
redact_secrets_with_context(trace, context);
}
}
}
Some(RedactionPolicy::RedactionNone) => {}
None => redact_secrets_with_context(value, context),
}
}
fn strip_suffix_ci(key: &str, suffix_lower: &str) -> Option<String> {
if let Some(s) = key.strip_suffix(suffix_lower) {
return Some(s.to_string());
}
let suffix_upper: String = suffix_lower
.chars()
.map(|c| c.to_ascii_uppercase())
.collect();
if let Some(s) = key.strip_suffix(&suffix_upper) {
return Some(s.to_string());
}
None
}
fn try_strip_generic_cents(key: &str) -> Option<(String, String)> {
let code = extract_currency_code(key)?;
let suffix_len = code.len() + "_cents".len() + 1; let stripped = &key[..key.len() - suffix_len];
if stripped.is_empty() {
return None;
}
Some((stripped.to_string(), code.to_string()))
}
fn as_int(value: &Value) -> Option<i64> {
if let Some(i) = value.as_i64() {
return Some(i);
}
let f = value.as_f64()?;
if f.is_finite() && f.fract() == 0.0 && (i64::MIN as f64..=i64::MAX as f64).contains(&f) {
return Some(f as i64);
}
None
}
fn as_uint(value: &Value) -> Option<u64> {
if let Some(u) = value.as_u64() {
return Some(u);
}
let f = value.as_f64()?;
if f.is_finite() && f.fract() == 0.0 && (0.0..=u64::MAX as f64).contains(&f) {
return Some(f as u64);
}
None
}
fn try_process_field(key: &str, value: &Value) -> Option<(String, String)> {
if let Some(stripped) = strip_suffix_ci(key, "_epoch_ms") {
return as_int(value)
.and_then(|ms| format_rfc3339_ms(ms).map(|formatted| (stripped, formatted)));
}
if let Some(stripped) = strip_suffix_ci(key, "_epoch_s") {
return as_int(value)
.and_then(|s| s.checked_mul(1000))
.and_then(|ms| format_rfc3339_ms(ms).map(|formatted| (stripped, formatted)));
}
if let Some(stripped) = strip_suffix_ci(key, "_epoch_ns") {
return as_int(value).and_then(|ns| {
format_rfc3339_ms(ns.div_euclid(1_000_000)).map(|formatted| (stripped, formatted))
});
}
if let Some(stripped) = strip_suffix_ci(key, "_usd_cents") {
return as_uint(value).map(|n| (stripped, format!("${}.{:02}", n / 100, n % 100)));
}
if let Some(stripped) = strip_suffix_ci(key, "_eur_cents") {
return as_uint(value).map(|n| (stripped, format!("€{}.{:02}", n / 100, n % 100)));
}
if let Some((stripped, code)) = try_strip_generic_cents(key) {
return as_uint(value).map(|n| {
(
stripped,
format!("{}.{:02} {}", n / 100, n % 100, code.to_uppercase()),
)
});
}
if let Some(stripped) = strip_suffix_ci(key, "_rfc3339") {
return value.as_str().map(|s| (stripped, s.to_string()));
}
if let Some(stripped) = strip_suffix_ci(key, "_minutes") {
return value
.is_number()
.then(|| (stripped, format!("{} minutes", number_str(value))));
}
if let Some(stripped) = strip_suffix_ci(key, "_hours") {
return value
.is_number()
.then(|| (stripped, format!("{} hours", number_str(value))));
}
if let Some(stripped) = strip_suffix_ci(key, "_days") {
return value
.is_number()
.then(|| (stripped, format!("{} days", number_str(value))));
}
if let Some(stripped) = strip_suffix_ci(key, "_msats") {
return value
.is_number()
.then(|| (stripped, format!("{}msats", number_str(value))));
}
if let Some(stripped) = strip_suffix_ci(key, "_sats") {
return value
.is_number()
.then(|| (stripped, format!("{}sats", number_str(value))));
}
if let Some(stripped) = strip_suffix_ci(key, "_bytes") {
return as_int(value).map(|n| (stripped, format_bytes_human(n)));
}
if let Some(stripped) = strip_suffix_ci(key, "_percent") {
return value
.is_number()
.then(|| (stripped, format!("{}%", number_str(value))));
}
if let Some(stripped) = strip_suffix_ci(key, "_btc") {
return value
.is_number()
.then(|| (stripped, format!("{} BTC", number_str(value))));
}
if let Some(stripped) = strip_suffix_ci(key, "_jpy") {
return as_uint(value).map(|n| (stripped, format!("¥{}", format_with_commas(n))));
}
if let Some(stripped) = strip_suffix_ci(key, "_ns") {
return value
.is_number()
.then(|| (stripped, format!("{}ns", number_str(value))));
}
if let Some(stripped) = strip_suffix_ci(key, "_us") {
return value
.is_number()
.then(|| (stripped, format!("{}μs", number_str(value))));
}
if let Some(stripped) = strip_suffix_ci(key, "_ms") {
return format_ms_value(value).map(|v| (stripped, v));
}
if let Some(stripped) = strip_suffix_ci(key, "_s") {
return value
.is_number()
.then(|| (stripped, format!("{}s", number_str(value))));
}
None
}
fn process_object_fields<'a>(
map: &'a serde_json::Map<String, Value>,
) -> Vec<(String, &'a Value, Option<String>)> {
let mut entries: Vec<(String, &'a str, &'a Value, Option<String>)> = Vec::new();
for (key, value) in map {
if let Some(stripped) = strip_suffix_ci(key, "_secret") {
entries.push((stripped, key.as_str(), value, None));
continue;
}
match try_process_field(key, value) {
Some((stripped, formatted)) => {
entries.push((stripped, key.as_str(), value, Some(formatted)));
}
None => {
entries.push((key.clone(), key.as_str(), value, None));
}
}
}
let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for (stripped, _, _, _) in &entries {
*counts.entry(stripped.clone()).or_insert(0) += 1;
}
let mut result: Vec<(String, &'a Value, Option<String>)> = entries
.into_iter()
.map(|(stripped, original, value, formatted)| {
if counts.get(&stripped).copied().unwrap_or(0) > 1 && original != stripped.as_str() {
(original.to_string(), value, None)
} else {
(stripped, value, formatted)
}
})
.collect();
result.sort_by(|(a, _, _), (b, _, _)| a.encode_utf16().cmp(b.encode_utf16()));
result
}
fn number_str(value: &Value) -> String {
match value {
Value::Number(n) => format_number(n),
_ => String::new(),
}
}
fn format_number(n: &serde_json::Number) -> String {
if n.is_f64() {
if let Some(f) = n.as_f64() {
if f.is_finite() && f.fract() == 0.0 && f.abs() < 1e21 {
return format!("{f:.0}");
}
}
}
normalize_exponent(&n.to_string())
}
fn normalize_exponent(s: &str) -> String {
let Some(e) = s.find(['e', 'E']) else {
return s.to_string();
};
let mantissa = &s[..e];
let mut exp = &s[e + 1..];
let mut sign = "";
if exp.starts_with(['+', '-']) {
sign = &exp[..1];
exp = &exp[1..];
}
let exp = exp.trim_start_matches('0');
let exp = if exp.is_empty() { "0" } else { exp };
format!("{mantissa}e{sign}{exp}")
}
fn format_ms_as_seconds(ms: f64) -> String {
let formatted = format!("{:.3}", ms / 1000.0);
let trimmed = formatted.trim_end_matches('0');
if trimmed.ends_with('.') {
format!("{}0s", trimmed)
} else {
format!("{}s", trimmed)
}
}
fn format_ms_value(value: &Value) -> Option<String> {
let n = value.as_f64()?;
if n.abs() >= 1000.0 {
Some(format_ms_as_seconds(n))
} else if let Some(i) = value.as_i64() {
Some(format!("{}ms", i))
} else {
Some(format!("{}ms", number_str(value)))
}
}
const MIN_RFC3339_MS: i64 = -62135596800000;
const MAX_RFC3339_MS: i64 = 253402300799999;
fn format_rfc3339_ms(ms: i64) -> Option<String> {
use chrono::{DateTime, Utc};
if !(MIN_RFC3339_MS..=MAX_RFC3339_MS).contains(&ms) {
return None;
}
let secs = ms.div_euclid(1000);
let nanos = (ms.rem_euclid(1000) * 1_000_000) as u32;
DateTime::from_timestamp(secs, nanos).map(|dt| {
dt.with_timezone(&Utc)
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
})
}
fn format_bytes_human(bytes: i64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = KB * 1024.0;
const GB: f64 = MB * 1024.0;
const TB: f64 = GB * 1024.0;
let sign = if bytes < 0 { "-" } else { "" };
let b = (bytes as f64).abs();
if b >= TB {
format!("{sign}{:.1}TB", b / TB)
} else if b >= GB {
format!("{sign}{:.1}GB", b / GB)
} else if b >= MB {
format!("{sign}{:.1}MB", b / MB)
} else if b >= KB {
format!("{sign}{:.1}KB", b / KB)
} else {
format!("{bytes}B")
}
}
fn format_with_commas(n: u64) -> String {
let s = n.to_string();
let mut result = String::with_capacity(s.len() + s.len() / 3);
for (i, c) in s.chars().enumerate() {
if i > 0 && (s.len() - i).is_multiple_of(3) {
result.push(',');
}
result.push(c);
}
result
}
fn extract_currency_code(key: &str) -> Option<&str> {
let without_cents = key
.strip_suffix("_cents")
.or_else(|| key.strip_suffix("_CENTS"))?;
let last_underscore = without_cents.rfind('_')?;
let code = &without_cents[last_underscore + 1..];
if code.is_empty()
|| !(3..=4).contains(&code.len())
|| !code.bytes().all(|b| b.is_ascii_alphabetic())
{
return None;
}
Some(code)
}
fn render_yaml_processed(value: &Value, indent: usize, lines: &mut Vec<String>) {
let prefix = " ".repeat(indent);
match value {
Value::Object(map) => {
let processed = process_object_fields(map);
for (display_key, v, formatted) in processed {
if let Some(fv) = formatted {
lines.push(format!(
"{}{}: \"{}\"",
prefix,
yaml_key(&display_key),
escape_yaml_str(&fv)
));
} else {
match v {
Value::Object(inner) if !inner.is_empty() => {
lines.push(format!("{}{}:", prefix, yaml_key(&display_key)));
render_yaml_processed(v, indent + 1, lines);
}
Value::Object(_) => {
lines.push(format!("{}{}: {{}}", prefix, yaml_key(&display_key)));
}
Value::Array(arr) => {
if arr.is_empty() {
lines.push(format!("{}{}: []", prefix, yaml_key(&display_key)));
} else {
lines.push(format!("{}{}:", prefix, yaml_key(&display_key)));
for item in arr {
if item.is_object() {
lines.push(format!("{} -", prefix));
render_yaml_processed(item, indent + 2, lines);
} else {
lines.push(format!("{} - {}", prefix, yaml_scalar(item)));
}
}
}
}
_ => {
lines.push(format!(
"{}{}: {}",
prefix,
yaml_key(&display_key),
yaml_scalar(v)
));
}
}
}
}
}
_ => {
lines.push(format!("{}{}", prefix, yaml_scalar(value)));
}
}
}
fn render_yaml_raw(value: &Value, indent: usize, lines: &mut Vec<String>) {
let prefix = " ".repeat(indent);
match value {
Value::Object(map) => {
for key in sorted_value_keys(map) {
render_yaml_field_raw(&prefix, &key, &map[&key], indent, lines);
}
}
Value::Array(arr) => {
render_yaml_array_raw(arr, indent, lines);
}
_ => {
lines.push(format!("{}{}", prefix, yaml_scalar(value)));
}
}
}
fn render_yaml_field_raw(
prefix: &str,
key: &str,
value: &Value,
indent: usize,
lines: &mut Vec<String>,
) {
match value {
Value::Object(inner) if !inner.is_empty() => {
lines.push(format!("{}{}:", prefix, yaml_key(key)));
render_yaml_raw(value, indent + 1, lines);
}
Value::Object(_) => {
lines.push(format!("{}{}: {{}}", prefix, yaml_key(key)));
}
Value::Array(arr) => {
if arr.is_empty() {
lines.push(format!("{}{}: []", prefix, yaml_key(key)));
} else {
lines.push(format!("{}{}:", prefix, yaml_key(key)));
render_yaml_array_raw(arr, indent + 1, lines);
}
}
_ => {
lines.push(format!(
"{}{}: {}",
prefix,
yaml_key(key),
yaml_scalar(value)
));
}
}
}
fn render_yaml_array_raw(arr: &[Value], indent: usize, lines: &mut Vec<String>) {
let prefix = " ".repeat(indent);
for item in arr {
match item {
Value::Object(inner) if !inner.is_empty() => {
lines.push(format!("{}-", prefix));
render_yaml_raw(item, indent + 1, lines);
}
Value::Array(nested) if !nested.is_empty() => {
lines.push(format!("{}-", prefix));
render_yaml_array_raw(nested, indent + 1, lines);
}
Value::Object(_) => {
lines.push(format!("{}- {{}}", prefix));
}
Value::Array(_) => {
lines.push(format!("{}- []", prefix));
}
_ => {
lines.push(format!("{}- {}", prefix, yaml_scalar(item)));
}
}
}
}
fn escape_yaml_str(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
.replace('\x0c', "\\f")
.replace('\x0b', "\\v")
}
fn yaml_key(key: &str) -> String {
if is_safe_key(key) {
key.to_string()
} else {
format!("\"{}\"", escape_yaml_str(key))
}
}
fn quote_logfmt_key(key: &str) -> String {
if is_safe_key(key) {
key.to_string()
} else {
quote_logfmt_value(key)
}
}
fn is_safe_key(key: &str) -> bool {
!key.is_empty()
&& key
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.'))
}
fn yaml_scalar(value: &Value) -> String {
match value {
Value::String(s) => format!("\"{}\"", escape_yaml_str(s)),
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => format_number(n),
Value::Object(_) | Value::Array(_) => {
format!("\"{}\"", escape_yaml_str(&canonical_json(value)))
}
}
}
fn collect_plain_pairs(value: &Value, prefix: &str, pairs: &mut Vec<(String, String)>) {
if let Value::Object(map) = value {
let processed = process_object_fields(map);
for (display_key, v, formatted) in processed {
let full_key = if prefix.is_empty() {
display_key
} else {
format!("{}.{}", prefix, display_key)
};
if let Some(fv) = formatted {
pairs.push((full_key, fv));
} else {
match v {
Value::Object(_) => collect_plain_pairs(v, &full_key, pairs),
Value::Array(arr) => {
let joined = arr.iter().map(plain_scalar).collect::<Vec<_>>().join(",");
pairs.push((full_key, joined));
}
Value::Null => pairs.push((full_key, String::new())),
_ => pairs.push((full_key, plain_scalar(v))),
}
}
}
}
}
fn collect_plain_pairs_raw(value: &Value, prefix: &str, pairs: &mut Vec<(String, String)>) {
if let Value::Object(map) = value {
for key in sorted_value_keys(map) {
let v = &map[&key];
let full_key = if prefix.is_empty() {
key.clone()
} else {
format!("{}.{}", prefix, key)
};
match v {
Value::Object(_) => collect_plain_pairs_raw(v, &full_key, pairs),
Value::Array(arr) => {
let joined = arr.iter().map(plain_scalar).collect::<Vec<_>>().join(",");
pairs.push((full_key, joined));
}
Value::Null => pairs.push((full_key, String::new())),
_ => pairs.push((full_key, plain_scalar(v))),
}
}
}
}
fn plain_scalar(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => format_number(n),
Value::Object(_) | Value::Array(_) => canonical_json(value),
}
}
fn quote_logfmt_value(value: &str) -> String {
if value.is_empty() {
return String::new();
}
if !value
.chars()
.any(|c| c.is_whitespace() || matches!(c, '=' | '"' | '\\'))
{
return value.to_string();
}
let escaped = value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
.replace('\x0c', "\\f")
.replace('\x0b', "\\v");
format!("\"{}\"", escaped)
}
fn canonical_json(value: &Value) -> String {
serde_json::to_string(&sort_json_value(value))
.unwrap_or_else(|_| "<unsupported:json>".to_string())
}
fn sort_json_value(value: &Value) -> Value {
match value {
Value::Object(map) => {
let mut out = serde_json::Map::new();
for key in sorted_value_keys(map) {
if let Some(v) = map.get(&key) {
out.insert(key, sort_json_value(v));
}
}
Value::Object(out)
}
Value::Array(arr) => Value::Array(arr.iter().map(sort_json_value).collect()),
_ => value.clone(),
}
}
fn sorted_value_keys(map: &serde_json::Map<String, Value>) -> Vec<String> {
let mut keys: Vec<String> = map.keys().cloned().collect();
keys.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16()));
keys
}
#[cfg(test)]
mod tests;