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}