okcodes_config/file/
mod.rs

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