standout-input 9.0.0

Declarative input collection for CLI applications
Documentation
use std::fmt::Write as _;

use super::definition::{Item, Questionnaire};

pub(crate) const FORMAT_LINE: &str = "#! standout-answers 1";
pub(crate) const QUESTIONNAIRE_PREFIX: &str = "#! questionnaire:";
pub(crate) const FINGERPRINT_PREFIX: &str = "#! fingerprint:";
pub(crate) const TAG_OPEN: &str = "<id:";

const REPEAT_GUIDANCE: &str =
    "(Add an item by copying one complete block - its heading line and its questions - below the last block, then answering the copy.)";

fn tag(id: &str) -> String {
    format!("{TAG_OPEN}{id}>")
}

impl Questionnaire {
    pub fn render_answer_sheet(&self) -> String {
        let mut out = String::new();
        out.push_str(FORMAT_LINE);
        out.push('\n');
        let _ = writeln!(out, "{QUESTIONNAIRE_PREFIX} {}", self.id());
        let _ = writeln!(out, "{FINGERPRINT_PREFIX} {}", self.fingerprint());
        render_items(self.items(), "", &mut out);
        out
    }
}

fn render_items(items: &[Item], number_prefix: &str, out: &mut String) {
    for (index, item) in items.iter().enumerate() {
        let number = display_number(number_prefix, index + 1);
        match item {
            Item::Field(field) => {
                out.push('\n');
                let _ = writeln!(
                    out,
                    "{number} {} ({}) {}",
                    field.prompt(),
                    field.type_hint(),
                    tag(field.id())
                );
                if let Some(default) = field.default() {
                    let _ = writeln!(out, "{default}");
                }
            }
            Item::Group(group) => {
                let occurrences = group.repeat().map_or(1, |repeat| repeat.min());
                for occurrence in 0..occurrences {
                    out.push('\n');
                    let _ = writeln!(
                        out,
                        "{number} {} ({}) {}",
                        group.prompt(),
                        group.type_hint(),
                        tag(group.id())
                    );
                    if group.repeat().is_some() && occurrence == 0 {
                        out.push_str(REPEAT_GUIDANCE);
                        out.push('\n');
                    }
                    render_items(group.children(), number.trim_end_matches('.'), out);
                }
            }
        }
    }
}

fn display_number(prefix: &str, ordinal: usize) -> String {
    if prefix.is_empty() {
        format!("{ordinal}.")
    } else {
        format!("{prefix}.{ordinal}")
    }
}