docopticon 0.1.2

An argument-parser based on the obligatory help-text
Documentation
use super::{
    home_path, load_all_from_path, load_from_path, load_home_env, save_to_path, system_paths,
};

use std::{
    env,
    path::{Path, PathBuf},
    str::Utf8Error,
    string::String,
};

use serde::{de, ser, Deserialize, Serialize};

/// User-specified configuration settings file. Stores how the application should behave
/// when it runs.
pub trait ConfigFile<E, T = Self>: for<'c> Deserialize<'c> + Serialize
where
    for<'c> T: Serialize + Deserialize<'c>,
    E: std::error::Error + std::convert::From<std::io::Error>,
{
    /// Name of the directory to-be-used for the application's storage. If no directory is desired, set this to "" (empty string).
    ///
    /// Defaults to "`$CARGO_CRATE_NAME`" which is the name of the crate at compile-time.
    const APP_DIR_NAME: &'static str = env!("CARGO_CRATE_NAME");
    /// Name of the directory to-be-used for more complicated setups, like organizing multiple
    /// configuration files into less of a mess, or allowing overrides to be separated from the
    /// actual config file.
    ///
    /// Defaults to "`$CARGO_CRATE_NAME`.d" which is the name of the crate at compile-time.
    const EXTRA_DIR: &'static str = concat!(env!("CARGO_CRATE_NAME"), ".d");
    /// Name of the environment variable which specifies the home path directory,
    ///
    /// Defaults to `$XDG_CONFIG_HOME`, which usually is `~/.config/`.
    const HOME_PATH_ENV: &'static str = "XDG_CONFIG_HOME";
    /// Fallback path for `HOME_PATH_ENV`, assumes getting `$HOME` is valid.
    const HOME_PATH_FALLBACK: &'static str = ".config";
    /// Name of the environment variable which specifies a preference-ordered set of directories
    /// for files to be searched for.
    ///
    /// Defaults to `XDG_CONFIG_DIRS`, which usually is `/etc/xdg/`.
    ///
    /// # Note
    /// You could override this at runtime if you wish to for example use `/etc` instead.
    const SYSTEM_PATHS_ENV: &'static str = "XDG_CONFIG_DIRS";
    /// Fallback paths for `SYSTEM_PATHS_ENV`.
    const SYSTEM_PATHS_FALLBACK: &'static str = "/etc/xdg";
    /// Name of the config file.
    const FILENAME: &'static str = "config";

    /// Fully constructed home-path.
    fn home() -> PathBuf {
        home_path(
            Self::HOME_PATH_ENV,
            Self::HOME_PATH_FALLBACK,
            Self::APP_DIR_NAME,
            Self::FILENAME,
        )
    }

    /// Fully constructed list of system paths.
    fn system() -> Vec<PathBuf> {
        system_paths(
            Self::SYSTEM_PATHS_ENV,
            Self::SYSTEM_PATHS_FALLBACK,
            Self::APP_DIR_NAME,
            Self::FILENAME,
        )
    }

    /// Save [ConfigFile] to home.
    fn save(&self) -> Result<(), E> {
        self.save_to_path(&Self::home(), true)
    }

    /// Save [ConfigFile] to first available system path.
    fn save_to_system(&self) -> Result<(), E> {
        self.save_to_path(
            &Self::system()
                .first()
                .expect("no paths specified in trait-constants"),
            true,
        )
    }

    /// Save [ConfigFile] to a custom-specified path, optionally creating all dirs needed on the way.
    fn save_to_path(&self, path: impl AsRef<Path>, create_dirs: bool) -> Result<(), E> {
        save_to_path(self, path.as_ref(), create_dirs)
    }

    /// Load [ConfigFile] from first available source in the following order:
    /// 1. `XDG_CONFIG_HOME/{Self::APP_DIR_NAME}/{Self::FILENAME}`
    /// 2. `$HOME/.config/{Self::APP_DIR_NAME}/{Self::FILENAME}`
    /// 3. `XDG_CONFIG_DIRS{{/Self::APP_DIR_NAME}/{Self::FILENAME}}`
    /// 4. `/etx/xdg/{Self::APP_DIR_NAME}/`
    fn load() -> Result<T, E> {
        match Self::load_from_home() {
            Ok(s) => Ok(s),
            Err(_) => Self::load_first_from_system(),
        }
    }

    /// Load [ConfigFile] from home.
    fn load_from_home() -> Result<T, E> {
        Self::load_from_path(&Self::home())
    }

    /// Load first available [ConfigFile] from system paths.
    fn load_first_from_system() -> Result<T, E> {
        Self::load_from_path(
            &Self::system()
                .first()
                .expect("no paths specified in trait-constants"),
        )
    }

    /// Load all available [ConfigFile]s from system paths.
    fn load_from_system() -> Result<Vec<T>, E> {
        let paths = Self::system();
        let mut loaded = Vec::with_capacity(paths.len());
        for path in paths {
            loaded.push(Self::load_from_path(path)?);
        }
        Ok(loaded)
    }

    /// Load all available [ConfigFile]s.
    fn load_all() -> Result<Vec<T>, E> {
        let paths = Self::system();
        let mut loaded = Vec::with_capacity(paths.len() + 1);
        loaded.push(Self::load_from_home()?);
        loaded.append(&mut Self::load_from_system()?);
        Ok(loaded)
    }

    /// Load all available [ConfigFile]s from home's [Self::EXTRA_DIR].
    fn load_from_home_extra_dirs() -> Result<Vec<T>, E> {
        load_all_from_path(&Self::home())
    }

    /// Load all available [ConfigFile]s from all [Self::EXTRA_DIR]s in [Self::SYSTEM_PATHS_ENV].
    fn load_from_system_extra_dirs() -> Result<Vec<T>, E> {
        let paths = Self::system();
        let mut loaded = Vec::with_capacity(paths.len());
        for path in paths {
            loaded.append(&mut load_all_from_path(path)?);
        }
        Ok(loaded)
    }

    /// Load [ConfigFile] from a given path.
    fn load_from_path(path: impl AsRef<Path>) -> Result<T, E> {
        load_from_path(path.as_ref())
    }
}

mod tests {
    use super::*;
    use std::env::set_var;

    use serde::{Deserialize, Serialize};

    #[derive(Serialize, Deserialize)]
    struct Config;
    impl ConfigFile<std::io::Error> for Config {}

    #[test]
    fn home_path() {
        // regular test
        set_var("XDG_CONFIG_HOME", "/tmp/.config/");
        assert_eq!(
            Config::home(),
            PathBuf::from("/tmp/.config/docopticon/config")
        );
        // relative test
        set_var("XDG_CONFIG_HOME", "relative/dir");
        set_var("HOME", "/tmp/");
        assert_eq!(
            Config::home(),
            PathBuf::from("/tmp/.config/docopticon/config")
        );
    }

    #[test]
    fn system_paths() {
        assert_eq!(
            Config::system(),
            vec!(PathBuf::from("/etc/xdg/docopticon/config"))
        );
    }
}