config/file/
mod.rs

1mod format;
2pub(crate) mod source;
3
4use std::fmt::Debug;
5use std::path::{Path, PathBuf};
6
7use self::source::FileSource;
8use crate::error::{ConfigError, Result};
9use crate::map::Map;
10use crate::source::Source;
11use crate::value::Value;
12use crate::Format;
13
14pub use self::format::FileFormat;
15pub use self::source::file::FileSourceFile;
16pub use self::source::string::FileSourceString;
17
18/// An extension of [`Format`] trait.
19///
20/// Associates format with file extensions, therefore linking storage-agnostic notion of format to a file system.
21pub trait FileStoredFormat: Format {
22    /// Returns a vector of file extensions, for instance `[yml, yaml]`.
23    fn file_extensions(&self) -> &'static [&'static str];
24}
25
26/// A configuration source backed up by a file.
27///
28/// It supports optional automatic file format discovery.
29#[derive(Clone, Debug)]
30#[must_use]
31pub struct File<T, F> {
32    source: T,
33
34    /// Format of file (which dictates what driver to use).
35    format: Option<F>,
36
37    /// A required File will error if it cannot be found
38    required: bool,
39}
40
41impl<F> File<FileSourceString, F>
42where
43    F: FileStoredFormat + 'static,
44{
45    pub fn from_str(s: &str, format: F) -> Self {
46        Self {
47            format: Some(format),
48            required: true,
49            source: s.into(),
50        }
51    }
52}
53
54impl<F> File<FileSourceFile, F>
55where
56    F: FileStoredFormat + 'static,
57{
58    pub fn new(name: &str, format: F) -> Self {
59        Self {
60            format: Some(format),
61            required: true,
62            source: FileSourceFile::new(name.into()),
63        }
64    }
65}
66
67impl File<FileSourceFile, FileFormat> {
68    /// Given the basename of a file, will attempt to locate a file by setting its
69    /// extension to a registered format.
70    pub fn with_name(base_name: &str) -> Self {
71        Self {
72            format: None,
73            required: true,
74            source: FileSourceFile::new(base_name.into()),
75        }
76    }
77}
78
79impl<T, F> File<T, F>
80where
81    F: FileStoredFormat + 'static,
82    T: FileSource<F>,
83{
84    pub fn format(mut self, format: F) -> Self {
85        self.format = Some(format);
86        self
87    }
88
89    /// Set required to false to make a file optional when building the config.
90    pub fn required(mut self, required: bool) -> Self {
91        self.required = required;
92        self
93    }
94}
95
96impl<'a> From<&'a Path> for File<FileSourceFile, FileFormat> {
97    fn from(path: &'a Path) -> Self {
98        Self {
99            format: None,
100            required: true,
101            source: FileSourceFile::new(path.to_path_buf()),
102        }
103    }
104}
105
106impl From<PathBuf> for File<FileSourceFile, FileFormat> {
107    fn from(path: PathBuf) -> Self {
108        Self {
109            format: None,
110            required: true,
111            source: FileSourceFile::new(path),
112        }
113    }
114}
115
116impl<T, F> Source for File<T, F>
117where
118    F: FileStoredFormat + Debug + Clone + Send + Sync + 'static,
119    T: Sync + Send + FileSource<F> + 'static,
120{
121    fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
122        Box::new((*self).clone())
123    }
124
125    fn collect(&self) -> Result<Map<String, Value>> {
126        // Coerce the file contents to a string
127        let (uri, contents, format) = match self
128            .source
129            .resolve(self.format.clone())
130            .map_err(ConfigError::Foreign)
131        {
132            Ok(result) => (result.uri, result.content, result.format),
133
134            Err(error) => {
135                if !self.required {
136                    return Ok(Map::new());
137                }
138
139                return Err(error);
140            }
141        };
142
143        // Parse the string using the given format
144        format
145            .parse(uri.as_ref(), &contents)
146            .map_err(|cause| ConfigError::FileParse { uri, cause })
147    }
148}