tier 0.1.17

Rust configuration library for layered TOML, env, and CLI settings
Documentation
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use crate::ConfigError;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WatchTargetKind {
    File,
    Directory,
}

#[derive(Debug, Clone)]
pub(super) struct WatchTarget {
    path: PathBuf,
    kind: WatchTargetKind,
    watch_root: PathBuf,
    recursive: bool,
}

impl WatchTarget {
    pub(super) fn matches_event_path(&self, path: &Path) -> bool {
        match self.kind {
            WatchTargetKind::File => path == self.path,
            WatchTargetKind::Directory => path == self.path || path.starts_with(&self.path),
        }
    }
}

#[derive(Debug, Clone)]
pub(super) struct WatchRegistration {
    pub(super) root: PathBuf,
    pub(super) recursive: bool,
}

pub(super) fn prepare_watch_targets(paths: Vec<PathBuf>) -> Result<Vec<WatchTarget>, ConfigError> {
    let mut targets = Vec::new();
    for path in paths {
        targets.push(watch_target_for_path(absolutize_path(&path)?)?);
    }
    Ok(targets)
}

fn watch_target_for_path(path: PathBuf) -> Result<WatchTarget, ConfigError> {
    if path.exists() && path.is_dir() {
        return Ok(WatchTarget {
            watch_root: path.clone(),
            path,
            kind: WatchTargetKind::Directory,
            recursive: true,
        });
    }

    let parent = path
        .parent()
        .map(Path::to_path_buf)
        .unwrap_or_else(|| path.clone());
    let (watch_root, recursive) = watch_root_for_parent(&parent)?;

    Ok(WatchTarget {
        path,
        kind: WatchTargetKind::File,
        watch_root,
        recursive,
    })
}

fn watch_root_for_parent(parent: &Path) -> Result<(PathBuf, bool), ConfigError> {
    if parent.exists() {
        Ok((parent.to_path_buf(), false))
    } else if let Some(root) = nearest_existing_ancestor(parent) {
        Ok((root, true))
    } else {
        Ok((std::env::current_dir().map_err(map_watch_io_error)?, true))
    }
}

pub(super) fn collect_watch_registrations(targets: &[WatchTarget]) -> Vec<WatchRegistration> {
    let mut registrations = BTreeMap::<PathBuf, bool>::new();
    for target in targets {
        registrations
            .entry(target.watch_root.clone())
            .and_modify(|recursive| *recursive |= target.recursive)
            .or_insert(target.recursive);
    }

    registrations
        .into_iter()
        .map(|(root, recursive)| WatchRegistration { root, recursive })
        .collect()
}

fn nearest_existing_ancestor(path: &Path) -> Option<PathBuf> {
    let mut current = Some(path);
    while let Some(candidate) = current {
        if candidate.exists() {
            return Some(candidate.to_path_buf());
        }
        current = candidate.parent();
    }
    None
}

fn absolutize_path(path: &Path) -> Result<PathBuf, ConfigError> {
    if path.is_absolute() {
        Ok(path.to_path_buf())
    } else {
        Ok(std::env::current_dir()
            .map_err(map_watch_io_error)?
            .join(path))
    }
}

pub(super) fn absolutize_event_path(path: &Path) -> Option<PathBuf> {
    if path.is_absolute() {
        Some(path.to_path_buf())
    } else {
        std::env::current_dir().ok().map(|cwd| cwd.join(path))
    }
}

fn map_watch_io_error(error: std::io::Error) -> ConfigError {
    ConfigError::Watch {
        message: error.to_string(),
    }
}