drmem_config/
lib.rs

1use serde_derive::Deserialize;
2use toml::value;
3use tracing::Level;
4
5use drmem_api::{driver::DriverConfig, types::device::Path};
6
7// This module is defined when no backend is specified. There are no
8// config parameters for this backend.
9
10#[cfg(not(feature = "redis-backend"))]
11pub mod backend {
12    use serde_derive::Deserialize;
13
14    #[derive(Deserialize, Clone)]
15    pub struct Config {}
16
17    impl Config {
18        pub const fn new() -> Config {
19            Config {}
20        }
21    }
22
23    pub static DEF: Config = Config::new();
24}
25
26// This module is defined when the REDIS backend is specified. It
27// provides configuration parameters that need to be provided in the
28// TOML file to help configure the REDIS support.
29
30#[cfg(feature = "redis-backend")]
31pub mod backend {
32    use serde_derive::Deserialize;
33    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
34
35    #[derive(Deserialize, Clone)]
36    pub struct Config {
37        pub addr: Option<SocketAddr>,
38        pub dbn: Option<i64>,
39    }
40
41    impl<'a> Config {
42        pub const fn new() -> Config {
43            Config {
44                addr: None,
45                dbn: None,
46            }
47        }
48
49        pub fn get_addr(&'a self) -> SocketAddr {
50            self.addr.unwrap_or_else(|| {
51                SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 6379)
52            })
53        }
54
55        #[cfg(debug_assertions)]
56        pub fn get_dbn(&self) -> i64 {
57            self.dbn.unwrap_or(1)
58        }
59        #[cfg(not(debug_assertions))]
60        pub fn get_dbn(&self) -> i64 {
61            self.dbn.unwrap_or(0)
62        }
63    }
64
65    pub static DEF: Config = Config::new();
66}
67
68#[derive(Deserialize)]
69pub struct Config {
70    log_level: Option<String>,
71    pub backend: Option<backend::Config>,
72    pub driver: Vec<Driver>,
73}
74
75impl<'a> Config {
76    pub fn get_log_level(&self) -> Level {
77        let v = self.log_level.as_deref().unwrap_or("warn");
78
79        match v {
80            "info" => Level::INFO,
81            "debug" => Level::DEBUG,
82            "trace" => Level::TRACE,
83            _ => Level::WARN,
84        }
85    }
86
87    pub fn get_backend(&'a self) -> &'a backend::Config {
88        self.backend.as_ref().unwrap_or(&backend::DEF)
89    }
90}
91
92impl Default for Config {
93    fn default() -> Self {
94        Config {
95            log_level: None,
96            backend: Some(backend::Config::new()),
97            driver: vec![],
98        }
99    }
100}
101
102#[derive(Deserialize)]
103pub struct Driver {
104    pub name: String,
105    pub prefix: Path,
106    pub max_history: Option<usize>,
107    pub cfg: Option<DriverConfig>,
108}
109
110fn from_cmdline(mut cfg: Config) -> (bool, Config) {
111    use clap::{App, Arg};
112
113    // Define the command line arguments.
114
115    let matches = App::new("DrMem Mini Control System")
116        .version("0.1")
117        .about("A small, yet capable, control system.")
118        .arg(
119            Arg::with_name("config")
120                .short("c")
121                .long("config")
122                .value_name("FILE")
123                .help("Specifies the configuration file")
124                .takes_value(true),
125        )
126        .arg(
127            Arg::with_name("verbose")
128                .short("v")
129                .long("verbose")
130                .multiple(true)
131                .help("Sets verbosity of log; can be used more than once")
132                .takes_value(false),
133        )
134        .arg(
135            Arg::with_name("print_cfg")
136                .long("print-config")
137                .help("Displays the configuration and exits")
138                .takes_value(false),
139        )
140        .get_matches();
141
142    // The number of '-v' options determines the log level.
143
144    match matches.occurrences_of("verbose") {
145        0 => (),
146        1 => cfg.log_level = Some(String::from("info")),
147        2 => cfg.log_level = Some(String::from("debug")),
148        _ => cfg.log_level = Some(String::from("trace")),
149    };
150
151    // Return the config built from the command line and a flag
152    // indicating the user wants the final configuration displayed.
153
154    (matches.is_present("print_cfg"), cfg)
155}
156
157fn parse_config(path: &str, contents: &str) -> Option<Config> {
158    match toml::from_str(contents) {
159        Ok(cfg) => Some(cfg),
160        Err(e) => {
161            print!("ERROR: {},\n       ignoring {}\n", e, path);
162            None
163        }
164    }
165}
166
167async fn from_file(path: &str) -> Option<Config> {
168    use tokio::fs;
169
170    if let Ok(contents) = fs::read(path).await {
171        let contents = String::from_utf8_lossy(&contents);
172
173        parse_config(path, &contents)
174    } else {
175        None
176    }
177}
178
179async fn find_cfg() -> Config {
180    use std::env;
181
182    const CFG_FILE: &str = "drmem.toml";
183
184    // Create a vector of directories that could contain a
185    // configuration file. The directories will be searched in their
186    // order within the vector.
187
188    let mut dirs = vec![String::from("./")];
189
190    // If the user has `HOME` defined, append their home directory to
191    // the search path. Note the end of the path has a period. This is
192    // done so the file will be named `.drmem.toml` in the home
193    // directory. (Kind of hack-y, I know.)
194
195    if let Ok(home) = env::var("HOME") {
196        dirs.push(format!("{}/.", home))
197    }
198
199    // Add other, common configuration areas.
200
201    dirs.push(String::from("/usr/local/etc/"));
202    dirs.push(String::from("/usr/pkg/etc/"));
203    dirs.push(String::from("/etc/"));
204
205    // Iterate through the directories. The first file that is found
206    // and can be parsed is used as the configuration.
207
208    for dir in dirs {
209        let file = format!("{}{}", &dir, CFG_FILE);
210
211        if let Some(cfg) = from_file(&file).await {
212            return cfg;
213        }
214    }
215    Config::default()
216}
217
218fn dump_config(cfg: &Config) {
219    println!("Configuration:");
220    println!("    log level: {}\n", cfg.get_log_level());
221
222    #[cfg(feature = "simple-backend")]
223    {
224        println!("Using SIMPLE backend -- no configuration for it.");
225    }
226
227    #[cfg(feature = "redis-backend")]
228    {
229        println!("Using REDIS for storage:");
230        println!("    address: {}", &cfg.get_backend().get_addr());
231        println!("    db #: {}\n", cfg.get_backend().get_dbn());
232    }
233
234    println!("Driver configuration:");
235    if !cfg.driver.is_empty() {
236        for ii in &cfg.driver {
237            print!(
238                "    name: {}, prefix: {}, cfg: {:?}",
239                &ii.name,
240                &ii.prefix,
241                ii.cfg.as_ref().unwrap_or(&value::Table::new())
242            )
243        }
244        println!();
245    } else {
246        println!("    No drivers specified.");
247    }
248}
249
250#[tracing::instrument(name = "loading config")]
251pub async fn get() -> Option<Config> {
252    let cfg = find_cfg().await;
253    let (print_cfg, cfg) = from_cmdline(cfg);
254
255    if print_cfg {
256        dump_config(&cfg);
257        None
258    } else {
259        Some(cfg)
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    fn test_defaults() {
268        // Verify that missing the [[driver]] section fails.
269
270        if let Ok(_) = toml::from_str::<Config>("") {
271            panic!("TOML parser accepted missing [[driver]] section")
272        }
273
274        // Verify that the [[driver]] section needs an entry to be
275        // defined..
276
277        assert!(
278            toml::from_str::<Config>(
279                r#"
280[[driver]]
281"#,
282            )
283            .is_err(),
284            "TOML parser accepted empty [[driver]] section"
285        );
286
287        // Verify a missing [backend] results in a properly defined
288        // default.
289
290        match toml::from_str::<Config>(
291            r#"
292[[driver]]
293name = "none"
294prefix = "null"
295"#,
296        ) {
297            Ok(cfg) => {
298                let def_cfg = Config::default();
299
300                #[cfg(feature = "redis-backend")]
301                {
302                    assert_eq!(
303                        cfg.get_backend().get_addr(),
304                        def_cfg.get_backend().get_addr()
305                    );
306                    assert_eq!(
307                        cfg.get_backend().get_dbn(),
308                        def_cfg.get_backend().get_dbn()
309                    );
310                }
311
312                assert_eq!(cfg.log_level, def_cfg.log_level);
313                assert_eq!(cfg.driver.len(), 1)
314            }
315            Err(e) => panic!("TOML parse error: {}", e),
316        }
317
318        // Verify the [backend] section can handle only one field at a
319        // time.
320
321        #[cfg(feature = "redis-backend")]
322        match toml::from_str::<Config>(
323            r#"
324[backend]
325addr = "192.168.1.1:6000"
326
327[[driver]]
328name = "none"
329prefix = "null"
330"#,
331        ) {
332            Ok(cfg) => {
333                let def_cfg = Config::default();
334
335                assert_eq!(
336                    cfg.get_backend().get_addr(),
337                    "192.168.1.1:6000".parse().unwrap()
338                );
339                assert_eq!(
340                    cfg.get_backend().get_dbn(),
341                    def_cfg.get_backend().get_dbn()
342                );
343            }
344            Err(e) => panic!("TOML parse error: {}", e),
345        }
346
347        #[cfg(feature = "redis-backend")]
348        match toml::from_str::<Config>(
349            r#"
350[backend]
351dbn = 3
352
353[[driver]]
354name = "none"
355prefix = "null"
356"#,
357        ) {
358            Ok(cfg) => {
359                let def_cfg = Config::default();
360
361                assert_eq!(
362                    cfg.get_backend().get_addr(),
363                    def_cfg.get_backend().get_addr()
364                );
365                assert_eq!(cfg.get_backend().get_dbn(), 3);
366            }
367            Err(e) => panic!("TOML parse error: {}", e),
368        }
369
370        // Verify the log_level can be set.
371
372        match toml::from_str::<Config>(
373            r#"
374log_level = "trace"
375
376[[driver]]
377name = "none"
378prefix = "null"
379"#,
380        ) {
381            Ok(cfg) => assert_eq!(cfg.get_log_level(), Level::TRACE),
382            Err(e) => panic!("TOML parse error: {}", e),
383        }
384        match toml::from_str::<Config>(
385            r#"
386log_level = "debug"
387
388[[driver]]
389name = "none"
390prefix = "null"
391"#,
392        ) {
393            Ok(cfg) => assert_eq!(cfg.get_log_level(), Level::DEBUG),
394            Err(e) => panic!("TOML parse error: {}", e),
395        }
396        match toml::from_str::<Config>(
397            r#"
398log_level = "info"
399
400[[driver]]
401name = "none"
402prefix = "null"
403"#,
404        ) {
405            Ok(cfg) => assert_eq!(cfg.get_log_level(), Level::INFO),
406            Err(e) => panic!("TOML parse error: {}", e),
407        }
408        match toml::from_str::<Config>(
409            r#"
410log_level = "warn"
411
412[[driver]]
413name = "none"
414prefix = "null"
415"#,
416        ) {
417            Ok(cfg) => assert_eq!(cfg.get_log_level(), Level::WARN),
418            Err(e) => panic!("TOML parse error: {}", e),
419        }
420
421        assert!(
422            toml::from_str::<Config>(
423                r#"
424[[driver]]
425name = "none"
426"#,
427            )
428            .is_err(),
429            "TOML parser accepted [[driver]] section with missing prefix"
430        );
431
432        assert!(
433            toml::from_str::<Config>(
434                r#"
435[[driver]]
436prefix = "null"
437"#,
438            )
439            .is_err(),
440            "TOML parser accepted [[driver]] section with missing name"
441        );
442
443        assert!(
444            toml::from_str::<Config>(
445                r#"
446[[driver]]
447name = "none"
448prefix = "null"
449max_history = false
450"#,
451            )
452            .is_err(),
453            "TOML parser accepted [[driver]] section with bad max_history"
454        );
455
456        match toml::from_str::<Config>(
457            r#"
458[[driver]]
459name = "none"
460prefix = "null"
461"#,
462        ) {
463            Ok(cfg) => {
464                assert_eq!(cfg.driver.len(), 1);
465
466                assert_eq!(cfg.driver[0].name, "none");
467                assert_eq!(
468                    cfg.driver[0].prefix,
469                    "null".parse::<Path>().unwrap()
470                );
471                assert_eq!(cfg.driver[0].max_history, None);
472            }
473            Err(e) => panic!("TOML parse error: {}", e),
474        }
475
476        match toml::from_str::<Config>(
477            r#"
478[[driver]]
479name = "none"
480prefix = "null"
481max_history = 10000
482"#,
483        ) {
484            Ok(cfg) => {
485                assert_eq!(cfg.driver.len(), 1);
486
487                assert_eq!(cfg.driver[0].name, "none");
488                assert_eq!(
489                    cfg.driver[0].prefix,
490                    "null".parse::<Path>().unwrap()
491                );
492                assert_eq!(cfg.driver[0].max_history, Some(10000));
493            }
494            Err(e) => panic!("TOML parse error: {}", e),
495        }
496    }
497
498    #[tokio::test]
499    async fn test_config() {
500        test_defaults()
501    }
502}