extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
pub mod cli;
mod layers;
mod load;
mod parse;
pub mod read;
pub use layers::{
CLI_SOURCE, CommandLine, ENV_SOURCE, SystemLayout, env_config, env_layer, is_secret_path,
loader_for, path_node, unknown_keys,
};
pub use load::{Loader, MAX_INCLUDE_DEPTH, origins};
pub const MAX_NESTING_DEPTH: usize = 32;
pub use parse::parse_str;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Origin {
Default,
Env(String),
File {
path: String,
line: usize,
},
Caller,
Cli(String),
}
impl core::fmt::Display for Origin {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Default => f.write_str("built-in default"),
Self::Env(name) => write!(f, "environment ${name}"),
Self::File { path, line } => write!(f, "{path}:{line}"),
Self::Caller => f.write_str("set by the caller"),
Self::Cli(flag) => write!(f, "command line `{flag}`"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Node {
Scalar(String, Origin),
Seq(Vec<Node>),
Map(BTreeMap<String, Node>),
}
impl Node {
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::Scalar(s, _) => Some(s.as_str()),
_ => None,
}
}
#[must_use]
pub fn origin(&self) -> Option<&Origin> {
match self {
Self::Scalar(_, o) => Some(o),
_ => None,
}
}
#[must_use]
pub fn get(&self, path: &str) -> Option<&Self> {
let mut here = self;
for segment in path.split('.') {
match here {
Self::Map(m) => here = m.get(segment)?,
_ => return None,
}
}
Some(here)
}
pub fn merge(&mut self, other: Self) -> Result<(), ConfigError> {
self.merge_at(other, 0)
}
fn merge_at(&mut self, other: Self, depth: usize) -> Result<(), ConfigError> {
if depth > MAX_NESTING_DEPTH {
return Err(ConfigError::TooDeep(MAX_NESTING_DEPTH));
}
if matches!(other, Self::Map(_)) && !matches!(self, Self::Map(_)) {
*self = Self::Map(BTreeMap::new());
}
let (mine, theirs) = match (&mut *self, other) {
(Self::Map(mine), Self::Map(theirs)) => (mine, theirs),
(slot, value) => {
*slot = value;
return Ok(());
}
};
for (key, value) in theirs {
if let Some(name) = key.strip_prefix('+') {
append(mine, name, value)?;
continue;
}
if matches!(value, Self::Map(_)) {
mine.entry(key)
.or_insert_with(|| Self::Map(BTreeMap::new()))
.merge_at(value, depth + 1)?;
} else {
mine.insert(key, value);
}
}
Ok(())
}
}
fn append(into: &mut BTreeMap<String, Node>, name: &str, value: Node) -> Result<(), ConfigError> {
match into.get_mut(name) {
Some(Node::Seq(existing)) => {
match value {
Node::Seq(added) => existing.extend(added),
single => existing.push(single),
}
Ok(())
}
Some(_) => Err(ConfigError::AppendToNonSeq(name.to_string())),
None => {
into.insert(
name.to_string(),
match value {
Node::Seq(v) => Node::Seq(v),
single => Node::Seq(alloc::vec![single]),
},
);
Ok(())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigError {
Io(String, String),
Syntax {
path: String,
line: usize,
message: String,
},
IncludeCycle(Vec<String>),
IncludeTooDeep(usize),
AppendToNonSeq(String),
TooDeep(usize),
Usage(String),
Invalid {
setting: String,
origin: Origin,
message: String,
},
}
impl core::fmt::Display for ConfigError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Io(path, why) => write!(f, "{path}: {why}"),
Self::Syntax {
path,
line,
message,
} => write!(f, "{path}:{line}: {message}"),
Self::IncludeCycle(chain) => {
write!(f, "include cycle: {}", chain.join(" -> "))
}
Self::IncludeTooDeep(depth) => {
write!(f, "includes nested deeper than {depth}")
}
Self::AppendToNonSeq(key) => {
write!(f, "`+{key}` appends, but `{key}` is not a sequence")
}
Self::TooDeep(depth) => {
write!(f, "nested deeper than {depth} levels")
}
Self::Usage(message) => f.write_str(message),
Self::Invalid {
setting,
origin,
message,
} => write!(f, "{origin}: invalid {setting}: {message}"),
}
}
}
impl std::error::Error for ConfigError {}
#[derive(Debug, Clone, Default)]
pub struct Config {
root: Option<Node>,
sources: Vec<String>,
}
impl Config {
#[must_use]
pub fn root(&self) -> Option<&Node> {
self.root.as_ref()
}
#[must_use]
pub fn sources(&self) -> &[String] {
&self.sources
}
#[must_use]
pub fn get(&self, path: &str) -> Option<&Node> {
self.root.as_ref()?.get(path)
}
#[must_use]
pub fn get_str(&self, path: &str) -> Option<&str> {
self.get(path)?.as_str()
}
#[must_use]
pub fn origin_of(&self, path: &str) -> Option<&Origin> {
self.get(path)?.origin()
}
pub fn push(&mut self, node: Node, source: String) -> Result<(), ConfigError> {
self.push_each(alloc::vec![node], source)
}
pub fn push_each(&mut self, nodes: Vec<Node>, source: String) -> Result<(), ConfigError> {
let mut root = self
.root
.clone()
.unwrap_or_else(|| Node::Map(BTreeMap::new()));
for node in nodes {
root.merge(node)?;
}
self.root = Some(root);
self.sources.push(source);
Ok(())
}
}