1use std::path::{Path, PathBuf};
8
9use serde::de::DeserializeOwned;
10
11#[non_exhaustive]
13#[derive(Debug, thiserror::Error)]
14pub enum LoadError {
15 #[error("no config file named {0:?} found")]
17 NotFound(String),
18 #[error("could not read {path}: {source}")]
20 Io {
21 path: PathBuf,
23 #[source]
25 source: std::io::Error,
26 },
27 #[error("could not parse {path}: {message}")]
29 Parse {
30 path: PathBuf,
32 message: String,
34 },
35}
36
37#[non_exhaustive]
39#[derive(Debug)]
40pub struct LoadedConfig<T> {
41 pub source: PathBuf,
43 pub value: T,
45}
46
47#[must_use]
50pub fn find_ancestor(start: &Path, file_name: &str) -> Option<PathBuf> {
51 let mut current: Option<&Path> = Some(start);
52 while let Some(dir) = current {
53 let candidate = dir.join(file_name);
54 if candidate.is_file() {
55 return Some(candidate);
56 }
57 current = dir.parent();
58 }
59 None
60}
61
62pub fn load_from_ancestor<T>(start: &Path, file_name: &str) -> Result<LoadedConfig<T>, LoadError>
68where
69 T: DeserializeOwned,
70{
71 let path =
72 find_ancestor(start, file_name).ok_or_else(|| LoadError::NotFound(file_name.to_owned()))?;
73 load_from_path(&path)
74}
75
76pub fn load_from_path<T>(path: &Path) -> Result<LoadedConfig<T>, LoadError>
82where
83 T: DeserializeOwned,
84{
85 let text = std::fs::read_to_string(path).map_err(|source| LoadError::Io {
86 path: path.to_path_buf(),
87 source,
88 })?;
89 let value: T = toml::from_str(&text).map_err(|err| LoadError::Parse {
90 path: path.to_path_buf(),
91 message: err.to_string(),
92 })?;
93 Ok(LoadedConfig {
94 source: path.to_path_buf(),
95 value,
96 })
97}