extern crate alloc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::path::{Path, PathBuf};
use crate::{Config, ConfigError, Node};
pub const MAX_INCLUDE_DEPTH: usize = 16;
#[derive(Debug, Default, Clone)]
pub struct Loader {
base: Option<PathBuf>,
drop_in_dirs: Vec<PathBuf>,
extension: Option<String>,
missing_base_is_error: bool,
}
impl Loader {
#[must_use]
pub fn new() -> Self {
Self {
extension: Some("yaml".to_string()),
..Self::default()
}
}
#[must_use]
pub fn base(mut self, path: impl AsRef<Path>) -> Self {
self.base = Some(path.as_ref().to_path_buf());
self
}
#[must_use]
pub fn require_base(mut self) -> Self {
self.missing_base_is_error = true;
self
}
#[must_use]
pub fn drop_in_dir(mut self, path: impl AsRef<Path>) -> Self {
self.drop_in_dirs.push(path.as_ref().to_path_buf());
self
}
#[must_use]
pub fn extension(mut self, ext: Option<&str>) -> Self {
self.extension = ext.map(ToString::to_string);
self
}
pub fn load(&self) -> Result<Config, ConfigError> {
self.load_onto(Config::default())
}
pub fn load_onto(&self, mut config: Config) -> Result<Config, ConfigError> {
if let Some(base) = &self.base {
if base
.try_exists()
.map_err(|error| ConfigError::Io(base.display().to_string(), error.to_string()))?
{
self.read_into(&mut config, base, &mut Vec::new(), 0)?;
} else if self.missing_base_is_error {
return Err(ConfigError::Io(
base.display().to_string(),
"no such file".to_string(),
));
}
}
for dir in &self.drop_in_dirs {
for file in self.sorted_files(dir)? {
self.read_into(&mut config, &file, &mut Vec::new(), 0)?;
}
}
Ok(config)
}
fn sorted_files(&self, dir: &Path) -> Result<Vec<PathBuf>, ConfigError> {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(ConfigError::Io(dir.display().to_string(), e.to_string())),
};
let mut files: Vec<PathBuf> = Vec::new();
for entry in entries {
let entry =
entry.map_err(|e| ConfigError::Io(dir.display().to_string(), e.to_string()))?;
let path = entry.path();
if let Some(want) = &self.extension {
if path.extension().and_then(|e| e.to_str()) != Some(want.as_str()) {
continue;
}
}
let metadata = std::fs::metadata(&path)
.map_err(|e| ConfigError::Io(path.display().to_string(), e.to_string()))?;
if !metadata.is_file() {
continue;
}
files.push(path);
}
files.sort();
Ok(files)
}
fn read_into(
&self,
config: &mut Config,
path: &Path,
chain: &mut Vec<String>,
depth: usize,
) -> Result<(), ConfigError> {
if depth > MAX_INCLUDE_DEPTH {
return Err(ConfigError::IncludeTooDeep(MAX_INCLUDE_DEPTH));
}
let shown = path.display().to_string();
let identity = std::fs::canonicalize(path)
.map(|p| p.display().to_string())
.unwrap_or_else(|_| shown.clone());
if chain.contains(&identity) {
let mut names = chain.clone();
names.push(identity);
return Err(ConfigError::IncludeCycle(names));
}
chain.push(identity);
let metadata =
std::fs::metadata(path).map_err(|e| ConfigError::Io(shown.clone(), e.to_string()))?;
if metadata.is_dir() {
for file in self.sorted_files(path)? {
self.read_into(config, &file, chain, depth + 1)?;
}
chain.pop();
return Ok(());
}
let text = std::fs::read_to_string(path)
.map_err(|e| ConfigError::Io(shown.clone(), e.to_string()))?;
let mut node = crate::parse_str(&text, &shown)?;
let includes = take_includes(&mut node, &shown)?;
for relative in includes {
let resolved = resolve_include(path, &relative);
self.read_into(config, &resolved, chain, depth + 1)?;
}
config.push(node, shown)?;
chain.pop();
Ok(())
}
}
fn take_includes(node: &mut Node, path: &str) -> Result<Vec<String>, ConfigError> {
let Node::Map(map) = node else {
return Ok(Vec::new());
};
match map.remove("include") {
None => Ok(Vec::new()),
Some(Node::Seq(items)) => items
.into_iter()
.map(|item| include_path(item, path))
.collect(),
Some(one) => Ok(alloc::vec![include_path(one, path)?]),
}
}
fn include_path(node: Node, path: &str) -> Result<String, ConfigError> {
let line = match node.origin() {
Some(crate::Origin::File { line, .. }) => *line,
_ => 1,
};
if let Node::Scalar(value, _) = node {
if !value.trim().is_empty() {
return Ok(value);
}
}
Err(ConfigError::Syntax {
path: path.to_string(),
line,
message: "include expects a nonempty path or a sequence of nonempty paths".to_string(),
})
}
fn resolve_include(from: &Path, relative: &str) -> PathBuf {
let candidate = Path::new(relative);
if candidate.is_absolute() {
return candidate.to_path_buf();
}
from.parent()
.unwrap_or_else(|| Path::new("."))
.join(candidate)
}
#[must_use]
pub fn origins(config: &Config) -> Vec<(String, crate::Origin)> {
let mut out = Vec::new();
if let Some(root) = config.root() {
walk(root, &mut String::new(), &mut out);
}
out
}
fn walk(node: &Node, prefix: &mut String, out: &mut Vec<(String, crate::Origin)>) {
walk_at(node, prefix, out, 0);
}
fn walk_at(node: &Node, prefix: &mut String, out: &mut Vec<(String, crate::Origin)>, depth: usize) {
if depth > crate::MAX_NESTING_DEPTH {
return;
}
match node {
Node::Scalar(_, origin) => out.push((prefix.clone(), origin.clone())),
Node::Map(map) => {
for (key, value) in map {
let mark = prefix.len();
if !prefix.is_empty() {
prefix.push('.');
}
prefix.push_str(key);
walk_at(value, prefix, out, depth + 1);
prefix.truncate(mark);
}
}
Node::Seq(items) => {
for (i, item) in items.iter().enumerate() {
let mark = prefix.len();
prefix.push_str(&alloc::format!("[{i}]"));
walk_at(item, prefix, out, depth + 1);
prefix.truncate(mark);
}
}
}
}