use clap::{CommandFactory, Parser};
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(
name = "noyafmt",
about = "Format YAML files via the noyalib CST formatter",
long_about = "noyafmt — auto-format YAML via the noyalib CST.\n\n\
Reads YAML from FILE arguments (or stdin via --stdin) and\n\
rewrites them through noyalib's lossless CST formatter.\n\
Comments, anchor positions, and document structure are\n\
preserved byte-for-byte; only whitespace and quoting are\n\
normalised.",
version = env!("CARGO_PKG_VERSION"),
after_help = "EXAMPLES:\n \
noyafmt config.yaml # print formatted source to stdout\n \
noyafmt --write config.yaml # rewrite in place\n \
noyafmt --check ci/*.yaml # CI gate\n \
cat foo.yaml | noyafmt --stdin",
)]
pub struct NoyafmtCli {
#[arg(long, conflicts_with = "write")]
pub check: bool,
#[arg(long)]
pub write: bool,
#[arg(long, conflicts_with = "files")]
pub stdin: bool,
#[arg(long, value_name = "N", default_value_t = 2)]
pub indent: usize,
#[arg(value_name = "FILE")]
pub files: Vec<PathBuf>,
}
#[derive(Debug, Parser)]
#[command(
name = "noyavalidate",
about = "Validate YAML syntax and (optionally) a JSON Schema",
long_about = "noyavalidate — check YAML syntax (and optional JSON Schema).\n\n\
Reads one or more YAML documents from a file (or stdin),\n\
reports syntax errors via the miette fancy renderer, and —\n\
when --schema PATH is given — validates each parsed\n\
document against a JSON Schema 2020-12 contract (the\n\
schema may itself be written in YAML or JSON).\n\n\
--fix rewrites the input in-place through the lossless\n\
CST formatter, normalising whitespace and quoting without\n\
changing semantics. When the input is stdin, the\n\
formatted output is written to stdout instead.",
version = env!("CARGO_PKG_VERSION"),
after_help = "EXIT CODES:\n \
0 All documents valid (and fixed if --fix)\n \
1 Parse error or schema violation\n \
2 Usage error\n \
3 I/O error",
)]
pub struct NoyavalidateCli {
#[arg(short = 's', long, value_name = "PATH")]
pub schema: Option<PathBuf>,
#[arg(long)]
pub fix: bool,
#[arg(short, long)]
pub quiet: bool,
#[arg(value_name = "FILE")]
pub file: Option<PathBuf>,
}
#[must_use]
pub fn noyafmt_command() -> clap::Command {
NoyafmtCli::command()
}
#[must_use]
pub fn noyavalidate_command() -> clap::Command {
NoyavalidateCli::command()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn noyafmt_help_flag_renders() {
let r = NoyafmtCli::try_parse_from(["noyafmt", "--help"]);
let err = r.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
}
#[test]
fn noyafmt_version_flag_renders() {
let r = NoyafmtCli::try_parse_from(["noyafmt", "--version"]);
let err = r.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
}
#[test]
fn noyafmt_check_with_files() {
let cli = NoyafmtCli::try_parse_from(["noyafmt", "--check", "a.yaml", "b.yaml"]).unwrap();
assert!(cli.check);
assert!(!cli.write);
assert_eq!(cli.files.len(), 2);
}
#[test]
fn noyafmt_write_with_file() {
let cli = NoyafmtCli::try_parse_from(["noyafmt", "--write", "x.yaml"]).unwrap();
assert!(cli.write);
assert_eq!(cli.files.len(), 1);
}
#[test]
fn noyafmt_stdin_alone() {
let cli = NoyafmtCli::try_parse_from(["noyafmt", "--stdin"]).unwrap();
assert!(cli.stdin);
assert!(cli.files.is_empty());
}
#[test]
fn noyafmt_indent_separate_value() {
let cli = NoyafmtCli::try_parse_from(["noyafmt", "--indent", "4", "--stdin"]).unwrap();
assert_eq!(cli.indent, 4);
}
#[test]
fn noyafmt_indent_eq_value() {
let cli = NoyafmtCli::try_parse_from(["noyafmt", "--indent=8", "--stdin"]).unwrap();
assert_eq!(cli.indent, 8);
}
#[test]
fn noyafmt_indent_default_is_two() {
let cli = NoyafmtCli::try_parse_from(["noyafmt", "--stdin"]).unwrap();
assert_eq!(cli.indent, 2);
}
#[test]
fn noyafmt_indent_non_numeric_errors() {
let r = NoyafmtCli::try_parse_from(["noyafmt", "--indent", "abc", "--stdin"]);
assert!(r.is_err());
}
#[test]
fn noyafmt_unknown_option_errors() {
let r = NoyafmtCli::try_parse_from(["noyafmt", "--frobnicate"]);
assert!(r.is_err());
}
#[test]
fn noyafmt_check_and_write_rejected() {
let r = NoyafmtCli::try_parse_from(["noyafmt", "--check", "--write", "f.yaml"]);
let err = r.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn noyafmt_stdin_with_files_rejected() {
let r = NoyafmtCli::try_parse_from(["noyafmt", "--stdin", "f.yaml"]);
let err = r.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn noyavalidate_help_flag_renders() {
let r = NoyavalidateCli::try_parse_from(["noyavalidate", "--help"]);
assert_eq!(r.unwrap_err().kind(), clap::error::ErrorKind::DisplayHelp);
}
#[test]
fn noyavalidate_schema_short_form() {
let cli =
NoyavalidateCli::try_parse_from(["noyavalidate", "-s", "s.json", "in.yaml"]).unwrap();
assert_eq!(cli.schema.unwrap().to_string_lossy(), "s.json");
assert_eq!(cli.file.unwrap().to_string_lossy(), "in.yaml");
}
#[test]
fn noyavalidate_schema_long_form() {
let cli =
NoyavalidateCli::try_parse_from(["noyavalidate", "--schema=schema.yaml", "x.yaml"])
.unwrap();
assert_eq!(cli.schema.unwrap().to_string_lossy(), "schema.yaml");
}
#[test]
fn noyavalidate_fix_quiet_flags() {
let cli = NoyavalidateCli::try_parse_from(["noyavalidate", "--fix", "--quiet", "in.yaml"])
.unwrap();
assert!(cli.fix);
assert!(cli.quiet);
}
#[test]
fn noyavalidate_no_args_means_stdin() {
let cli = NoyavalidateCli::try_parse_from(["noyavalidate"]).unwrap();
assert!(cli.file.is_none());
}
#[test]
fn commands_render_help_without_panic() {
let mut a = noyafmt_command();
let mut b = noyavalidate_command();
let _ = a.render_help();
let _ = b.render_help();
}
}