1use serde_derive::Deserialize;
2use toml::value;
3use tracing::Level;
4
5use drmem_api::{driver::DriverConfig, types::device::Path};
6
7#[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#[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 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 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 (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 let mut dirs = vec![String::from("./")];
189
190 if let Ok(home) = env::var("HOME") {
196 dirs.push(format!("{}/.", home))
197 }
198
199 dirs.push(String::from("/usr/local/etc/"));
202 dirs.push(String::from("/usr/pkg/etc/"));
203 dirs.push(String::from("/etc/"));
204
205 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 if let Ok(_) = toml::from_str::<Config>("") {
271 panic!("TOML parser accepted missing [[driver]] section")
272 }
273
274 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 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 #[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 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}