Skip to main content

standout_input/questionnaire/
derive.rs

1//! Trait support for `#[derive(Questionnaire)]`.
2//!
3//! The derive macro lowers an application struct to the public
4//! [`Questionnaire`] builder, then generates direct filling code from decoded
5//! [`Answers`]. This keeps derived questionnaires on the same validation,
6//! rendering, parsing, and fingerprinting path as hand-built definitions.
7
8use super::{
9    Answers, FormError, Item, Questionnaire, QuestionnaireError, RawAnswers, ValidationDiagnostic,
10};
11
12/// A Rust enum-backed choice vocabulary for derived questionnaires.
13///
14/// Implemented by `standout-macros` for enums that derive
15/// `QuestionnaireChoices`. The enum is then the single source for the
16/// accepted answer strings, rendered hints, parsing, and display: a derived
17/// questionnaire field of this enum type marked `#[question(choice)]` lowers
18/// to a string field constrained with
19/// [`ScalarField::one_of`](super::ScalarField::one_of), and typed filling
20/// parses the validated answer back into the enum.
21pub trait QuestionnaireChoices:
22    Sized + std::str::FromStr<Err = QuestionnaireChoiceParseError> + std::fmt::Display
23{
24    /// The declared user-facing choices for this enum.
25    fn choices() -> &'static [&'static str];
26}
27
28/// Error returned when parsing an undeclared enum choice.
29///
30/// The invalid submitted value is intentionally not retained or displayed;
31/// diagnostics elsewhere in the questionnaire pipeline follow the same
32/// no-echo rule for answer values.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct QuestionnaireChoiceParseError {
35    choices: &'static [&'static str],
36}
37
38impl QuestionnaireChoiceParseError {
39    /// Create an error for a vocabulary with the given declared choices.
40    pub const fn new(choices: &'static [&'static str]) -> Self {
41        Self { choices }
42    }
43
44    /// The choices accepted by the enum parser.
45    pub fn choices(&self) -> &'static [&'static str] {
46        self.choices
47    }
48}
49
50impl std::fmt::Display for QuestionnaireChoiceParseError {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(f, "expected one of: {}", self.choices.join(", "))
53    }
54}
55
56impl std::error::Error for QuestionnaireChoiceParseError {}
57
58/// A derived questionnaire definition and typed filler.
59///
60/// Implemented by `standout-macros` for structs that derive
61/// `Questionnaire`. The generated implementation has two pieces:
62///
63/// - [`questionnaire`](Self::questionnaire), which constructs the runtime
64///   definition through [`Questionnaire::new`] so all existing construction
65///   invariants remain load-bearing.
66/// - [`from_decoded_answers`](Self::from_decoded_answers), which directly
67///   materializes the struct from successfully decoded [`Answers`] without
68///   involving serde or stringly application conversion code.
69/// - [`from_raw_answers_with`](Self::from_raw_answers_with), which lets an
70///   application add whole-form rules over the filled struct while keeping
71///   field decoding, defaults, conditions, and validators on the shared
72///   runtime path.
73///
74/// The derive also emits hidden helpers used to lower and fill nested
75/// questionnaire structs. They keep nested fields on the same stable
76/// group-prefix and occurrence-path model as the public runtime definition.
77/// An `active_when` controller must name a field of the same derived struct;
78/// the macro resolves it to the field's stable definition ID at expansion
79/// time.
80pub trait QuestionnaireInput: Sized {
81    /// Construct the runtime questionnaire definition for this type.
82    ///
83    /// Construction errors are the normal [`QuestionnaireError`] values from
84    /// the public builder and identify the invalid questionnaire or field ID.
85    fn questionnaire() -> Result<Questionnaire, QuestionnaireError>;
86
87    /// Fill this type from answers decoded by its own questionnaire.
88    ///
89    /// This method is generated code for the closed questionnaire type
90    /// universe: scalars, `Option<T>`, scalar `Vec<T>`, enum choices, nested
91    /// structs, and repeatable groups. Conditional fields are represented as
92    /// `Option<T>` because inactive decoded answers are absent. Call
93    /// [`from_raw_answers`](Self::from_raw_answers) for the public checked
94    /// path: it decodes with the generated definition first, then fills only
95    /// after field validation succeeds.
96    #[doc(hidden)]
97    fn from_decoded_answers(answers: &Answers) -> Self;
98
99    /// Build this type's items under `prefix`.
100    ///
101    /// Generated implementations use `prefix` to make nested struct field IDs
102    /// extend their enclosing group ID. Manual implementations may keep the
103    /// default root-only behavior unless they need nested reuse. The default
104    /// implementation panics for non-empty prefixes so accidental nested reuse
105    /// fails at the call site instead of constructing root-scoped items.
106    #[doc(hidden)]
107    fn questionnaire_items(prefix: &str) -> Vec<Item> {
108        assert!(
109            prefix.is_empty(),
110            "manual QuestionnaireInput implementation cannot be nested unless questionnaire_items is overridden"
111        );
112        Self::questionnaire()
113            .expect("manual QuestionnaireInput implementation cannot be nested unless questionnaire_items is overridden")
114            .items()
115            .to_vec()
116    }
117
118    /// Fill this type from answers rooted at `prefix`.
119    ///
120    /// Generated implementations use `prefix` to read fields inside nested and
121    /// repeatable group occurrences. Manual implementations may keep the
122    /// default root-only behavior unless they need nested reuse. The default
123    /// implementation panics for non-empty prefixes so accidental nested reuse
124    /// fails at the call site instead of reading root-scoped answers.
125    #[doc(hidden)]
126    fn from_decoded_answers_at(answers: &Answers, prefix: &str) -> Self {
127        assert!(
128            prefix.is_empty(),
129            "manual QuestionnaireInput implementation cannot be nested unless from_decoded_answers_at is overridden"
130        );
131        Self::from_decoded_answers(answers)
132    }
133
134    /// Decode raw answers with this type's generated definition and return
135    /// the filled struct.
136    ///
137    /// Definition construction runs through the public builder, then the
138    /// normal shared decode pipeline validates fields, defaults,
139    /// optionality, conditions, and validators before the generated filler
140    /// constructs the struct.
141    fn from_raw_answers(raw: &RawAnswers) -> Result<Self, QuestionnaireInputError> {
142        let questionnaire = Self::questionnaire().map_err(QuestionnaireInputError::Definition)?;
143        let answers = questionnaire
144            .decode_answers(raw)
145            .map_err(QuestionnaireInputError::Validation)?;
146        Ok(Self::from_decoded_answers(&answers))
147    }
148
149    /// Decode raw answers, fill the struct, then run typed whole-form rules.
150    ///
151    /// Use this when a questionnaire has constraints that span fields after
152    /// the field-level stage has already produced typed values. The `form`
153    /// closure receives the filled struct and returns [`FormError`] values
154    /// that are reported as normal validation diagnostics. The form closure
155    /// does not run when field decoding fails.
156    fn from_raw_answers_with<F>(raw: &RawAnswers, form: F) -> Result<Self, QuestionnaireInputError>
157    where
158        F: FnOnce(&Self) -> Vec<FormError>,
159    {
160        let questionnaire = Self::questionnaire().map_err(QuestionnaireInputError::Definition)?;
161        let answers = questionnaire
162            .decode_answers(raw)
163            .map_err(QuestionnaireInputError::Validation)?;
164        let value = Self::from_decoded_answers(&answers);
165        let form_errors = form(&value);
166        if form_errors.is_empty() {
167            return Ok(value);
168        }
169        Err(QuestionnaireInputError::Validation(
170            form_errors
171                .into_iter()
172                .map(|error| ValidationDiagnostic::Form {
173                    fields: error.fields,
174                    message: error.message,
175                })
176                .collect(),
177        ))
178    }
179}
180
181/// Errors returned while decoding and filling a derived questionnaire.
182#[derive(Debug, thiserror::Error, PartialEq, Eq)]
183pub enum QuestionnaireInputError {
184    /// The generated definition failed public builder validation.
185    #[error("derived questionnaire definition is invalid: {0}")]
186    Definition(QuestionnaireError),
187
188    /// The submitted answers failed the shared validation pipeline; the
189    /// display message includes value-safe diagnostics from that pipeline.
190    #[error("derived questionnaire answers are invalid: {}", validation_diagnostics_display(.0))]
191    Validation(Vec<ValidationDiagnostic>),
192}
193
194fn validation_diagnostics_display(diagnostics: &[ValidationDiagnostic]) -> String {
195    if diagnostics.is_empty() {
196        return "no diagnostics reported".to_string();
197    }
198
199    diagnostics
200        .iter()
201        .map(ToString::to_string)
202        .collect::<Vec<_>>()
203        .join("; ")
204}