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};

/// Stores data unrelated to the config or state files: logs, history and so on.
///
/// Example of things to store:
/// * logs
/// * history
pub trait DataFile<E, T = Self>: for<'d> Deserialize<'d> + Serialize
where
    for<'d> T: Serialize + Deserialize<'d>,
    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_DATA_HOME`, which usually is `~/.local/share`.
    const HOME_PATH_ENV: &'static str = "XDG_DATA_HOME";
    /// Fallback path for `HOME_PATH_ENV`, assumes getting `$HOME` is valid.
    const HOME_PATH_FALLBACK: &'static str = ".local/share";
    /// Name of the environment variable which specifies a preference-ordered set of directories
    /// for files to be searched for.
    ///
    /// Defaults to `XDG_DATA_DIRS`, which usually is `/usr/local/share/:/usr/share/`.
    ///
    /// # Note
    /// You could override this at runtime if you wish to for example use `/etc` instead.
    const SYSTEM_PATHS_ENV: &'static str = "XDG_DATA_DIRS";
    /// Fallback paths for `SYSTEM_PATHS_ENV`.
    const SYSTEM_PATHS_FALLBACK: &'static str = "/usr/local/share/:/usr/share/";
    /// Name of the data file.
    const FILENAME: &'static str = "data";

    /// 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 [DataFile] to home.
    fn save(&self) -> Result<(), E> {
        Self::save_to_path(self, &Self::home(), true)
    }

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

    /// Save [DataFile] 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 [DataFile] from first available source in the following order:
    /// 1. `XDG_DATA_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 [DataFile] from home.
    fn load_from_home() -> Result<T, E> {
        Self::load_from_path(&Self::home())
    }

    /// Load first available [DataFile] 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 [DataFile]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 [DataFile]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 [DataFile]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 [DataFile]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 [DataFile] 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 Data;
    impl DataFile<std::io::Error> for Data {}

    #[test]
    fn home_path() {
        // regular test
        set_var("XDG_DATA_HOME", "/tmp/.local/share/");
        assert_eq!(
            Data::home(),
            PathBuf::from("/tmp/.local/share/docopticon/data")
        );
        // relative test
        set_var("XDG_DATA_HOME", "relative/dir");
        set_var("HOME", "/tmp/");
        assert_eq!(
            Data::home(),
            PathBuf::from("/tmp/.local/share/docopticon/data")
        );
    }

    #[test]
    fn system_paths() {
        assert!(Data::system().contains(&PathBuf::from("/usr/share/docopticon/data")));
        assert!(Data::system().contains(&PathBuf::from("/usr/local/share/docopticon/data")));
    }
}