rsconfig/
lib.rs

1#![warn(missing_docs)]
2
3//! A simple configuration library that allows developers to quickly make configuration for their apps.
4
5/// Contains useful functions for importing from files
6pub mod files;
7
8use serde_json::Value;
9use yaml_rust::Yaml;
10
11use std::io;
12
13/// Represents a configuration struct that can be created from commandline arguments.
14/// ### Example Code
15/// ```rust
16/// use rsconfig::CommandlineConfig;
17///
18/// use std::env;
19///
20/// // our config class that we can expand upon to add different values
21/// // to expand upon it, simply add more fields and update the import function(s)
22/// #[derive(Debug)]
23/// struct TestConfig {
24///     test: bool
25/// }
26///
27/// impl CommandlineConfig for TestConfig {
28///     fn from_env_args(args: Vec<String>) -> Self {
29///         // check if commandline args contains --test
30///         Self { test: args.contains(&"--test".to_string()) }
31///     }
32/// }
33///
34///
35/// fn main() {
36///     // fetch commandline args
37///     let args: Vec<String> = env::args().collect();
38///
39///     // load config from commandline args
40///     let mut config = TestConfig::from_env_args(args);
41///
42///     // should output TestConfig { test: true } if --test is in the command
43///     // otherwise, it will print TestConfig { test: false }
44///     println!("{:?}", config);
45/// }
46/// ```
47pub trait CommandlineConfig {
48    /// Initialize a CommandlineConfig struct given the commandline arguments that the program was run with.
49    /// ### Example
50    /// ```rust
51    /// # use rsconfig::CommandlineConfig;
52    /// # struct T { test: bool }
53    /// # impl CommandlineConfig for T {
54    /// fn from_env_args(args: Vec<String>) -> Self {
55    ///     // check if commandline args contains --test
56    ///     Self { test: args.contains(&"--test".to_string()) }
57    /// }
58    /// # }
59    /// ```
60    fn from_env_args(args: Vec<String>) -> Self;
61}
62
63/// Represents a configuration struct that can be created from a YAML (YML) file.
64/// ### Example
65/// ```rust
66/// use yaml_rust;
67/// use rsconfig::YamlConfig;
68/// 
69/// use std::{fs, io::Result};
70///
71/// struct TestConfig {
72///     test: bool
73/// }
74///
75/// impl YamlConfig for TestConfig {
76///     fn from_yaml(yaml: Vec<yaml_rust::Yaml>) -> Self {
77///         // fetch "test" value of the first yaml document using yaml_rust crate
78///         // NOTE: this code is not error-safe, will panic if the correct file formatting is not used
79///         Self { test: *&yaml[0]["test"].as_bool().unwrap() }
80///     }
81///
82///     fn save_yaml(&self, path: &str) -> Result<()> {
83///         // might want to do this differently for config with more fields
84///         let mut data = "test: ".to_string();
85///
86///         // add the value to the file data
87///         data.push_str(self.test.to_string().as_str());
88///
89///         // write to the file
90///         fs::write(path, data).unwrap();
91///
92///         // return an Ok result
93///         // required because fs::write could fail, which would pass on an Err(()).
94///         Ok(())
95///     }
96/// }
97/// ```
98pub trait YamlConfig {
99    /// Initialize a YamlConfig struct given a list of Yaml documents from a parsed file.
100    /// ### Example
101    /// ```rust
102    /// # use yaml_rust;
103    /// # use rsconfig::YamlConfig;
104    /// # use std::io::Result;
105    /// 
106    /// # struct T { test: bool }
107    /// # impl YamlConfig for T {
108    /// fn from_yaml(yaml: Vec<yaml_rust::Yaml>) -> Self {
109    ///     // fetch "test" value of the first yaml document using yaml_rust crate
110    ///     // NOTE: this code is not error-safe, will panic if the file does not contain a bool named "test"
111    ///     Self { test: *&yaml[0]["test"].as_bool().unwrap() }
112    /// }
113    /// # fn save_yaml(&self, path: &str) -> Result<()> {Ok(())}
114    /// # }
115    /// ```
116    fn from_yaml(yaml: Vec<Yaml>) -> Self;
117
118    /// Save a YamlConfig struct's contents to a YAML (YML) file.
119    /// ### Example
120    /// ```rust
121    /// # use std::{fs, io::Result};
122    /// # use rsconfig::YamlConfig;
123    /// # use rust_yaml::Yaml;
124    /// 
125    /// # struct T { test: bool }
126    /// # impl YamlConfig for T {
127    /// # fn from_yaml(yaml: Vec<Yaml>) -> Self {Self{test: false}}
128    /// fn save_yaml(&self, path: &str) -> Result<()> {
129    ///         // might want to do this differently for config with more fields
130    ///
131    ///         let mut data = "test: ".to_string();
132    ///
133    ///         // add the value to the file data
134    ///         data.push_str(self.test.to_string().as_str());
135    ///
136    ///         // write to the file
137    ///         fs::write(path, data).unwrap();
138    ///
139    ///         // return an Ok result
140    ///         // required because fs::write could fail, which would pass on an Err(()).
141    ///         Ok(())
142    ///     }
143    /// # }
144    /// ```
145    fn save_yaml(&self, path: &str) -> io::Result<()>;
146}
147
148/// Represents a configuration struct that can be created from a JSON file.
149/// ### Example
150/// ```rust
151/// use serde_json;
152///
153/// use rsconfig::JsonConfig;
154/// 
155/// use std::fs;
156///
157/// #[derive(Debug)]
158/// struct TestConfig {
159///     test: bool
160/// }
161///
162/// impl JsonConfig for TestConfig {
163///     fn from_json(val: serde_json::Value) -> Self {
164///         // look for "test" val
165///         // NOTE: this code is not error-safe, will panic if the json does not contain a bool named "test"
166///         Self { test: val["test"].as_bool().unwrap() }
167///     }
168///
169///     fn save_json(&self, path: &str) -> io::Result<()> {
170///         // convert to json pretty format and save
171///         let mut m: Hashmap<&str, Value> = Hashmap::new();
172///         m.insert("test", &Value::from(self.test));
173///         let data = serde_json::to_string_pretty(m).unwrap();
174///         fs::write(path, data).unwrap();
175///
176///         Ok(())
177///     }
178/// }
179/// ```
180pub trait JsonConfig {
181    /// Initialize a JsonConfig struct from a given json value.
182    /// ### Example
183    /// ```rust
184    /// # use serde_json;
185    /// # use rsconfig::JsonConfig;
186    /// # use std::io::Result;
187    /// 
188    /// # struct T { test: bool }
189    /// # impl JsonConfig for T {
190    /// fn from_json(val: serde_json::Value) -> Self {
191    ///         // look for "test" val
192    ///         // NOTE: this code is not error-safe, will panic if the json does not contain a bool named "test"
193    ///         Self { test: val["test"].as_bool().unwrap() }
194    /// }
195    /// # fn save_json(&self, path: &str) -> Result<()> {Ok(())}
196    /// # }
197    /// ```
198    fn from_json(val: Value) -> Self;
199
200    /// Save a JsonConfig struct's contents to a JSON file.
201    /// ### Example
202    /// ```rust
203    /// # use std::{fs, io::Result, collections::HashMap};
204    /// # use serde_json::Value;
205    /// # use rsconfig::JsonConfig;
206    /// 
207    /// # struct T { test: bool }
208    /// # impl JsonConfig for T {
209    /// # fn from_json(val: Value) -> Self{Self{test: true}}
210    /// fn save_json(&self, path: &str) -> Result<()> {
211    ///         // convert to json pretty format and save
212    ///         let mut m: HashMap<&str, Value> = HashMap::new();
213    ///         m.insert("test", Value::from(self.test));
214    ///         let data = serde_json::to_string_pretty(&m).unwrap();
215    ///         fs::write(path, data).unwrap();
216    ///
217    ///         Ok(())
218    /// }
219    /// # }
220    /// ```
221    fn save_json(&self, path: &str) -> io::Result<()>;
222}
223
224/// Represents a configuration struct that can be created from a number of file types.
225/// ### Example
226/// ```rust
227/// use rsconfig::{YamlConfig, JsonConfig, FileConfig};
228///
229/// use serde_json;
230/// use yaml_rust;
231/// 
232/// // rsconfig-macros crate has a derive macro for this trait
233/// #[derive(Debug)]
234/// struct TestConfig {
235///     test: bool
236/// }
237///
238/// impl YamlConfig for TestConfig {
239///     fn from_yaml(yaml: Vec<yaml_rust::Yaml>) -> Self {
240///         Self { test: *&yaml[0]["test"].as_bool().unwrap() }
241///     }
242///
243///     fn save_yaml(&self, path: &str) -> Result<()> {
244///         let mut data = "test: ".to_string();
245///         data.push_str(self.test.to_string().as_str());
246///
247///         fs::write(path, data).unwrap();
248///
249///         Ok(())
250///     }
251/// }
252///
253/// impl JsonConfig for TestConfig {
254///     fn from_json(val: Value) -> Self {
255///         Self { test: val["test"].as_bool().unwrap() }
256///     }
257///
258///     fn save_json(&self, path: &str) -> io::Result<()> {
259///         // convert to json pretty format and save
260///         let mut m: Hashmap<&str, Value> = Hashmap::new();
261///         m.insert("test", &Value::from(self.test));
262///         let data = serde_json::to_string_pretty(m).unwrap();
263///         fs::write(path, data).unwrap();
264///
265///         Ok(())
266///     }
267/// }
268/// impl FileConfig for TestConfig {}
269/// ```
270
271pub trait FileConfig: YamlConfig + JsonConfig {}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use rsconfig_macros::*;
277
278    use std::{collections::HashMap, env, fs, io::Result};
279
280    // config class that we can expand upon to add different values
281    #[derive(Debug)]
282    struct TestConfig {
283        test: bool,
284    }
285
286    impl CommandlineConfig for TestConfig {
287        fn from_env_args(args: Vec<String>) -> Self {
288            Self {
289                test: args.contains(&"test".to_string()),
290            }
291        }
292    }
293
294    impl YamlConfig for TestConfig {
295        fn from_yaml(yaml: Vec<yaml_rust::Yaml>) -> Self {
296            Self {
297                test: *&yaml[0]["test"].as_bool().unwrap(),
298            }
299        }
300
301        fn save_yaml(&self, path: &str) -> Result<()> {
302            let mut data = "test: ".to_string();
303            data.push_str(self.test.to_string().as_str());
304
305            fs::write(path, data).unwrap();
306
307            Ok(())
308        }
309    }
310
311    impl JsonConfig for TestConfig {
312        fn from_json(val: Value) -> Self {
313            Self {
314                test: val["test"].as_bool().unwrap(),
315            }
316        }
317
318        fn save_json(&self, path: &str) -> io::Result<()> {
319            // convert to json pretty format and save
320            let mut m: HashMap<&str, Value> = HashMap::new();
321            m.insert("test", Value::from(self.test));
322            let data = serde_json::to_string_pretty(&m).unwrap();
323            fs::write(path, data).unwrap();
324
325            Ok(())
326        }
327    }
328    
329    impl FileConfig for TestConfig {}
330
331    // path to test files
332    const YAML_PATH: &str = "testing\\test.yml";
333    const JSON_PATH: &str = "testing\\test.json";
334
335    #[test]
336    fn args_test() {
337        // under normal test command (cargo test --package rsconfig --lib -- tests --nocapture),
338        // this will always create `config` with `test` as false
339
340        let args: Vec<String> = env::args().collect();
341
342        let mut config = TestConfig::from_env_args(args);
343
344        println!("{:?}", config);
345
346        change_config(&mut config);
347    }
348
349    #[test]
350    fn yaml_test() {
351        // loads from yaml; could use files::load_from_file(),
352        // but since we already know the filetype, it's better to just do this
353
354        let mut config: TestConfig = files::load_from_yaml(YAML_PATH);
355
356        println!("{:?}", config);
357
358        change_config(&mut config);
359    }
360
361    #[test]
362    fn json_test() {
363        // loads from json; could use files::load_from_file(),
364        // but since we already know the filetype, it's better to just do this
365
366        let mut config: TestConfig = files::load_from_json(JSON_PATH);
367
368        println!("{:?}", config);
369
370        change_config(&mut config);
371
372        // saving both yaml and json but idc don't want to copy one line of code
373        config.save_json(JSON_PATH).expect("Unable to save");
374    }
375
376    #[test]
377    fn file_test() {
378        let mut config: TestConfig =
379            files::load_from_file(YAML_PATH).expect("Unable to load from file");
380
381        println!("{:?}", config);
382
383        change_config(&mut config);
384    }
385
386    // swaps the `test` variable value and saves
387    fn change_config(config: &mut TestConfig) {
388        config.test = !config.test;
389
390        config.save_yaml(YAML_PATH).expect("Unable to save");
391
392        println!("{:?}", config);
393    }
394}