pub mod format;
pub mod fs;
mod interp;
mod merge;
mod path;
mod set;
pub mod value;
mod env;
use std::io::Read;
use std::path::{Path, PathBuf};
use anyhow::Context;
pub use env::ProcessEnv;
pub use format::{ConfigFormat, Format};
pub use interp::{Cycle, Env, InterpError, Problem, Syntax, interpolate};
pub use merge::{MergeError, MergeOptions, merge, merge_into};
pub use path::{PathError, RefPath, Seg, render_path};
pub use set::{PathLeaf, json_or_string, toml_or_string};
pub use value::{ConfigObject, ConfigValue};
use format::SourceName;
pub const STDIN: &str = "-";
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum LoadError {
#[error("`-` reads stdin, which has no extension")]
StdinNeedsFormat,
#[error("inputs mix JSON and TOML formats; layers must use one format")]
MixedFormats,
#[error("`{}` is a directory; knf takes files as layers", path.display())]
Directory {
path: PathBuf,
},
#[error("cannot infer a format from `{}`", path.display())]
UnknownExtension {
path: PathBuf,
},
}
impl LoadError {
pub fn path(&self) -> Option<&Path> {
match self {
Self::StdinNeedsFormat | Self::MixedFormats => None,
Self::Directory { path } | Self::UnknownExtension { path } => Some(path),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Layers {
Json(Vec<serde_json::Value>),
Toml(Vec<toml::Value>),
}
pub fn resolve_format<P: AsRef<Path>>(
paths: &[P],
explicit: Option<Format>,
) -> Result<Format, LoadError> {
let mut selected = explicit;
for path in paths {
let path = path.as_ref();
if path.is_dir() {
return Err(LoadError::Directory {
path: path.to_path_buf(),
});
}
if explicit.is_some() {
continue;
}
let format = if path.as_os_str() == STDIN {
return Err(LoadError::StdinNeedsFormat);
} else {
Format::from_path(path).ok_or_else(|| LoadError::UnknownExtension {
path: path.to_path_buf(),
})?
};
if selected.is_some_and(|previous| previous != format) {
return Err(LoadError::MixedFormats);
}
selected = Some(format);
}
Ok(selected.unwrap_or(Format::Json))
}
pub fn load_layers<P: AsRef<Path>>(
paths: &[P],
explicit_format: Option<Format>,
) -> anyhow::Result<Layers> {
match resolve_format(paths, explicit_format)? {
Format::Json => Ok(Layers::Json(load_native(paths)?)),
Format::Toml => Ok(Layers::Toml(load_native(paths)?)),
}
}
fn load_native<P: AsRef<Path>, V: ConfigFormat>(paths: &[P]) -> anyhow::Result<Vec<V>> {
paths
.iter()
.map(|path| {
let path = path.as_ref();
let (name, text) = if path.as_os_str() == STDIN {
let mut text = String::new();
std::io::stdin()
.read_to_string(&mut text)
.context("reading stdin")?;
(SourceName::Stdin, text)
} else {
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading `{}`", path.display()))?;
(SourceName::File(path.to_path_buf()), text)
};
format::parse(&text, &name)
})
.collect()
}