1use std::{
2 collections::BTreeMap,
3 path::{Path, PathBuf},
4};
5
6use crate::{error::SourceError, format::Format, value::ConfigValue};
7
8pub trait Source {
10 fn collect(&self) -> Result<ConfigValue, SourceError>;
15
16 #[must_use]
24 fn with_app_name(self, _name: &str) -> Self
25 where
26 Self: Sized,
27 {
28 self
29 }
30}
31
32#[derive(Default)]
38pub struct EnvSource {
39 prefix: String,
40}
41
42impl From<String> for EnvSource {
43 fn from(prefix: String) -> Self {
44 Self { prefix }
45 }
46}
47
48impl From<&str> for EnvSource {
49 fn from(prefix: &str) -> Self {
50 Self {
51 prefix: prefix.to_owned(),
52 }
53 }
54}
55
56impl Source for EnvSource {
57 fn with_app_name(mut self, name: &str) -> Self {
58 if self.prefix.is_empty() {
59 name.clone_into(&mut self.prefix);
60 }
61 self
62 }
63
64 fn collect(&self) -> Result<ConfigValue, SourceError> {
65 if self.prefix.is_empty() {
66 return Ok(ConfigValue::Table(BTreeMap::new()));
67 }
68 let prefix = format!("{}_", self.prefix.to_uppercase());
69 let mut table = BTreeMap::new();
70
71 for (key, val) in std::env::vars() {
72 let Some(stripped) = key.strip_prefix(&prefix) else {
73 continue;
74 };
75 let path: Vec<String> = stripped
76 .to_lowercase()
77 .split("__")
78 .map(String::from)
79 .collect();
80 insert_nested(&mut table, &path, val.parse().unwrap());
81 }
82
83 Ok(ConfigValue::Table(table))
84 }
85}
86
87fn insert_nested(table: &mut BTreeMap<String, ConfigValue>, path: &[String], value: ConfigValue) {
88 let Some((head, tail)) = path.split_first() else {
89 return;
90 };
91 if tail.is_empty() {
92 table.insert(head.clone(), value);
93 return;
94 }
95 let entry = table
96 .entry(head.clone())
97 .or_insert_with(|| ConfigValue::Table(BTreeMap::new()));
98 if let ConfigValue::Table(inner) = entry {
99 insert_nested(inner, tail, value);
100 }
101}
102
103pub struct CliSource {
108 values: BTreeMap<String, ConfigValue>,
109}
110
111impl FromIterator<(String, ConfigValue)> for CliSource {
112 fn from_iter<I: IntoIterator<Item = (String, ConfigValue)>>(iter: I) -> Self {
113 Self {
114 values: iter.into_iter().collect(),
115 }
116 }
117}
118
119impl Source for CliSource {
120 fn collect(&self) -> Result<ConfigValue, SourceError> {
121 Ok(ConfigValue::Table(self.values.clone()))
122 }
123}
124
125pub struct FileSource<F: Format> {
127 candidates: Vec<PathBuf>,
128 format: F,
129}
130
131impl<F: Format> FileSource<F> {
132 #[must_use]
133 pub fn new(candidates: impl IntoIterator<Item = PathBuf>, format: F) -> Self {
134 Self {
135 candidates: candidates.into_iter().collect(),
136 format,
137 }
138 }
139
140 #[must_use]
142 pub fn resolve(&self) -> Option<&Path> {
143 self.candidates
144 .iter()
145 .find(|p| p.exists())
146 .map(PathBuf::as_path)
147 }
148}
149
150impl<F: Format> Source for FileSource<F> {
151 fn collect(&self) -> Result<ConfigValue, SourceError> {
152 let Some(path) = self.resolve() else {
153 return Ok(ConfigValue::Table(BTreeMap::new()));
154 };
155 let bytes = std::fs::read(path).map_err(|e| SourceError::Io {
156 path: path.to_owned(),
157 source: e,
158 })?;
159 self.format.parse(path, &bytes)
160 }
161}
162
163#[cfg(feature = "hot-reload")]
169pub trait WatchableSource: Source + Send + Sync {
170 fn watch_paths(&self) -> Vec<std::path::PathBuf>;
174}
175
176#[cfg(feature = "hot-reload")]
177impl<F: Format + Send + Sync> WatchableSource for FileSource<F> {
178 fn watch_paths(&self) -> Vec<std::path::PathBuf> {
179 self.candidates.clone()
180 }
181}
182
183#[cfg(feature = "async")]
185pub type CollectFuture<'a> = std::pin::Pin<
186 Box<dyn std::future::Future<Output = Result<ConfigValue, SourceError>> + Send + 'a>,
187>;
188
189#[cfg(feature = "async")]
209pub trait AsyncSource: Send + Sync {
210 fn collect(&self) -> CollectFuture<'_>;
215}
216
217#[cfg(feature = "async")]
219impl<T: Source + Send + Sync> AsyncSource for T {
220 fn collect(&self) -> CollectFuture<'_> {
221 Box::pin(std::future::ready(Source::collect(self)))
222 }
223}