dinglebit_config/
lib.rs

1//! Simplified configuration management.
2//!
3//! This configuration package isn't meant to solve all of the
4//! configuration needs you'll ever need. Instead, it provides a trait
5//! for a config and then allows for including other confugiration
6//! systems as needed.
7//!
8//! A simple environment config and file config are provided.
9//!
10//! ```
11//! use dinglebit_config::{Config, Environment, MultiConfig, Simple};
12//! use std::collections::HashMap;
13//!
14//! fn main() {
15//!     let mut m = HashMap::new();
16//!     m.insert("foo", "bar");
17//!     let cfg = MultiConfig::new(vec![
18//!         Box::new(m),
19//!         Box::new(Simple::from_str("baz=foo").unwrap()),
20//!     ]);
21//!
22//!     assert_eq!(cfg.must_get("foo"), "bar".to_string());
23//!     assert_eq!(cfg.must_get("baz"), "foo".to_string());
24//!     assert!(cfg.get("bar").is_none());
25//! }
26
27use std::collections::HashMap;
28
29pub mod env;
30pub mod multi;
31pub mod simple;
32
33pub use env::Environment;
34pub use multi::MultiConfig;
35pub use simple::{Error, Simple};
36
37/// The main trait for this package. This should be implemented if you
38/// want to use this package with your configuration systems.
39pub trait Config {
40    /// Returns the value associated with the given key.
41    fn get(&self, key: &str) -> Option<String>;
42
43    /// Similar to `get` but panics if there is no value.
44    fn must_get(&self, key: &str) -> String {
45        self.get(key).unwrap()
46    }
47
48    /// Get the value as a string or panics if one isn't found.
49    fn string(&self, key: &str) -> String {
50        self.get(key).unwrap()
51    }
52
53    /// Get the value as an integer or panics if one isn't found or
54    /// cannot be parsed.
55    fn int(&self, key: &str) -> i64 {
56        self.must_get(key).parse::<i64>().unwrap()
57    }
58
59    /// Get the value as a float or panics if one isn't found or
60    /// cannot be parsed.
61    fn float(&self, key: &str) -> f64 {
62        self.must_get(key).parse::<f64>().unwrap()
63    }
64
65    /// Get the value as a bool or panics if one isn't found or cannot
66    /// be parsed. The following case-insensitive values are considered
67    /// true: t, true, 1, y, yes. All other values are considered
68    /// false.
69    fn bool(&self, key: &str) -> bool {
70        match self.must_get(key).to_lowercase().as_str() {
71            "t" => true,
72            "true" => true,
73            "1" => true,
74            "y" => true,
75            "yes" => true,
76            _ => false,
77        }
78    }
79
80    /// Get the value as a duration or panics if one isn't found or
81    /// can't be parsed. Thre doesn't appear to be a parsing function
82    /// for a duration, so it attempts to convert to an integer and use
83    /// that as the number of seconds.
84    fn duration(&self, key: &str) -> chrono::Duration {
85        // There doesn't seem to be a parse function for
86        // chrono::Duration. We just assume i64 seconds.
87        chrono::Duration::seconds(self.int(key))
88    }
89
90    /// Get the value as a duration or panics if one isn't found or it
91    /// can't be parsed. It uses RFC339 to parse it.
92    fn datetime(&self, key: &str) -> chrono::DateTime<chrono::Utc> {
93        chrono::DateTime::<chrono::Utc>::from_utc(
94            chrono::DateTime::parse_from_rfc3339(self.must_get(key).as_str())
95                .unwrap()
96                .naive_utc(),
97            chrono::Utc,
98        )
99    }
100
101    /// Get a list or panics if one isn't found. The list should be a
102    /// comma-delimited list surrouned by brackets (e.g. [1, 2, 3] =>
103    /// vec!["1", "2", "3"].
104    fn list(&self, key: &str) -> Vec<String> {
105        let s = self.must_get(key);
106        let s = s.trim_matches(|c| c == '[' || c == ']' || char::is_whitespace(c));
107        s.split(',')
108            .map(|p| p.trim().to_string())
109            .collect::<Vec<String>>()
110    }
111
112    /// Get a map or panics if one isn't found. The list should be a
113    /// comma-delimited list surrouned by braces with key/value pairs
114    /// associated with => (e.g. {a=>1, b=>2, c=>3} => ((a,1), (b,2),
115    /// (c,3))).
116    fn map(&self, key: &str) -> HashMap<String, String> {
117        let s = self.must_get(key);
118        let s = s.trim_matches(|c| c == '{' || c == '}' || char::is_whitespace(c));
119        s.split(',')
120            .map(|p| {
121                let parts = p.split("=>").map(|k| k.trim()).collect::<Vec<&str>>();
122                if parts.len() < 2 {
123                    (parts[0], "")
124                } else {
125                    (parts[0], parts[1])
126                }
127            })
128            .map(|(k, v)| (k.to_string(), v.to_string()))
129            .collect::<HashMap<String, String>>()
130    }
131}
132
133/// Create a config from a list of key/value pairs.
134#[macro_export]
135macro_rules! default_config(
136    { $($key:expr => $value:expr),+ } => {
137        {
138            let mut m: ::std::collections::HashMap<&str, &str> = ::std::collections::HashMap::new();
139            $(
140                m.insert($key, $value);
141            )+
142            Box::new(m)
143        }
144     };
145);
146
147impl Config for HashMap<&str, &str> {
148    fn get(&self, key: &str) -> Option<String> {
149        match self.get(key) {
150            None => None,
151            Some(v) => Some(v.to_string()),
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use crate::*;
159    use chrono::{TimeZone, Utc};
160    use lazy_static::lazy_static;
161    use std::collections::HashMap;
162
163    #[test]
164    fn default() {
165        let config = default_config! {
166            "foo" => "bar",
167            "bar" => "baz",
168            "baz" => "foo"
169        };
170        assert_eq!(config.string("foo"), "bar".to_string());
171    }
172
173    #[test]
174    fn hash_map() {
175        use std::collections::HashMap;
176        let mut m = HashMap::new();
177        m.insert("foo", "bar");
178        assert_eq!(m.must_get("foo"), "bar".to_string());
179        assert!(m.get("bar").is_none());
180    }
181
182    lazy_static! {
183        static ref HASHMAP: HashMap<&'static str, &'static str> = {
184            let mut m = HashMap::new();
185            m.insert("foo", "bar");
186            m.insert("int", "100");
187            m.insert("float", "-2.4");
188            m.insert("bool", "t");
189            m.insert("duration", "50");
190            m.insert("datetime", "2015-05-15T05:05:05+00:00");
191            m.insert("list", "[1, 2, 3]");
192            m.insert("map", "{a=>1, b=>2, c=>3}");
193            m
194        };
195    }
196
197    macro_rules! test_gets {
198        ($(($name:ident, $test:expr): $exp:expr,)*) => {
199            $(
200                #[test]
201                fn $name() {
202                    assert_eq!(
203                        $test,
204                        $exp
205                    );
206                }
207
208            )*
209
210        }
211    }
212
213    test_gets! {
214        (string, HASHMAP.string("foo")): "bar".to_string(),
215        (int, HASHMAP.int("int")): 100,
216        (float, HASHMAP.float("float")): -2.4,
217        (bool, HASHMAP.bool("bool")): true,
218        (duration, HASHMAP.duration("duration")): chrono::Duration::seconds(50),
219        (datetime, HASHMAP.datetime("datetime")): Utc.ymd(2015, 5, 15).and_hms(5, 5, 5),
220        (list, HASHMAP.list("list")): vec!["1", "2", "3"],
221        (map, HASHMAP.map("map")): {
222            let mut m: HashMap<String, String> = HashMap::new();
223            m.insert("a".to_string(), "1".to_string());
224            m.insert("b".to_string(), "2".to_string());
225            m.insert("c".to_string(), "3".to_string());
226            m
227        },
228    }
229}