Skip to main content

knf/
lib.rs

1//! Load, merge and emit layered JSON and TOML configuration.
2//!
3//! Three steps, each a function, and a caller composes them:
4//!
5//! 1. [`load_layers`] reads each path into a [`Value`], keeping the format it
6//!    was read as;
7//! 2. [`merge`] folds the flat layer list from left to right — any in-memory
8//!    overlays are just more layers appended to the list;
9//! 3. [`interpolate`], if wanted, resolves `${...}` references once, on the
10//!    merged document.
11//!
12//! [`format::emit`] renders the result. JSON and TOML appear only in
13//! [`format`](mod@format) and [`value`]; the merge and interpolation never learn either
14//! exists.
15//!
16//! No flag names. An error from this crate carries key paths and file paths;
17//! the command-line spelling that produced it is `knf-cli`'s to add. That is
18//! what [`LoadError`] exists for — the failures that have an obvious
19//! command-line remedy are typed, so the caller decides how to name them.
20
21pub mod format;
22pub mod fs;
23mod interp;
24mod ir;
25mod merge;
26mod path;
27mod set;
28pub mod value;
29
30mod env;
31
32use std::io::Read;
33use std::path::{Path, PathBuf};
34
35use anyhow::Context;
36
37pub use env::ProcessEnv;
38pub use format::Format;
39pub use interp::{Cycle, Env, EnvValue, InterpError, Problem, Syntax, interpolate};
40pub use ir::{Map, Number, Value};
41pub use merge::{MergeError, MergeOptions, merge, merge_into};
42pub use path::{PathError, RefPath, Seg, render_path};
43pub use set::{PathLeaf, json_or_string};
44pub use value::{BadDatetime, IntegerOutOfRange, NonFiniteFloat, NullInToml, TomlError};
45
46use format::SourceName;
47
48/// The positional that means "read stdin".
49pub const STDIN: &str = "-";
50
51/// Why a positional could not be turned into a layer.
52///
53/// Carries paths and nothing else. Each of these has an obvious command-line
54/// remedy and no library-level one, which is exactly why the remedy is not
55/// spelled here: `knf-cli` matches on the variant and adds the flag.
56#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
57pub enum LoadError {
58    /// `-` was given without an explicit input format.
59    #[error("`-` reads stdin, which has no extension")]
60    StdinNeedsFormat,
61    /// A positional named a directory.
62    #[error("`{}` is a directory; knf takes files as layers", path.display())]
63    Directory {
64        /// The directory that was named.
65        path: PathBuf,
66    },
67    /// A positional's extension is neither `json` nor `toml`.
68    #[error("cannot infer a format from `{}`", path.display())]
69    UnknownExtension {
70        /// The file whose extension said nothing.
71        path: PathBuf,
72    },
73}
74
75impl LoadError {
76    /// The path this failed on, or `None` for stdin.
77    pub fn path(&self) -> Option<&Path> {
78        match self {
79            Self::StdinNeedsFormat => None,
80            Self::Directory { path } | Self::UnknownExtension { path } => Some(path),
81        }
82    }
83}
84
85/// Reads and parses every positional into the merge IR, keeping the format each
86/// input was read as.
87///
88/// A path equal to [`STDIN`] reads standard input and therefore requires an
89/// explicit `input_format`; otherwise the format is inferred from the extension
90/// unless `input_format` overrides it. JSON and TOML may be mixed.
91///
92/// The formats are returned because a caller may have a decision to make before
93/// the fold, and they are the input to it: `knf-cli` resolves the
94/// *output* format here. A missing `-f` is a mistake in argv alone, and
95/// reporting it must not wait behind a merge conflict the user would otherwise
96/// fix first, only to learn about the flag on the next run.
97pub fn load_layers<P: AsRef<Path>>(
98    paths: &[P],
99    input_format: Option<Format>,
100) -> anyhow::Result<(Vec<Value>, Vec<Format>)> {
101    let mut layers: Vec<Value> = Vec::with_capacity(paths.len());
102    let mut input_formats: Vec<Format> = Vec::with_capacity(paths.len());
103
104    for path in paths {
105        let (name, format, text) = read_input(path.as_ref(), input_format)?;
106        let value = format::parse(format, &text, &name)?;
107        input_formats.push(format);
108        layers.push(value);
109    }
110    Ok((layers, input_formats))
111}
112
113/// Reads one positional, resolving its format.
114fn read_input(
115    path: &Path,
116    override_format: Option<Format>,
117) -> anyhow::Result<(SourceName, Format, String)> {
118    if path.as_os_str() == STDIN {
119        let format = override_format.ok_or(LoadError::StdinNeedsFormat)?;
120        let mut text = String::new();
121        std::io::stdin()
122            .read_to_string(&mut text)
123            .context("reading stdin")?;
124        return Ok((SourceName::Stdin, format, text));
125    }
126
127    if path.is_dir() {
128        return Err(LoadError::Directory {
129            path: path.to_path_buf(),
130        }
131        .into());
132    }
133
134    let format = match override_format {
135        Some(format) => format,
136        None => Format::from_path(path).ok_or_else(|| LoadError::UnknownExtension {
137            path: path.to_path_buf(),
138        })?,
139    };
140    let text =
141        std::fs::read_to_string(path).with_context(|| format!("reading `{}`", path.display()))?;
142    Ok((SourceName::File(path.to_path_buf()), format, text))
143}