preftool-file 0.2.0

Configuration library for CLI tools/servers.
extern crate preftool;
extern crate preftool_dirs;

use preftool::*;
use preftool_dirs::*;
use std::collections::HashSet;
use std::fs::File;
use std::io::{Error, ErrorKind, Read, Result};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};

pub trait ConfigurationFileFormat: Clone + Sized {
  type Provider: ConfigurationProvider + 'static;

  fn get_config<R: Read>(reader: R) -> Result<Self::Provider>;
  fn default_suffixes() -> &'static [&'static str];

  fn file<P: AsRef<Path>>(path: P) -> ConfigurationFileSource<Self> {
    ConfigurationFileSource::new(PathBuf::from(path.as_ref()), false)
  }

  fn files<P: AsRef<Path>, I: IntoIterator<Item = P> + Clone + 'static, S: SearchPaths + Clone>(
    suffixes: I,
    search_paths: S,
    app_name: String,
  ) -> ConfigurationFileSources<S, Self> {
    ConfigurationFileSources::new(suffixes, search_paths, app_name)
  }

  fn default_config_files<S: SearchPaths + Clone>(
    search_paths: S,
    app_name: String,
  ) -> ConfigurationFileSources<S, Self> {
    Self::files(Self::default_suffixes(), search_paths, app_name)
  }
}

#[derive(Clone)]
pub struct ConfigurationFileSource<F: ConfigurationFileFormat> {
  format: PhantomData<*const F>,
  path: PathBuf,
  required: bool,
}

impl<F: ConfigurationFileFormat> ConfigurationFileSource<F> {
  fn new(path: PathBuf, required: bool) -> Self {
    Self {
      format: PhantomData,
      path,
      required,
    }
  }
}

impl<F> ConfigurationSource for ConfigurationFileSource<F>
where
  F: ConfigurationFileFormat,
{
  fn build<B: ConfigurationBuilder>(self, mut builder: B) -> Result<B> {
    if self.required && !self.path.is_file() {
      return Err(Error::new(
        ErrorKind::NotFound,
        format!("Required config file {:#?} not found.", &self.path),
      ));
    }

    if self.path.is_file() {
      let f = File::open(&self.path)?;
      builder = builder.push_provider(F::get_config(f)?);
    }

    Ok(builder)
  }
}

#[derive(Clone)]
pub struct ConfigurationFileSources<S, F>
where
  S: SearchPaths + Clone,
  F: ConfigurationFileFormat,
{
  format: PhantomData<*const F>,
  search_paths: S,
  suffixes: Vec<PathBuf>,
  app_name: String,
}

impl<S, F> ConfigurationFileSources<S, F>
where
  S: SearchPaths + Clone,
  F: ConfigurationFileFormat,
{
  fn new<P: AsRef<Path>, I: IntoIterator<Item = P> + Clone + 'static>(
    suffixes: I,
    search_paths: S,
    app_name: String,
  ) -> Self {
    let suffixes = suffixes
      .into_iter()
      .map(|name| PathBuf::from(name.as_ref()))
      .collect();
    Self {
      format: PhantomData,
      search_paths,
      suffixes,
      app_name,
    }
  }
}

impl<S, F> ConfigurationSource for ConfigurationFileSources<S, F>
where
  S: SearchPaths + Clone + 'static,
  F: ConfigurationFileFormat + 'static,
{
  fn build<B: ConfigurationBuilder>(self, mut builder: B) -> Result<B> {
    let search_paths = self.search_paths;
    let suffixes = self.suffixes;
    let app_name = self.app_name;
    for file in search_paths.files(move |p| {
      let mut files = HashSet::new();
      if p.is_bin() || p.is_cwd() || p.is_user() {
        for suffix in suffixes.iter() {
          files.insert(
            p.path()
              .join(format!("{}rc{}", app_name, suffix.to_str().unwrap())),
          );
          files.insert(
            p.path()
              .join(format!(".{}rc{}", app_name, suffix.to_str().unwrap())),
          );
        }
      }

      if p.is_app() {
        for suffix in suffixes.iter() {
          files.insert(p.path().join(format!("config{}", suffix.to_str().unwrap())));
        }
      }

      files.into_iter()
    }) {
      let f = File::open(file)?;
      builder = builder.push_provider(F::get_config(f)?);
    }

    Ok(builder)
  }
}