standout-input 9.0.0

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

use crate::collector::{InputCollector, InputSourceKind, ResolvedInput};
use crate::InputError;

#[derive(Debug, Clone)]
pub struct ArgSource {
    name: String,
}

impl ArgSource {
    pub fn new(name: impl Into<String>) -> Self {
        Self { name: name.into() }
    }

    pub fn arg_name(&self) -> &str {
        &self.name
    }
}

impl InputCollector<String> for ArgSource {
    fn name(&self) -> &'static str {
        "argument"
    }

    fn is_available(&self, matches: &ArgMatches) -> bool {
        matches.contains_id(&self.name) && matches.get_one::<String>(&self.name).is_some()
    }

    fn collect(&self, matches: &ArgMatches) -> Result<Option<String>, InputError> {
        Ok(matches.get_one::<String>(&self.name).cloned())
    }
}

#[derive(Debug, Clone)]
pub struct FlagSource {
    name: String,
    invert: bool,
}

impl FlagSource {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            invert: false,
        }
    }

    pub fn inverted(mut self) -> Self {
        self.invert = true;
        self
    }

    pub fn flag_name(&self) -> &str {
        &self.name
    }
}

impl InputCollector<bool> for FlagSource {
    fn name(&self) -> &'static str {
        "flag"
    }

    fn is_available(&self, matches: &ArgMatches) -> bool {
        matches.contains_id(&self.name)
    }

    fn collect(&self, matches: &ArgMatches) -> Result<Option<bool>, InputError> {
        let value = matches.get_flag(&self.name);
        let result = if self.invert { !value } else { value };

        if matches.get_flag(&self.name) {
            Ok(Some(result))
        } else {
            Ok(None)
        }
    }
}

impl FlagSource {
    pub fn resolve(&self, matches: &ArgMatches) -> Result<ResolvedInput<bool>, InputError> {
        let value = matches.get_flag(&self.name);
        let result = if self.invert { !value } else { value };

        Ok(ResolvedInput {
            value: result,
            source: InputSourceKind::Flag,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::{Arg, Command};

    fn make_matches(args: &[&str]) -> ArgMatches {
        Command::new("test")
            .arg(Arg::new("message").long("message").short('m'))
            .arg(
                Arg::new("verbose")
                    .long("verbose")
                    .short('v')
                    .action(clap::ArgAction::SetTrue),
            )
            .arg(
                Arg::new("no-editor")
                    .long("no-editor")
                    .action(clap::ArgAction::SetTrue),
            )
            .try_get_matches_from(args)
            .unwrap()
    }

    #[test]
    fn arg_source_available_when_provided() {
        let matches = make_matches(&["test", "--message", "hello"]);
        let source = ArgSource::new("message");

        assert!(source.is_available(&matches));
        assert_eq!(source.collect(&matches).unwrap(), Some("hello".to_string()));
    }

    #[test]
    fn arg_source_unavailable_when_missing() {
        let matches = make_matches(&["test"]);
        let source = ArgSource::new("message");

        assert!(!source.is_available(&matches));
        assert_eq!(source.collect(&matches).unwrap(), None);
    }

    #[test]
    fn flag_source_returns_some_when_set() {
        let matches = make_matches(&["test", "--verbose"]);
        let source = FlagSource::new("verbose");

        assert!(source.is_available(&matches));
        assert_eq!(source.collect(&matches).unwrap(), Some(true));
    }

    #[test]
    fn flag_source_returns_none_when_not_set() {
        let matches = make_matches(&["test"]);
        let source = FlagSource::new("verbose");

        assert!(source.is_available(&matches));
        assert_eq!(source.collect(&matches).unwrap(), None);
    }

    #[test]
    fn flag_source_inverted() {
        let matches = make_matches(&["test", "--no-editor"]);
        let source = FlagSource::new("no-editor").inverted();

        assert_eq!(source.collect(&matches).unwrap(), Some(false));
    }

    #[test]
    fn flag_source_inverted_not_set() {
        let matches = make_matches(&["test"]);
        let source = FlagSource::new("no-editor").inverted();

        assert_eq!(source.collect(&matches).unwrap(), None);
    }
}