config-load 0.1.1

Conditional configuration loader for Rust applications that use the `config` crate.
Documentation
use std::env;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};

use either::Either;

use crate::location::Location;

pub enum FileLocation {
    FirstSome(Option<PathBuf>),
}

impl FileLocation {
    pub fn first_some_path() -> Self {
        Self::FirstSome(None)
    }

    pub fn from_file(mut self, file_path: Option<PathBuf>) -> Self {
        if let Some(file_path) = file_path {
            if file_path.is_relative() {
                return self.from_cwd(file_path);
            } else {
                match &mut self {
                    FileLocation::FirstSome(path) => {
                        if path.is_none() {
                            path.replace(file_path);
                        }
                    },
                }
            }
        }
        self
    }

    pub fn from_file_exists(mut self, file_path: Option<PathBuf>) -> Self {
        if let Some(file_path) = file_path {
            if file_path.is_relative() {
                return self.from_cwd_exists(file_path);
            } else {
                match &mut self {
                    FileLocation::FirstSome(path) => {
                        if path.is_none() {
                            *path = existing_file(Some(file_path), Option::<&Path>::None);
                        }
                    },
                }
            }
        }
        self
    }

    pub fn from_env(mut self, env_var: impl AsRef<OsStr>) -> Self {
        match &mut self {
            FileLocation::FirstSome(path) => {
                if path.is_none() {
                    *path = env::var(env_var).ok().map(Into::into);
                }
            },
        }
        self
    }

    pub fn from_env_exists(mut self, env_var: impl AsRef<OsStr>) -> Self {
        match &mut self {
            FileLocation::FirstSome(path) => {
                if path.is_none() {
                    *path = existing_file(env::var(env_var).ok().map(Into::into), Option::<&Path>::None);
                }
            },
        }
        self
    }

    pub fn from_home(mut self, relative_path: impl AsRef<Path>) -> Self {
        match &mut self {
            FileLocation::FirstSome(path) => {
                if path.is_none() {
                    *path = env::home_dir().map(|home| home.join(relative_path));
                }
            },
        }
        self
    }

    pub fn from_home_exists(mut self, relative_path: impl AsRef<Path>) -> Self {
        match &mut self {
            FileLocation::FirstSome(path) => {
                if path.is_none() {
                    *path = existing_file(env::home_dir(), Some(relative_path));
                }
            },
        }
        self
    }

    pub fn from_cwd(mut self, relative_path: impl AsRef<Path>) -> Self {
        match &mut self {
            FileLocation::FirstSome(path) => {
                if path.is_none() {
                    *path = env::current_dir().ok().map(|cwd| cwd.join(relative_path));
                }
            },
        }
        self
    }

    pub fn from_cwd_exists(mut self, relative_path: impl AsRef<Path>) -> Self {
        match &mut self {
            FileLocation::FirstSome(path) => {
                if path.is_none() {
                    *path = existing_file(env::current_dir().ok(), Some(relative_path));
                }
            },
        }
        self
    }

    pub fn from_cwd_and_parents_exists(mut self, relative_path: impl AsRef<Path>) -> Self {
        match &mut self {
            FileLocation::FirstSome(path) => {
                if path.is_none() {
                    *path = env::current_dir()
                        .ok()
                        .and_then(|cwd| find_existing_file_in_dir_and_parents(cwd, relative_path));
                }
            },
        }
        self
    }
}

impl Location for FileLocation {
    fn try_into_path(self) -> Either<PathBuf, Self>
    where
        Self: Sized,
    {
        match self {
            Self::FirstSome(path) => path
                .map(Either::Left)
                .unwrap_or_else(|| Either::Right(Self::first_some_path())),
        }
    }
}

fn existing_file(root_path: Option<PathBuf>, relative_path: Option<impl AsRef<Path>>) -> Option<PathBuf> {
    root_path.and_then(|root| {
        let path = if let Some(relative_path) = relative_path {
            root.join(relative_path)
        } else {
            root
        };
        path.is_file().then_some(path)
    })
}

fn find_existing_file_in_dir_and_parents(dir: impl AsRef<Path>, relative_path: impl AsRef<Path>) -> Option<PathBuf> {
    let mut current_dir = dir.as_ref();
    loop {
        let path = current_dir.join(relative_path.as_ref());
        if path.is_file() {
            break Some(path);
        } else {
            current_dir = current_dir.parent()?;
        }
    }
}