1pub mod format;
8pub mod fs;
9mod interp;
10mod merge;
11mod path;
12mod set;
13pub mod value;
14
15mod env;
16
17use std::io::Read;
18use std::path::{Path, PathBuf};
19
20use anyhow::Context;
21
22pub use env::ProcessEnv;
23pub use format::{ConfigFormat, Format};
24pub use interp::{Cycle, Env, InterpError, Problem, Syntax, interpolate};
25pub use merge::{MergeError, MergeOptions, merge, merge_into};
26pub use path::{PathError, RefPath, Seg, render_path};
27pub use set::{PathLeaf, json_or_string, toml_or_string};
28pub use value::{ConfigObject, ConfigValue};
29
30use format::SourceName;
31
32pub const STDIN: &str = "-";
34
35#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
40pub enum LoadError {
41 #[error("`-` reads stdin, which has no extension")]
43 StdinNeedsFormat,
44 #[error("inputs mix JSON and TOML formats; layers must use one format")]
46 MixedFormats,
47 #[error("`{}` is a directory; knf takes files as layers", path.display())]
49 Directory {
50 path: PathBuf,
52 },
53 #[error("cannot infer a format from `{}`", path.display())]
55 UnknownExtension {
56 path: PathBuf,
58 },
59}
60
61impl LoadError {
62 pub fn path(&self) -> Option<&Path> {
64 match self {
65 Self::StdinNeedsFormat | Self::MixedFormats => None,
66 Self::Directory { path } | Self::UnknownExtension { path } => Some(path),
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq)]
73pub enum Layers {
74 Json(Vec<serde_json::Value>),
76 Toml(Vec<toml::Value>),
78}
79
80pub fn resolve_format<P: AsRef<Path>>(
85 paths: &[P],
86 explicit: Option<Format>,
87) -> Result<Format, LoadError> {
88 let mut selected = explicit;
89 for path in paths {
90 let path = path.as_ref();
91 if path.is_dir() {
92 return Err(LoadError::Directory {
93 path: path.to_path_buf(),
94 });
95 }
96 if explicit.is_some() {
97 continue;
98 }
99 let format = if path.as_os_str() == STDIN {
100 return Err(LoadError::StdinNeedsFormat);
101 } else {
102 Format::from_path(path).ok_or_else(|| LoadError::UnknownExtension {
103 path: path.to_path_buf(),
104 })?
105 };
106 if selected.is_some_and(|previous| previous != format) {
107 return Err(LoadError::MixedFormats);
108 }
109 selected = Some(format);
110 }
111 Ok(selected.unwrap_or(Format::Json))
112}
113
114pub fn load_layers<P: AsRef<Path>>(
119 paths: &[P],
120 explicit_format: Option<Format>,
121) -> anyhow::Result<Layers> {
122 match resolve_format(paths, explicit_format)? {
123 Format::Json => Ok(Layers::Json(load_native(paths)?)),
124 Format::Toml => Ok(Layers::Toml(load_native(paths)?)),
125 }
126}
127
128fn load_native<P: AsRef<Path>, V: ConfigFormat>(paths: &[P]) -> anyhow::Result<Vec<V>> {
129 paths
130 .iter()
131 .map(|path| {
132 let path = path.as_ref();
133 let (name, text) = if path.as_os_str() == STDIN {
134 let mut text = String::new();
135 std::io::stdin()
136 .read_to_string(&mut text)
137 .context("reading stdin")?;
138 (SourceName::Stdin, text)
139 } else {
140 let text = std::fs::read_to_string(path)
141 .with_context(|| format!("reading `{}`", path.display()))?;
142 (SourceName::File(path.to_path_buf()), text)
143 };
144 format::parse(&text, &name)
145 })
146 .collect()
147}