aocli 0.0.9

Advent of Code helper CLI
use std::fmt;

use crate::display;

#[derive(thiserror::Error, Debug)]
pub enum AocError {
    #[error("invalid directory for command `{0}`")]
    CommandDir(String),
    #[error("invalid year directory")]
    YearDir,
    #[error("invalid day directory")]
    DayDir,
    #[error("must be an integer at least 2015")]
    YearArg,
    #[error("must be an integer between 1 and 25")]
    DayArg,
    #[error("unexpected argument `{0}`")]
    ExtraArg(String),
    #[error("invalid value for argument <{0}>: `{1}`")]
    InvalidArg(Arg, String),
    #[error("missing argument <{0}>")]
    MissingArg(Arg),
    #[error("must be `1` or `2`")]
    Part,
    #[error("year directory not found: {0}")]
    MissingYearDir(String),
    #[error("day directory not found: {0}")]
    MissingDayDir(String),
    #[error("path already exists: {0}")]
    PathExists(String),
    #[error("failed to read file system")]
    FileRead,
    #[error("failed to write to file system")]
    FileWrite,
    #[error("failed to initialise git repository")]
    GitInit,
    #[error("file empty at {0}")]
    EmptyFile(String),
    #[error("no file found at {0}")]
    NoFile(String),
    #[error("no puzzle input")]
    NoInput,
    #[error("network error")]
    Network,
    #[error("invalid session cookie")]
    Session,
    #[error("server response error")]
    Response,
    #[error("webpage not available")]
    PageAvailable,
    #[error("failed to open browser")]
    Browser,
    #[error("no days to run")]
    NoDays,
    #[error("invalid term in argument <DAYS>: `{0}`")]
    InvalidTerm(String),
    #[error("term must be in the form X, -X, X..Y or -X..Y")]
    TermFormat,
    #[error("day must be between 1 and 25")]
    TermDayRange,
    #[error("invalid input name")]
    InputName,
    #[error("not a valid directory name")]
    InputNameFormat,
    #[error("failed to read workspace Cargo.toml")]
    WorkspaceCargo,
    #[error("failed to add workspace member")]
    WorkspaceMember,
}

pub type Result<T, E = Error> = core::result::Result<T, E>;

pub struct Error {
    error: Vec<String>,
    usage: Vec<String>,
}

impl<E: ToString> From<E> for Error {
    fn from(value: E) -> Self {
        Self {
            error: vec![value.to_string()],
            usage: Vec::new(),
        }
    }
}

impl Error {
    pub fn context<C: ToString>(mut self, context: C) -> Self {
        self.error.push(context.to_string());
        self
    }

    pub fn with_context<C: ToString, F: Fn() -> C>(mut self, context: F) -> Self {
        self.error.push(context().to_string());
        self
    }

    pub fn usage<U: ToString>(mut self, usage: U) -> Self {
        self.usage.push(usage.to_string());
        self
    }

    pub fn with_usage<U: ToString, F: Fn() -> U>(mut self, usage: F) -> Self {
        self.usage.push(usage().to_string());
        self
    }

    pub fn usages<U: ToString>(mut self, usages: impl IntoIterator<Item = U>) -> Self {
        for usage in usages {
            self = self.usage(usage);
        }
        self
    }
}

pub trait Context<T, E: Into<Error>> {
    fn context<C: ToString>(self, context: C) -> Result<T, Error>;
    fn with_context<C: ToString, F: Fn() -> C>(self, context: F) -> Result<T, Error>;
    fn usage<U: ToString>(self, usage: U) -> Result<T, Error>;
    fn with_usage<U: ToString, F: Fn() -> U>(self, usage: F) -> Result<T, Error>;
    fn usages<U: ToString>(self, usages: impl IntoIterator<Item = U>) -> Result<T, Error>;
}

impl<T, E: Into<Error>> Context<T, E> for Result<T, E> {
    fn context<C: ToString>(self, context: C) -> Result<T, Error> {
        self.map_err(|e| e.into().context(context))
    }

    fn with_context<C: ToString, F: Fn() -> C>(self, context: F) -> Result<T, Error> {
        self.map_err(|e| e.into().with_context(context))
    }

    fn usage<U: ToString>(self, usage: U) -> Result<T, Error> {
        self.map_err(|e| e.into().usage(usage))
    }

    fn with_usage<U: ToString, F: Fn() -> U>(self, usage: F) -> Result<T, Error> {
        self.map_err(|e| e.into().with_usage(usage))
    }

    fn usages<U: ToString>(self, usages: impl IntoIterator<Item = U>) -> Result<T, Error> {
        self.map_err(|e| e.into().usages(usages))
    }
}

pub trait ErrorDisplayer {
    type Output;
    fn display_err(self) -> Option<Self::Output>;
}

impl<T, E: Into<Error>> ErrorDisplayer for Result<T, E> {
    type Output = T;

    fn display_err(self) -> Option<Self::Output> {
        match self {
            Ok(x) => Some(x),
            Err(e) => {
                let e: Error = e.into();
                e.display_err();
                None
            }
        }
    }
}

impl ErrorDisplayer for Error {
    type Output = ();

    fn display_err(self) -> Option<Self::Output> {
        let mut errors = self.error.into_iter().rev();
        display::error(errors.next().unwrap());
        for error in errors {
            display::cause(error);
        }
        let mut usages = self.usage.into_iter();
        if let Some(usage) = usages.next() {
            display::usage(usage);
            for usage in usages {
                display::or(usage);
            }
        }
        None
    }
}

pub trait ToErr {
    fn err<T>(self) -> Result<T>;
    fn error(self) -> Error;
}

impl<E: Into<Error>> ToErr for E {
    fn err<T>(self) -> Result<T> {
        Err(self.into())
    }

    fn error(self) -> Error {
        self.into()
    }
}

#[derive(Debug, Clone, Copy)]
pub enum Arg {
    Year,
    Day,
    Part,
    Input,
}

impl fmt::Display for Arg {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::Year => "YEAR",
                Self::Day => "DAY",
                Self::Part => "PART",
                Self::Input => "INPUT",
            }
        )
    }
}