preftool-dirs 0.2.0

Configuration library for CLI tools/servers.
#[macro_use]
extern crate bitflags;
#[macro_use]
extern crate cfg_if;
extern crate dirs_sys;
#[macro_use]
extern crate slog;
extern crate slog_stdlog;

use preftool::*;
use slog::Drain;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;

macro_rules! vec_opt {
  (__count) => {
    0
  };

  (__count $_fst:expr, $($value:expr,)*) => {
    1 + vec_opt!(__count $($value:expr,)*)
  };

  (__add $vec:ident) => {

  };

  (__add $vec:ident $val:expr, $($value:expr,)*) => {
    match $val {
      None => (),
      Some(v) => {
        $vec.push(v);
      }
    };
    vec_opt!(__add $vec $($value,)*);
  };

  (
    $($value:expr),+$(,)?
  ) => {{
    let mut vec = Vec::with_capacity(vec_opt!(__count $($value,)+));
    vec_opt!(__add vec $($value,)+);
    vec
  }};
}

cfg_if! {
    if #[cfg(target_os = "windows")] {
        mod win;
        use win as sys;
    } else if #[cfg(target_os = "macos")] {
        mod mac;
        use mac as sys;
    } else {
        mod lin;
        use lin as sys;
    }
}

bitflags! {
  pub struct ConfigPathFlags: u32 {
    const NONE = 0;
    const APP_CONFIG_PATH = 1 << 0;
    const USER_CONFIG_PATH = 1 << 1;
    const BIN_CONFIG_PATH = 1 << 2;
    const CWD_CONFIG_PATH = 1 << 3;
  }
}

#[derive(Debug, Clone)]
pub struct ConfigPath {
  path: PathBuf,
  flags: ConfigPathFlags,
}

impl ConfigPath {
  pub fn is_app(&self) -> bool {
    self.flags.contains(ConfigPathFlags::APP_CONFIG_PATH)
  }

  pub fn is_user(&self) -> bool {
    self.flags.contains(ConfigPathFlags::USER_CONFIG_PATH)
  }

  pub fn is_bin(&self) -> bool {
    self.flags.contains(ConfigPathFlags::BIN_CONFIG_PATH)
  }

  pub fn is_cwd(&self) -> bool {
    self.flags.contains(ConfigPathFlags::CWD_CONFIG_PATH)
  }

  pub fn path(&self) -> &Path {
    &self.path
  }
}

impl ConfigPath {
  pub fn new(path: PathBuf, flags: ConfigPathFlags) -> Self {
    Self { path, flags }
  }
}

pub trait SearchPaths: LogOwner {
  type Iter: Iterator<Item = ConfigPath> + 'static;

  fn iter(&self) -> Self::Iter;
}

pub trait SearchPathsExt: SearchPaths {
  fn files<P, I, F>(&self, get_names: F) -> Box<dyn Iterator<Item = PathBuf>>
  where
    P: AsRef<Path>,
    I: IntoIterator<Item = P>,
    I::IntoIter: 'static,
    F: 'static + Fn(ConfigPath) -> I,
  {
    let logger = self.logger().clone();
    Box::new(
      self
        .iter()
        .flat_map(move |dir| {
          let logger = logger.clone();
          get_names(dir).into_iter().filter_map(move |p| {
            let p = p.as_ref();
            if p.is_file() {
              trace!(logger, "Found config file {path:?}", path = p.to_str());
              Some(PathBuf::from(p))
            } else {
              None
            }
          })
        })
        .fuse(),
    )
  }
}

impl<T: SearchPaths> SearchPathsExt for T {}

#[derive(Debug, Clone)]
pub struct ConfigDirs {
  logger: slog::Logger,
  dirs: Vec<PathBuf>,
  app_dir: PathBuf,
  home_dir: Option<PathBuf>,
  cwd_dir: PathBuf,
}

impl ConfigDirs {
  pub fn new<S: AsRef<str>>(
    application: S,
    logger: Option<slog::Logger>,
  ) -> std::io::Result<ConfigDirs> {
    let ret = sys::config_dirs(
      application.as_ref(),
      logger.unwrap_or_else(|| slog::Logger::root(slog_stdlog::StdLog.fuse(), o!())),
    )?;

    debug!(ret.logger, "Config dirs"; &ret);
    Ok(ret)
  }
}

impl LogOwner for ConfigDirs {
  #[inline]
  fn logger(&self) -> &slog::Logger {
    &self.logger
  }
}

impl SearchPaths for ConfigDirs {
  type Iter = std::vec::IntoIter<ConfigPath>;

  fn iter(&self) -> Self::Iter {
    let mut dirs = Vec::new();
    let mut indexes = HashMap::new();

    fn add<'p: 'c, 'c>(
      logger: &slog::Logger,
      indexes: &'c mut HashMap<&'p Path, usize>,
      dirs: &'c mut Vec<ConfigPath>,
      path: &'p Path,
      flags: ConfigPathFlags,
    ) {
      match indexes.get(path) {
        None => {
          let index = dirs.len();
          indexes.insert(path, index);
          trace!(
            logger,
            "Added config search path {path:?}",
            path = path.to_str()
          );
          dirs.push(ConfigPath::new(path.to_owned(), flags));
        }

        Some(i) => {
          let item = dirs.get_mut(*i).unwrap();
          item.flags |= flags;
        }
      }
    };

    fn add_out<'p: 'c, 'c>(
      logger: &slog::Logger,
      indexes: &'c mut HashMap<&'p Path, usize>,
      dirs: &'c mut Vec<ConfigPath>,
      path: &'p Path,
      flags: ConfigPathFlags,
    ) {
      add(logger, indexes, dirs, path, flags);
      match path.parent() {
        None => (),
        Some(p) => {
          add_out(logger, indexes, dirs, p, flags);
        }
      }
    };

    for dir in self.dirs.iter() {
      add(
        &self.logger,
        &mut indexes,
        &mut dirs,
        dir,
        ConfigPathFlags::APP_CONFIG_PATH,
      );
    }

    if let Some(home_dir) = &self.home_dir {
      add(
        &self.logger,
        &mut indexes,
        &mut dirs,
        home_dir,
        ConfigPathFlags::USER_CONFIG_PATH,
      );
    }

    add_out(
      &self.logger,
      &mut indexes,
      &mut dirs,
      &self.app_dir,
      ConfigPathFlags::BIN_CONFIG_PATH,
    );
    add_out(
      &self.logger,
      &mut indexes,
      &mut dirs,
      &self.cwd_dir,
      ConfigPathFlags::CWD_CONFIG_PATH,
    );

    dirs.into_iter()
  }
}

impl slog::KV for ConfigDirs {
  fn serialize(&self, rec: &slog::Record, serializer: &mut slog::Serializer) -> slog::Result {
    slog::Value::serialize(
      &log::PathArg::from(&self.app_dir),
      rec,
      "app_dir",
      serializer,
    )?;
    slog::Value::serialize(&log::PathArg::from(&self.cwd_dir), rec, "cwd", serializer)?;
    slog::Value::serialize(
      &self.home_dir.as_ref().map(log::PathArg::from),
      rec,
      "home_dir",
      serializer,
    )?;

    Ok(())
  }
}

mod log {
  use slog::{Key, Record, Result, Serializer, Value};
  use std::path::Path;

  pub struct PathArg<'a>(&'a Path);

  impl<'a> PathArg<'a> {
    fn new<P: AsRef<Path>>(path: &'a P) -> Self {
      Self(path.as_ref())
    }
  }

  impl<'a, P: AsRef<Path>> From<&'a P> for PathArg<'a> {
    fn from(path: &'a P) -> Self {
      Self::new(path)
    }
  }

  impl<'a> Value for PathArg<'a> {
    fn serialize(&self, rec: &Record, key: Key, serializer: &mut Serializer) -> Result {
      Value::serialize(&self.0.display(), rec, key, serializer)
    }
  }
}