1use std::borrow::Cow;
2use std::path::{Path, PathBuf};
3
4use serde::de;
5
6use crate::source::{utils, Any, Expansion, Source};
7
8pub trait FileSystem {
17 fn read(&mut self, path: &Path) -> std::io::Result<Vec<u8>>;
19
20 fn read_to_string(&mut self, path: &Path) -> std::io::Result<String> {
22 let bytes = self.read(path)?;
23 String::from_utf8(bytes)
24 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
25 }
26}
27
28#[derive(Debug, Clone, Copy)]
30pub struct StdFileSystem;
31
32impl FileSystem for StdFileSystem {
33 fn read(&mut self, path: &Path) -> std::io::Result<Vec<u8>> {
34 std::fs::read(path)
35 }
36
37 fn read_to_string(&mut self, path: &Path) -> std::io::Result<String> {
38 std::fs::read_to_string(path)
39 }
40}
41
42pub struct FileSource<F> {
77 base_path: PathBuf,
78 variable: utils::Variable,
79 filesystem: F,
80}
81
82impl FileSource<StdFileSystem> {
83 pub fn new() -> Self {
104 Self {
105 base_path: PathBuf::new(),
106 variable: Default::default(),
107 filesystem: StdFileSystem,
108 }
109 }
110}
111
112impl<F> FileSource<F> {
113 pub fn with_base_path<P>(mut self, path: P) -> Self
120 where
121 P: Into<PathBuf>,
122 {
123 self.base_path = path.into();
124 self
125 }
126
127 pub fn with_file_system<T>(self, filesystem: T) -> FileSource<T> {
154 FileSource {
155 base_path: self.base_path,
156 variable: self.variable,
157 filesystem,
158 }
159 }
160
161 pub fn with_variable_prefix(mut self, prefix: impl Into<String>) -> Self {
179 self.variable.prefix = prefix.into();
180 self
181 }
182
183 pub fn with_variable_suffix(mut self, suffix: impl Into<String>) -> Self {
185 self.variable.suffix = suffix.into();
186 self
187 }
188}
189
190impl<F: FileSystem> FileSource<F> {
191 fn resolve_path<'a>(&self, path: &'a Path) -> Cow<'a, Path> {
192 match path.is_absolute() {
193 true => Cow::Borrowed(path),
194 false => Cow::Owned(self.base_path.join(path)),
195 }
196 }
197
198 fn io_error<E>(&self, path: &Path, v: &Path, error: std::io::Error) -> E
199 where
200 E: de::Error,
201 {
202 let path = path.display();
203 let var = self.variable.fmt(v.display());
204 E::custom(format!(
205 "failed to read file `{path}` from variable `{var}`: {error}"
206 ))
207 }
208
209 fn mismatched_type<E>(&self, var: &str, unexpected: de::Unexpected<'_>, expected: &str) -> E
210 where
211 E: de::Error,
212 {
213 let var = self.variable.fmt(var);
214 E::invalid_value(
215 unexpected,
216 &format!("file contents of variable `{var}` to be {expected}").as_str(),
217 )
218 }
219
220 fn parsed<V, E>(&mut self, v: &str, expected: &str) -> Result<Option<V>, E>
221 where
222 V: std::str::FromStr,
223 V::Err: std::fmt::Display,
224 E: de::Error,
225 {
226 let Some(var) = self.variable.parse_str(v) else {
227 return Ok(None);
228 };
229
230 let path = self.resolve_path(var.as_ref());
231 let value = self
232 .filesystem
233 .read_to_string(&path)
234 .map_err(|error| self.io_error(&path, var.as_ref(), error))?;
235
236 value
237 .parse()
238 .map(Some)
239 .map_err(|_| self.mismatched_type(var, de::Unexpected::Str(&value), expected))
240 }
241}
242
243impl<F: FileSystem> Source for FileSource<F> {
244 fn expand_str<'a, E>(&mut self, v: Cow<'a, str>) -> Result<Expansion<Cow<'a, str>>, E>
245 where
246 E: serde::de::Error,
247 {
248 let Some(var) = self.variable.parse_str(&v) else {
249 return Ok(Expansion::Original(v));
250 };
251
252 let path = self.resolve_path(var.as_ref());
253 let value = self
254 .filesystem
255 .read_to_string(&path)
256 .map_err(|error| self.io_error(&path, var.as_ref(), error))?;
257
258 match utils::parse(Cow::Owned(value)) {
259 Any::Str(value) => Ok(Expansion::Expanded(value)),
260 other => Err(self.mismatched_type(var, other.unexpected(), "a string")),
261 }
262 }
263
264 fn expand_bytes<'a, E>(&mut self, v: Cow<'a, [u8]>) -> Result<Expansion<Cow<'a, [u8]>>, E>
265 where
266 E: serde::de::Error,
267 {
268 let Some(var) = self.variable.parse_bytes(&v) else {
269 return Ok(Expansion::Original(v));
270 };
271
272 #[cfg(unix)]
273 let path = {
274 use std::{ffi::OsStr, os::unix::ffi::OsStrExt, path::Path};
275 Path::new(OsStr::from_bytes(var))
276 };
277 #[cfg(not(unix))]
281 let path = std::str::from_utf8(var).map(Path::new).map_err(E::custom)?;
282
283 let full_path = self.resolve_path(path);
284 let value = self
285 .filesystem
286 .read(&full_path)
287 .map_err(|error| self.io_error(&full_path, path, error))?;
288
289 Ok(Expansion::Expanded(Cow::Owned(value)))
290 }
291
292 fn expand_bool<E>(&mut self, v: &str) -> Result<Option<bool>, E>
293 where
294 E: de::Error,
295 {
296 self.parsed(v, "a boolean")
297 }
298
299 fn expand_i8<E>(&mut self, v: &str) -> Result<Option<i8>, E>
300 where
301 E: de::Error,
302 {
303 self.parsed(v, "a signed integer (i8)")
304 }
305
306 fn expand_i16<E>(&mut self, v: &str) -> Result<Option<i16>, E>
307 where
308 E: de::Error,
309 {
310 self.parsed(v, "a signed integer (i16)")
311 }
312
313 fn expand_i32<E>(&mut self, v: &str) -> Result<Option<i32>, E>
314 where
315 E: de::Error,
316 {
317 self.parsed(v, "a signed integer (i32)")
318 }
319
320 fn expand_i64<E>(&mut self, v: &str) -> Result<Option<i64>, E>
321 where
322 E: de::Error,
323 {
324 self.parsed(v, "a signed integer (i64)")
325 }
326
327 fn expand_u8<E>(&mut self, v: &str) -> Result<Option<u8>, E>
328 where
329 E: de::Error,
330 {
331 self.parsed(v, "an unsigned integer (i8)")
332 }
333
334 fn expand_u16<E>(&mut self, v: &str) -> Result<Option<u16>, E>
335 where
336 E: de::Error,
337 {
338 self.parsed(v, "an unsigned integer (i16)")
339 }
340
341 fn expand_u32<E>(&mut self, v: &str) -> Result<Option<u32>, E>
342 where
343 E: de::Error,
344 {
345 self.parsed(v, "an unsigned integer (i32)")
346 }
347
348 fn expand_u64<E>(&mut self, v: &str) -> Result<Option<u64>, E>
349 where
350 E: de::Error,
351 {
352 self.parsed(v, "an unsigned integer (i64)")
353 }
354
355 fn expand_f32<E>(&mut self, v: &str) -> Result<Option<f32>, E>
356 where
357 E: de::Error,
358 {
359 self.parsed(v, "a floating point")
360 }
361
362 fn expand_f64<E>(&mut self, v: &str) -> Result<Option<f64>, E>
363 where
364 E: de::Error,
365 {
366 self.parsed(v, "a floating point")
367 }
368
369 fn expand_any<'a, E>(&mut self, v: Cow<'a, str>) -> Result<Expansion<Any<'a>, Cow<'a, str>>, E>
370 where
371 E: de::Error,
372 {
373 let Some(var) = self.variable.parse_str(&v) else {
374 return Ok(Expansion::Original(v));
375 };
376
377 let path = self.resolve_path(var.as_ref());
378 let value = self
379 .filesystem
380 .read(&path)
381 .map_err(|error| self.io_error(&path, var.as_ref(), error))?;
382
383 let value = String::from_utf8(value)
384 .map(Cow::Owned)
385 .map(utils::parse)
386 .unwrap_or_else(|err| Any::Bytes(Cow::Owned(err.into_bytes())));
387 Ok(Expansion::Expanded(value))
388 }
389}
390
391impl Default for FileSource<StdFileSystem> {
392 fn default() -> Self {
393 Self::new()
394 }
395}