config_more_formats/
lib.rs

1/*!
2Add more formats to [`config`](https://crates.io/crates/config).
3
4This crate provides additional formats for figment.
5If you need additional formats, you can include this crate and use the provided formats.
6
7Additionally, this adds a function to parse a file by its extension.
8
9Supported formats added by this crate:
10
11| Format          | Feature      | Crate                                                                   | Description                                                                      |
12|-----------------|--------------|-------------------------------------------------------------------------|----------------------------------------------------------------------------------|
13| YAML-NG         | `yaml_ng`    | [serde-yaml-ng](https://crates.io/crates/serde_yaml_ng)                 | An actively maintained fork of [serde_yaml](https://crates.io/crates/serde_yaml) |
14| JAVA Properties | `properties` | [serde-java-properties](https://crates.io/crates/serde_java_properties) | Java properties file format                                                      |
15| HJSON           | `hjson`      | [serde-hjson](https://crates.io/crates/serde_hjson)                     | [Human JSON](https://hjson.github.io/)                                           |
16| HCL             | `hcl`        | [hcl-rs](https://crates.io/crates/hcl)                                  | HashiCorp Configuration Language                                                 |
17| Ason            | `ason`       | [ason](https://crates.io/crates/ason)                                   | Ason format                                                                      |
18| JSON            | `json`       | [serde_json](https://crates.io/crates/serde_json)                       | config supplied JSON format                                                     |
19| JSON5           | `json5`      | [serde-json5](https://crates.io/crates/serde_json5)                     | config supplied JSON5 format                                                    |
20| RON             | `ron`        | [ron](https://crates.io/crates/ron)                                     | config supplied RON format                                                      |
21| TOML            | `toml`       | [toml](https://crates.io/crates/toml)                                   | config supplied TOML format                                                     |
22| YAML            | `yaml`       | [yaml-rust](https://crates.io/crates/yaml-rust)                         | config supplied YAML format (using deprecated `serde_yaml`)                     |
23| INI             | `ini`        | [ini](https://crates.io/crates/ini)                                     | config supplied INI format                                                      |
24|                 | `all`        |                                                                         | Enable all formats except for `yaml`                                             |
25
26If you do not enable yaml, yaml_ng will be used for yaml files instead.
27Instead of `all`, enable only the formats you need to reduce compile times and dependencies.
28
29The current development version of config already uses [`yaml-rust2`](https://crates.io/crates/yaml-rust2)
30which is a fork of `yaml-rust` and is actively maintained.
31This crate uses `serde_yaml_ng` which is another actively maintained solution for YAML.
32
33# Example of [`by_file_extension`]
34
35```rust
36use config::Config;use config_more_formats::by_file_extension;
37
38# fn main() {
39#     std::fs::File::create("settings.toml").unwrap();
40
41    let settings = Config::builder()
42        .add_source(by_file_extension("settings.toml").unwrap())
43        .build()
44        .unwrap();
45
46#     std::fs::remove_file("settings.toml").unwrap();
47# }
48```
49*/
50
51#[cfg(feature = "hjson")]
52mod hjson;
53
54#[cfg(feature = "hjson")]
55pub use hjson::Hjson;
56use std::error::Error;
57#[cfg(feature = "properties")]
58mod properties;
59#[cfg(feature = "properties")]
60pub use properties::Properties;
61mod util;
62
63#[cfg(feature = "hcl")]
64mod hcl;
65#[cfg(feature = "hcl")]
66pub use hcl::Hcl;
67#[cfg(feature = "ason")]
68mod ason;
69#[cfg(feature = "ason")]
70pub use ason::Ason;
71#[cfg(feature = "yaml_ng")]
72mod yaml_ng;
73#[cfg(feature = "yaml_ng")]
74pub use yaml_ng::YamlNg;
75
76use config::{FileFormat, FileSourceFile, FileStoredFormat, Format, Map, Value};
77use std::fmt::Display;
78use std::path::Path;
79
80/// Error type for format parsing.
81///
82/// This error type is used when parsing by file extension fails.
83#[derive(Debug)]
84pub enum FormatError {
85    /// There was no file extension found.
86    NoExtensionFound,
87    /// The file format is not supported.
88    UnsupportedFormat(String),
89}
90
91impl Display for FormatError {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        match self {
94            FormatError::NoExtensionFound => write!(f, "No file extension found"),
95            FormatError::UnsupportedFormat(ext) => write!(f, "Unsupported file format: {}", ext),
96        }
97    }
98}
99
100impl Error for FormatError {}
101
102/// Parse a file by its extension.
103///
104/// This function will attempt to parse a file by its extension. If the extension is not supported,
105/// it will return an error.
106pub fn by_file_extension(file: &str) -> Result<config::File<FileSourceFile, FormatWrapper>, FormatError> {
107    let ext = Path::new(file);
108    let ext = ext.extension();
109    let ext = ext.ok_or(FormatError::NoExtensionFound)?;
110    let ext = ext.to_string_lossy();
111    let ext = ext.to_string();
112
113    match ext.as_str() {
114        // config native formats
115        #[cfg(feature = "toml")]
116        "toml" => Ok(config::File::new(file, config::FileFormat::Toml.into())),
117        #[cfg(feature = "json")]
118        "json" => Ok(config::File::new(file, config::FileFormat::Json.into())),
119        #[cfg(feature = "yaml")]
120        "yaml" | "yml" => Ok(config::File::new(file, config::FileFormat::Yaml.into())),
121        #[cfg(feature = "ron")]
122        "ron" => Ok(config::File::new(file, config::FileFormat::Ron.into())),
123        #[cfg(feature = "json5")]
124        "json5" => Ok(config::File::new(file, config::FileFormat::Json5.into())),
125        #[cfg(feature = "ini")]
126        "ini" => Ok(config::File::new(file, config::FileFormat::Ini.into())),
127
128        // unit structs
129        #[cfg(feature = "hjson")]
130        "hjson" => Ok(config::File::new(file, FormatWrapper::Hjson)),
131        #[cfg(feature = "properties")]
132        "properties" => Ok(config::File::new(file, FormatWrapper::Properties)),
133        #[cfg(feature = "yaml_ng")]
134        "yaml_ng" => Ok(config::File::new(file, FormatWrapper::YamlNg)),
135        #[cfg(feature = "hcl")]
136        "hcl" => Ok(config::File::new(file, FormatWrapper::Hcl)),
137        #[cfg(feature = "ason")]
138        "ason" => Ok(config::File::new(file, FormatWrapper::Ason)),
139        #[cfg(all(feature = "yaml_ng", not(feature = "yaml")))]
140        "yaml" | "yml" => Ok(config::File::new(file, FormatWrapper::YamlNg)),
141
142        _ => Err(FormatError::UnsupportedFormat(ext)),
143    }
144}
145
146#[derive(Debug, Clone)]
147pub enum FormatWrapper {
148    Enum(FileFormat),
149    #[cfg(feature = "ason")]
150    Ason,
151    #[cfg(feature = "hcl")]
152    Hcl,
153    #[cfg(feature = "hjson")]
154    Hjson,
155    #[cfg(feature = "properties")]
156    Properties,
157    #[cfg(feature = "yaml_ng")]
158    YamlNg,
159}
160
161impl Format for FormatWrapper {
162    fn parse(&self, uri: Option<&String>, text: &str) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
163        match self {
164            FormatWrapper::Enum(f) => f.parse(uri, text),
165            #[cfg(feature = "ason")]
166            FormatWrapper::Ason => Ason.parse(uri, text),
167            #[cfg(feature = "hcl")]
168            FormatWrapper::Hcl => Hcl.parse(uri, text),
169            #[cfg(feature = "hjson")]
170            FormatWrapper::Hjson => Hjson.parse(uri, text),
171            #[cfg(feature = "properties")]
172            FormatWrapper::Properties => Properties.parse(uri, text),
173            #[cfg(feature = "yaml_ng")]
174            FormatWrapper::YamlNg => YamlNg.parse(uri, text),
175        }
176    }
177}
178
179impl FileStoredFormat for FormatWrapper {
180    fn file_extensions(&self) -> &'static [&'static str] {
181        match self {
182            FormatWrapper::Enum(f) => f.file_extensions(),
183            #[cfg(feature = "ason")]
184            FormatWrapper::Ason => Ason.file_extensions(),
185            #[cfg(feature = "hcl")]
186            FormatWrapper::Hcl => Hcl.file_extensions(),
187            #[cfg(feature = "hjson")]
188            FormatWrapper::Hjson => Hjson.file_extensions(),
189            #[cfg(feature = "properties")]
190            FormatWrapper::Properties => Properties.file_extensions(),
191            #[cfg(feature = "yaml_ng")]
192            FormatWrapper::YamlNg => YamlNg.file_extensions(),
193        }
194    }
195}
196
197impl From<FileFormat> for FormatWrapper {
198    fn from(f: FileFormat) -> Self {
199        FormatWrapper::Enum(f)
200    }
201}
202
203#[cfg(feature = "ason")]
204impl From<Ason> for FormatWrapper {
205    fn from(_: Ason) -> Self {
206        FormatWrapper::Ason
207    }
208}
209
210#[cfg(feature = "hcl")]
211impl From<Hcl> for FormatWrapper {
212    fn from(_: Hcl) -> Self {
213        FormatWrapper::Hcl
214    }
215}
216
217#[cfg(feature = "hjson")]
218impl From<Hjson> for FormatWrapper {
219    fn from(_: Hjson) -> Self {
220        FormatWrapper::Hjson
221    }
222}
223
224#[cfg(feature = "properties")]
225impl From<Properties> for FormatWrapper {
226    fn from(_: Properties) -> Self {
227        FormatWrapper::Properties
228    }
229}
230
231#[cfg(feature = "yaml_ng")]
232impl From<YamlNg> for FormatWrapper {
233    fn from(_: YamlNg) -> Self {
234        FormatWrapper::YamlNg
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_by_file_extension_toml() {
244        #[cfg(feature = "toml")]
245        {
246            let result = by_file_extension("settings.toml");
247            assert!(result.is_ok());
248        }
249    }
250
251    #[test]
252    fn test_by_file_extension_json() {
253        #[cfg(feature = "json")]
254        {
255            let result = by_file_extension("settings.json");
256            assert!(result.is_ok());
257        }
258    }
259
260    #[test]
261    fn test_by_file_extension_yaml() {
262        #[cfg(feature = "yaml")]
263        {
264            let result = by_file_extension("settings.yaml");
265            assert!(result.is_ok());
266        }
267    }
268
269    #[test]
270    fn test_by_file_extension_unsupported() {
271        let result = by_file_extension("settings.unsupported");
272        assert!(matches!(result, Err(FormatError::UnsupportedFormat(_))));
273    }
274
275    #[test]
276    fn test_by_file_extension_no_extension() {
277        let result = by_file_extension("settings");
278        assert!(matches!(result, Err(FormatError::NoExtensionFound)));
279    }
280
281    #[test]
282    fn test_by_file_extension_properties() {
283        #[cfg(feature = "properties")]
284        {
285            let result = by_file_extension("settings.properties");
286            assert!(result.is_ok());
287        }
288    }
289
290    #[test]
291    fn test_by_file_extension_hjson() {
292        #[cfg(feature = "hjson")]
293        {
294            let result = by_file_extension("settings.hjson");
295            assert!(result.is_ok());
296        }
297    }
298
299    #[test]
300    fn test_by_file_extension_hcl() {
301        #[cfg(feature = "hcl")]
302        {
303            let result = by_file_extension("settings.hcl");
304            assert!(result.is_ok());
305        }
306    }
307
308    #[test]
309    fn test_by_file_extension_ason() {
310        #[cfg(feature = "ason")]
311        {
312            let result = by_file_extension("settings.ason");
313            assert!(result.is_ok());
314        }
315    }
316}