dango/system/
application.rs

1//
2// Copyright © 2022, Oleg Lelenkov
3// License: BSD 3-Clause
4// Authors: Oleg Lelenkov
5//
6
7use std::collections::HashSet;
8use std::path::{Path, PathBuf};
9
10use clap::{Arg, Command};
11use semver::Version;
12use thiserror::Error;
13use uninode::{loaders::UniNodeLoadError, UniNode};
14
15#[derive(Debug, Error)]
16pub enum AppError {
17    #[error(transparent)]
18    SemVer(#[from] semver::Error),
19    #[error(transparent)]
20    Load(#[from] UniNodeLoadError),
21}
22
23pub trait Application {
24    fn new(name: &str, version: &str, summary: &str) -> Result<Self, AppError>
25    where
26        Self: Sized;
27
28    fn name(&self) -> &str;
29
30    fn version(&self) -> &Version;
31
32    fn summary(&self) -> &str;
33
34    fn add_default_config<P: AsRef<Path>>(&mut self, path: P);
35
36    fn config(&self) -> &UniNode;
37}
38
39type ApplicationBootstrap = Box<dyn FnOnce(&UniNode) -> anyhow::Result<()> + 'static>;
40
41pub struct DangoApplication {
42    name: String,
43    summary: String,
44    version: Version,
45    default_configs: Vec<PathBuf>,
46    config: UniNode,
47    bootstraps: Vec<ApplicationBootstrap>,
48}
49
50impl Application for DangoApplication {
51    fn new(name: &str, version: &str, summary: &str) -> Result<Self, AppError> {
52        Ok(Self {
53            name: name.to_string(),
54            summary: summary.to_string(),
55            version: Version::parse(version)?,
56            default_configs: Vec::new(),
57            config: UniNode::Null,
58            bootstraps: Vec::new(),
59        })
60    }
61
62    fn name(&self) -> &str {
63        &self.name
64    }
65
66    fn version(&self) -> &Version {
67        &self.version
68    }
69
70    fn summary(&self) -> &str {
71        &self.summary
72    }
73
74    fn config(&self) -> &UniNode {
75        &self.config
76    }
77
78    fn add_default_config<P: AsRef<Path>>(&mut self, path: P) {
79        self.default_configs.push(path.as_ref().to_path_buf());
80    }
81}
82
83impl DangoApplication {
84    pub fn register_bootstrap<B>(&mut self, bootstrap: B)
85    where
86        B: FnOnce(&UniNode) -> anyhow::Result<()> + 'static,
87    {
88        self.bootstraps.push(Box::new(bootstrap));
89    }
90
91    pub fn start(&mut self) -> anyhow::Result<()> {
92        let version = self.version().to_string();
93        let app = Command::new(self.name())
94            .version(version.as_ref())
95            .about(self.summary())
96            .arg(
97                Arg::new("config")
98                    .short('c')
99                    .long("config")
100                    .value_name("FILE")
101                    .help("Sets a custom config file")
102                    .multiple_values(true)
103                    .forbid_empty_values(true)
104                    .takes_value(true),
105            );
106        let matches = app.get_matches();
107
108        fn load_configs(config_files: &[PathBuf]) -> Result<UniNode, UniNodeLoadError> {
109            let config_files: HashSet<&PathBuf> = config_files.iter().collect();
110            let mut config = UniNode::empty_object();
111            for c_file in config_files {
112                config.merge(UniNode::load(c_file)?);
113            }
114            Ok(config)
115        }
116
117        self.config = if matches.occurrences_of("config") == 0 {
118            load_configs(&self.default_configs)?
119        } else {
120            let opt_files = matches
121                .values_of("config")
122                .unwrap()
123                .map(PathBuf::from)
124                .collect::<Vec<_>>();
125            load_configs(&opt_files)?
126        };
127
128        if let Some(logger_cfg) = self.config.find("logger") {
129            super::logging::logger_init(logger_cfg).unwrap();
130        }
131
132        for bootstrap in self.bootstraps.drain(..) {
133            (bootstrap)(&self.config)?;
134        }
135
136        Ok(())
137    }
138}