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