1pub mod cli;
4pub mod format;
5pub mod value;
6
7use std::io::{Read, Write};
8use std::path::Path;
9
10use anyhow::{Context, bail};
11use knf_core::{MergeOptions, Value, merge_with};
12use knf_dotted::PathLeaf;
13
14use cli::Cli;
15use format::{Format, SourceName};
16
17const STDIN: &str = "-";
19
20pub fn run(cli: Cli) -> anyhow::Result<()> {
26 let mut layers: Vec<(SourceName, Value)> = Vec::new();
27 let mut input_formats: Vec<Format> = Vec::new();
28
29 for path in &cli.files {
30 let (name, format, text) = read_input(path, cli.input_format)?;
31 let value = format::parse(format, &text, &name)?;
32 input_formats.push(format);
33 layers.push((name, value));
34 }
35
36 for path_leaf in &cli.set {
39 let name = SourceName::Set(path_leaf.to_string());
40 let json = serde_json::Value::from(PathLeaf::<serde_json::Value>::from(path_leaf.clone()));
41 layers.push((name, value::from_json(json)));
42 }
43
44 let out_format = resolve_output_format(cli.format, &input_formats)?;
45 let opts = merge_options(&cli);
46
47 let sources: Vec<(SourceName, Value)> = match out_format {
50 Format::Toml => layers.clone(),
51 Format::Json => Vec::new(),
52 };
53
54 let merged = merge_with(layers.into_iter().map(|(_, value)| value), &opts)?;
55 let text = format::emit(merged, out_format, !cli.compact, &sources)?;
56 write_stdout(&text)
57}
58
59fn read_input(
61 path: &Path,
62 override_format: Option<Format>,
63) -> anyhow::Result<(SourceName, Format, String)> {
64 if path.as_os_str() == STDIN {
65 let format = override_format.context(
66 "`-` reads stdin, which has no extension: pass --input-format json or --input-format toml",
67 )?;
68 let mut text = String::new();
69 std::io::stdin()
70 .read_to_string(&mut text)
71 .context("reading stdin")?;
72 return Ok((SourceName::Stdin, format, text));
73 }
74
75 if path.is_dir() {
76 bail!(
77 "`{}` is a directory; knf takes files as layers\n\
78 help: `knf {}/*.toml` merges its files as layers",
79 path.display(),
80 path.display(),
81 );
82 }
83
84 let format = match override_format {
85 Some(format) => format,
86 None => Format::from_path(path).with_context(|| {
87 format!(
88 "cannot infer a format from `{}`: pass --input-format json or --input-format toml",
89 path.display()
90 )
91 })?,
92 };
93 let text =
94 std::fs::read_to_string(path).with_context(|| format!("reading `{}`", path.display()))?;
95 Ok((SourceName::File(path.to_path_buf()), format, text))
96}
97
98pub fn resolve_output_format(
103 explicit: Option<Format>,
104 inputs: &[Format],
105) -> anyhow::Result<Format> {
106 if let Some(format) = explicit {
107 return Ok(format);
108 }
109 let mut distinct: Vec<Format> = Vec::new();
110 for format in inputs {
111 if !distinct.contains(format) {
112 distinct.push(*format);
113 }
114 }
115 match distinct.as_slice() {
116 [] => Ok(Format::Json),
118 [only] => Ok(*only),
119 mixed => {
120 let names: Vec<String> = mixed.iter().map(Format::to_string).collect();
121 bail!(
122 "inputs mix {} formats; -f is required to choose the output format\n\
123 help: pass -f json or -f toml",
124 names.join(" and "),
125 )
126 }
127 }
128}
129
130pub fn merge_options(cli: &Cli) -> MergeOptions {
131 MergeOptions { strict: cli.strict }
132}
133
134pub fn write_stdout(text: &str) -> anyhow::Result<()> {
137 match std::io::stdout().write_all(text.as_bytes()) {
138 Ok(()) => Ok(()),
139 Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
140 Err(e) => Err(e).context("writing to stdout"),
141 }
142}