standout-input 9.0.0

Declarative input collection for CLI applications
Documentation
use super::{
    Answers, FormError, Item, Questionnaire, QuestionnaireError, RawAnswers, ValidationDiagnostic,
};

pub trait QuestionnaireChoices:
    Sized + std::str::FromStr<Err = QuestionnaireChoiceParseError> + std::fmt::Display
{
    fn choices() -> &'static [&'static str];
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QuestionnaireChoiceParseError {
    choices: &'static [&'static str],
}

impl QuestionnaireChoiceParseError {
    pub const fn new(choices: &'static [&'static str]) -> Self {
        Self { choices }
    }

    pub fn choices(&self) -> &'static [&'static str] {
        self.choices
    }
}

impl std::fmt::Display for QuestionnaireChoiceParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "expected one of: {}", self.choices.join(", "))
    }
}

impl std::error::Error for QuestionnaireChoiceParseError {}

pub trait QuestionnaireInput: Sized {
    fn questionnaire() -> Result<Questionnaire, QuestionnaireError>;

    #[doc(hidden)]
    fn from_decoded_answers(answers: &Answers) -> Self;

    #[doc(hidden)]
    fn questionnaire_items(prefix: &str) -> Vec<Item> {
        assert!(
            prefix.is_empty(),
            "manual QuestionnaireInput implementation cannot be nested unless questionnaire_items is overridden"
        );
        Self::questionnaire()
            .expect("manual QuestionnaireInput implementation cannot be nested unless questionnaire_items is overridden")
            .items()
            .to_vec()
    }

    #[doc(hidden)]
    fn from_decoded_answers_at(answers: &Answers, prefix: &str) -> Self {
        assert!(
            prefix.is_empty(),
            "manual QuestionnaireInput implementation cannot be nested unless from_decoded_answers_at is overridden"
        );
        Self::from_decoded_answers(answers)
    }

    fn from_raw_answers(raw: &RawAnswers) -> Result<Self, QuestionnaireInputError> {
        let questionnaire = Self::questionnaire().map_err(QuestionnaireInputError::Definition)?;
        let answers = questionnaire
            .decode_answers(raw)
            .map_err(QuestionnaireInputError::Validation)?;
        Ok(Self::from_decoded_answers(&answers))
    }

    fn from_raw_answers_with<F>(raw: &RawAnswers, form: F) -> Result<Self, QuestionnaireInputError>
    where
        F: FnOnce(&Self) -> Vec<FormError>,
    {
        let questionnaire = Self::questionnaire().map_err(QuestionnaireInputError::Definition)?;
        let answers = questionnaire
            .decode_answers(raw)
            .map_err(QuestionnaireInputError::Validation)?;
        let value = Self::from_decoded_answers(&answers);
        let form_errors = form(&value);
        if form_errors.is_empty() {
            return Ok(value);
        }
        Err(QuestionnaireInputError::Validation(
            form_errors
                .into_iter()
                .map(|error| ValidationDiagnostic::Form {
                    fields: error.fields,
                    message: error.message,
                })
                .collect(),
        ))
    }
}

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum QuestionnaireInputError {
    #[error("derived questionnaire definition is invalid: {0}")]
    Definition(QuestionnaireError),

    #[error("derived questionnaire answers are invalid: {}", validation_diagnostics_display(.0))]
    Validation(Vec<ValidationDiagnostic>),
}

fn validation_diagnostics_display(diagnostics: &[ValidationDiagnostic]) -> String {
    if diagnostics.is_empty() {
        return "no diagnostics reported".to_string();
    }

    diagnostics
        .iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join("; ")
}