standout-input 9.0.0

Declarative input collection for CLI applications
Documentation
use clap::ArgMatches;

use crate::InputError;
use crate::InputSources;

pub trait InputCollector<T>: Send + Sync {
    fn name(&self) -> &'static str;

    fn is_available(&self, matches: &ArgMatches) -> bool;

    fn collect(&self, matches: &ArgMatches) -> Result<Option<T>, InputError>;

    fn bind_sources(&self, _sources: &InputSources) -> Option<Box<dyn InputCollector<T>>> {
        None
    }

    fn validate(&self, _value: &T) -> Result<(), String> {
        Ok(())
    }

    fn can_retry(&self) -> bool {
        false
    }
}

#[cfg(any(feature = "editor", feature = "simple-prompts", feature = "inquire"))]
pub(crate) fn prompt_value_from<T>(
    collector: &dyn InputCollector<T>,
    sources: &InputSources,
) -> Result<T, InputError> {
    let bound = collector.bind_sources(sources);
    let source: &dyn InputCollector<T> = match bound.as_ref() {
        Some(bound) => bound.as_ref(),
        None => collector,
    };
    let matches = empty_matches();
    if !source.is_available(matches) {
        return Err(InputError::NoInput);
    }
    source.collect(matches)?.ok_or(InputError::NoInput)
}

#[cfg(any(feature = "editor", feature = "simple-prompts", feature = "inquire"))]
pub(crate) fn empty_matches() -> &'static ArgMatches {
    use std::sync::OnceLock;
    static MATCHES: OnceLock<ArgMatches> = OnceLock::new();
    MATCHES.get_or_init(|| {
        clap::Command::new("__standout_input_prompt__")
            .no_binary_name(true)
            .try_get_matches_from(std::iter::empty::<&str>())
            .expect("empty command always parses with no args")
    })
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedInput<T> {
    pub value: T,
    pub source: InputSourceKind,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputSourceKind {
    Arg,
    Flag,
    File,
    Stdin,
    Env,
    Clipboard,
    Editor,
    Prompt,
    Default,
}

impl std::fmt::Display for InputSourceKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Arg => write!(f, "argument"),
            Self::Flag => write!(f, "flag"),
            Self::File => write!(f, "file"),
            Self::Stdin => write!(f, "stdin"),
            Self::Env => write!(f, "environment variable"),
            Self::Clipboard => write!(f, "clipboard"),
            Self::Editor => write!(f, "editor"),
            Self::Prompt => write!(f, "prompt"),
            Self::Default => write!(f, "default"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn source_kind_display() {
        assert_eq!(InputSourceKind::Arg.to_string(), "argument");
        assert_eq!(InputSourceKind::File.to_string(), "file");
        assert_eq!(InputSourceKind::Stdin.to_string(), "stdin");
        assert_eq!(InputSourceKind::Editor.to_string(), "editor");
    }
}