dinglebit_config/
simple.rs

1//! Extremely simplistic configuration from a file or string.
2
3use std::collections::HashMap;
4use std::fs::read_to_string;
5
6use crate::Config;
7
8#[derive(Debug, PartialEq)]
9pub struct Simple {
10    values: HashMap<String, String>,
11}
12
13#[derive(Debug, PartialEq)]
14pub enum Error {
15    File(String),
16    InvalidKeyValuePair,
17}
18
19fn parse_line(line: &str) -> Result<Option<(String, String)>, Error> {
20    // Cleanup and check for comments
21    let line = line.trim();
22    if line.starts_with("#") {
23        return Ok(None);
24    } else if line.len() < 1 {
25        return Ok(None);
26    }
27
28    // Split by the equal sign. Expect exactly two.
29    let parts: Vec<&str> = line.splitn(2, "=").collect();
30    if parts.len() < 2 {
31        return Err(Error::InvalidKeyValuePair);
32    }
33
34    Ok(Some((
35        parts[0].trim().to_string(),
36        parts[1].trim().to_string(),
37    )))
38}
39
40fn parse(s: &str) -> Result<HashMap<String, String>, Error> {
41    let mut values = HashMap::new();
42
43    for line in s.split("\n") {
44        match parse_line(&line) {
45            Err(e) => return Err(e),
46            Ok(v) => match v {
47                None => continue,
48                Some(s) => {
49                    values.insert(s.0, s.1);
50                }
51            },
52        }
53    }
54
55    Ok(values)
56}
57
58impl Simple {
59    /// Create a new configuration from the given string. This is an
60    /// extremely simple configuration format. It expects key/value
61    /// pairs separated by an equal sign. Whitespace is trimmed from
62    /// the line as well as each key/value. Lines that begin with `#`
63    /// are considered a comment and empty lines are ignored. Thre is
64    /// no hierarchy or anything. If you want to provide some
65    /// yourself, you can use dot-notation. For example:
66    ///
67    /// ```ini
68    /// ## i am a comment
69    /// mongo.uri = mongodb://localhost/
70    /// mongo.db  = test
71    /// ```
72    pub fn from_str(s: &str) -> Result<Self, Error> {
73        Ok(Self { values: parse(s)? })
74    }
75
76    /// Similar to `from_str` except that the given path is used as
77    /// the contents for the string to parse.
78    pub fn from_file(path: &str) -> Result<Self, Error> {
79        let file = match read_to_string(path) {
80            Ok(s) => s,
81            Err(e) => return Err(Error::File(e.to_string())),
82        };
83        Ok(Self {
84            values: parse(&file)?,
85        })
86    }
87}
88
89impl Config for Simple {
90    fn get(&self, key: &str) -> Option<String> {
91        match self.values.get(key) {
92            Some(value) => Some(value.to_string()),
93            None => None,
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use crate::simple::{parse_line, Error, Simple};
101    use crate::Config;
102
103    use std::array::IntoIter;
104    use std::collections::HashMap;
105    use std::iter::FromIterator;
106
107    #[test]
108    fn test_parse_line() {
109        let tests =
110            HashMap::<&str, Result<Option<(String, String)>, Error>>::from_iter(IntoIter::new([
111                ("     # comment   ", Ok(None)),
112                ("  test", Err(Error::InvalidKeyValuePair)),
113                (
114                    "  foo    =    bar    ",
115                    Ok(Some(("foo".to_string(), "bar".to_string()))),
116                ),
117            ]));
118        tests.iter().for_each(|(k, v)| {
119            assert_eq!(parse_line(k), *v);
120        });
121    }
122
123    #[test]
124    fn test_file() {
125        // not found
126        let exp: Result<Simple, Error> = Err(Error::File(
127            "No such file or directory (os error 2)".to_string(),
128        ));
129        assert_eq!(Simple::from_file("/i/hope/i/do/not/exist.cfg"), exp);
130
131        // our example config
132        let cfg = match Simple::from_file("example.cfg") {
133            Err(e) => panic!("reading 'example.cfg': {:?}", e),
134            Ok(cfg) => cfg,
135        };
136        assert_eq!(cfg.get("foo"), Some("bar".to_string()));
137        assert_eq!(cfg.get("list"), Some("one, two, three".to_string()));
138    }
139}