mod cli;
mod explain;
use std::io::Write;
use anyhow::bail;
use clap::Parser;
use knf::{Format, Map, MergeOpts, PathLeaf, Rules, Strategy, format, load_layers, merge_layers};
use cli::Cli;
use explain::{explain_pipeline, explain_rules, name_the_rule_flag, name_the_set_flag};
fn main() {
let cli = Cli::parse();
if let Err(err) = run(cli) {
eprintln!("error: {err}");
for cause in err.chain().skip(1) {
eprintln!(" caused by: {cause}");
}
std::process::exit(1);
}
}
fn run(cli: Cli) -> anyhow::Result<()> {
let opts = merge_opts(&cli)?;
let (layers, input_formats) =
load_layers(&cli.files, opts.input_format).map_err(explain_pipeline)?;
let out_format = resolve_output_format(cli.format.map(Format::from), &input_formats)?;
let merged = merge_layers(layers, opts, &knf::ProcessEnv).map_err(explain_pipeline)?;
let text = format::emit(merged, out_format, !cli.compact, cli.null_as.as_deref())
.map_err(explain_pipeline)?;
write_stdout(&text)
}
fn merge_opts(cli: &Cli) -> anyhow::Result<MergeOpts> {
let flags = [
("--append", &cli.append, Strategy::Append),
("--replace", &cli.replace, Strategy::Replace),
("--fail", &cli.fail, Strategy::Fail),
];
let mut rules: Vec<(Vec<String>, Strategy)> = Vec::new();
for (flag, paths, strategy) in flags {
for path in paths {
let keys = path
.clone()
.try_into_keys()
.map_err(|err| name_the_rule_flag(err, flag))?;
rules.push((keys, strategy));
}
}
let mut overlays: Vec<Map> = Vec::with_capacity(cli.set.len());
for path_leaf in &cli.set {
let typed = PathLeaf::<serde_json::Value>::from(path_leaf.clone());
let json = serde_json::Value::try_from(typed).map_err(name_the_set_flag)?;
let serde_json::Value::Object(obj) = json else {
unreachable!("a --set expression expands to a nested object")
};
overlays.push(knf::value::object_from_json(obj));
}
Ok(MergeOpts {
input_format: cli.input_format.map(Format::from),
strict: cli.strict,
rules: Rules::build(rules).map_err(explain_rules)?,
overlays,
interpolate: cli.interpolate,
})
}
fn resolve_output_format(explicit: Option<Format>, inputs: &[Format]) -> anyhow::Result<Format> {
if let Some(format) = explicit {
return Ok(format);
}
let mut distinct: Vec<Format> = Vec::new();
for format in inputs {
if !distinct.contains(format) {
distinct.push(*format);
}
}
match distinct.as_slice() {
[] => Ok(Format::Json),
[only] => Ok(*only),
mixed => {
let names: Vec<String> = mixed.iter().map(Format::to_string).collect();
bail!(
"inputs mix {} formats; -f is required to choose the output format\n\
help: pass -f json or -f toml",
names.join(" and "),
)
}
}
}
fn write_stdout(text: &str) -> anyhow::Result<()> {
use anyhow::Context;
match std::io::stdout().write_all(text.as_bytes()) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
Err(e) => Err(e).context("writing to stdout"),
}
}