use std::path::PathBuf;
use clap::Parser;
use knf::glob::KeyGlobPattern;
use knf::{Format, PathLeaf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum FormatArg {
Json,
Toml,
}
impl From<FormatArg> for Format {
fn from(arg: FormatArg) -> Self {
match arg {
FormatArg::Json => Format::Json,
FormatArg::Toml => Format::Toml,
}
}
}
#[derive(Parser, Debug)]
#[command(
name = "knf",
version,
about = "Merge layered configuration files and print the result",
long_about = "\
Merge layered JSON or TOML files, left to right, and print the result.
Objects merge recursively; arrays, scalars and null replace wholesale.
knf base.toml prod.toml
knf base.json - -f json
knf defaults.toml -c server.port=8080"
)]
pub struct Cli {
#[arg(value_name = "FILE")]
pub files: Vec<PathBuf>,
#[arg(
short = 'g',
long,
value_name = "PATTERN",
conflicts_with = "glob_filename",
long_help = "\
Keep only inputs whose whole path matches a case-sensitive glob. Supports *,
**, ?, [...], {a,b} and leading ! negation. Stdin matches as -.
knf config/**/*.toml -g 'config/**/prod.toml'"
)]
pub glob: Option<knf::glob::GlobPattern>,
#[arg(
short = 'G',
long,
value_name = "PATTERN",
conflicts_with = "glob",
long_help = "\
Like --glob, but match only the filename.
knf -a services/api/prod.toml -G '{defaults,prod}.toml'"
)]
pub glob_filename: Option<knf::glob::GlobPattern>,
#[arg(
short = 'a',
long = "accumulate",
value_name = "TARGET",
value_parser = super::accumulate::parser(),
conflicts_with = "files",
long_help = "\
Collect same-format files from each directory along a relative target path,
shallowest first, sorted by filename, with the target last. Files directly in
the working directory are excluded.
knf -a foo/bar/config.toml --list-files"
)]
pub accumulate: Option<knf::fs::AccumulateTarget>,
#[arg(short = 'l', long)]
pub list_files: bool,
#[arg(
short = 'c',
value_name = "KEY.PATH=VALUE",
long_help = "\
Inline layer applied after all files; repeatable. The value is parsed in the
selected format, falling back to a string.
-c port=8080 -c name=foo -c version='\"1.0\"'"
)]
pub set: Vec<PathLeaf<String>>,
#[arg(
long,
value_name = "PATTERN",
long_help = "\
Replace values at matching key paths wholesale instead of merging them. Dots
separate keys; single-quoted spans are literal.
--shallow '*' top-level keys (jq's a + b)
--shallow 'db' replace db entirely
--shallow 'db.*' replace db's children"
)]
pub shallow: Option<KeyGlobPattern>,
#[arg(short = 'f', long, value_name = "FORMAT")]
pub format: Option<FormatArg>,
#[arg(
short = 'i',
long,
long_help = "\
Resolve ${key.path} and ${env:VAR} references in the merged document.
A whole-string reference keeps the value's type; an embedded one becomes text.
$$ is a literal $.
data_dir = \"${root}/data\"
port = \"${env:PORT}\""
)]
pub interpolate: bool,
#[arg(long)]
pub strict: bool,
#[arg(long)]
pub compact: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shallow_accepts_one_required_glob() {
for args in [
vec!["--shallow", "{db,app}.*"],
vec!["--shallow={db,app}.*"],
] {
let cli = Cli::try_parse_from(std::iter::once("knf").chain(args)).unwrap();
let glob = cli.shallow.unwrap();
assert!(glob.matches_keys(&["db".into(), "pool".into()]));
assert!(!glob.matches_keys(&["db".into()]));
}
assert!(
Cli::try_parse_from(["knf", "a.json"])
.unwrap()
.shallow
.is_none()
);
for args in [
vec!["knf", "--shallow"],
vec!["knf", "--shallow="],
vec!["knf", "--shallow=a", "--shallow=b"],
] {
assert!(Cli::try_parse_from(args).is_err());
}
}
}