storm_config/file/
mod.rs

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