dango 0.1.1

Application skeleton
Documentation
//
// Copyright © 2022, Oleg Lelenkov
// License: BSD 3-Clause
// Authors: Oleg Lelenkov
//

use std::collections::HashSet;
use std::path::{Path, PathBuf};

use clap::{Arg, Command};
use semver::Version;
use thiserror::Error;
use uninode::{loaders::UniNodeLoadError, UniNode};

#[derive(Debug, Error)]
pub enum AppError {
    #[error(transparent)]
    SemVer(#[from] semver::Error),
    #[error(transparent)]
    Load(#[from] UniNodeLoadError),
}

pub trait Application {
    fn new(name: &str, version: &str, summary: &str) -> Result<Self, AppError>
    where
        Self: Sized;

    fn name(&self) -> &str;

    fn version(&self) -> &Version;

    fn summary(&self) -> &str;

    fn add_default_config<P: AsRef<Path>>(&mut self, path: P);

    fn config(&self) -> &UniNode;
}

type ApplicationBootstrap = Box<dyn FnOnce(&UniNode) -> anyhow::Result<()> + 'static>;

pub struct DangoApplication {
    name: String,
    summary: String,
    version: Version,
    default_configs: Vec<PathBuf>,
    config: UniNode,
    bootstraps: Vec<ApplicationBootstrap>,
}

impl Application for DangoApplication {
    fn new(name: &str, version: &str, summary: &str) -> Result<Self, AppError> {
        Ok(Self {
            name: name.to_string(),
            summary: summary.to_string(),
            version: Version::parse(version)?,
            default_configs: Vec::new(),
            config: UniNode::Null,
            bootstraps: Vec::new(),
        })
    }

    fn name(&self) -> &str {
        &self.name
    }

    fn version(&self) -> &Version {
        &self.version
    }

    fn summary(&self) -> &str {
        &self.summary
    }

    fn config(&self) -> &UniNode {
        &self.config
    }

    fn add_default_config<P: AsRef<Path>>(&mut self, path: P) {
        self.default_configs.push(path.as_ref().to_path_buf());
    }
}

impl DangoApplication {
    pub fn register_bootstrap<B>(&mut self, bootstrap: B)
    where
        B: FnOnce(&UniNode) -> anyhow::Result<()> + 'static,
    {
        self.bootstraps.push(Box::new(bootstrap));
    }

    pub fn start(&mut self) -> anyhow::Result<()> {
        let version = self.version().to_string();
        let app = Command::new(self.name())
            .version(version.as_ref())
            .about(self.summary())
            .arg(
                Arg::new("config")
                    .short('c')
                    .long("config")
                    .value_name("FILE")
                    .help("Sets a custom config file")
                    .multiple_values(true)
                    .forbid_empty_values(true)
                    .takes_value(true),
            );
        let matches = app.get_matches();

        fn load_configs(config_files: &[PathBuf]) -> Result<UniNode, UniNodeLoadError> {
            let config_files: HashSet<&PathBuf> = config_files.iter().collect();
            let mut config = UniNode::empty_object();
            for c_file in config_files {
                config.merge(UniNode::load(c_file)?);
            }
            Ok(config)
        }

        self.config = if matches.occurrences_of("config") == 0 {
            load_configs(&self.default_configs)?
        } else {
            let opt_files = matches
                .values_of("config")
                .unwrap()
                .map(PathBuf::from)
                .collect::<Vec<_>>();
            load_configs(&opt_files)?
        };

        if let Some(logger_cfg) = self.config.find("logger") {
            super::logging::logger_init(logger_cfg).unwrap();
        }

        for bootstrap in self.bootstraps.drain(..) {
            (bootstrap)(&self.config)?;
        }

        Ok(())
    }
}