1use proc_macro::TokenStream;
2use proc_macro2::Ident;
3use quote::quote;
4use syn::{Data, Fields, Lit, Meta, Type, parse_macro_input};
5
6use derive_wizard_types::interview::{Alternative, Interview, Section, Sequence};
7use derive_wizard_types::question::{
8 ConfirmQuestion, FloatQuestion, InputQuestion, IntQuestion, MaskedQuestion, MultilineQuestion,
9 NestedQuestion, Question, QuestionKind,
10};
11
12#[proc_macro_derive(
13 Wizard,
14 attributes(
15 prompt,
16 mask,
17 editor,
18 validate_on_submit,
19 validate_on_key,
20 validate,
21 min,
22 max
23 )
24)]
25pub fn wizard_derive(input: TokenStream) -> TokenStream {
26 let input = parse_macro_input!(input);
27 implement_wizard(&input)
28}
29
30fn implement_wizard(input: &syn::DeriveInput) -> TokenStream {
31 let name = &input.ident;
32 let interview = build_interview(input);
33 let interview_code = generate_interview_code(&interview);
34
35 let from_answers_code = match &input.data {
36 Data::Struct(data) => generate_from_answers_struct(name, data),
37 Data::Enum(data) => generate_from_answers_enum(name, data),
38 Data::Union(_) => unimplemented!(),
39 };
40
41 let interview_with_defaults_code = match &input.data {
42 Data::Struct(data) => generate_interview_with_defaults_struct(data, &interview),
43 Data::Enum(_data) => {
44 quote! { Self::interview() }
46 }
47 Data::Union(_) => unimplemented!(),
48 };
49
50 TokenStream::from(quote! {
51 impl Wizard for #name {
52 fn interview() -> derive_wizard::interview::Interview {
53 #interview_code
54 }
55
56 fn interview_with_defaults(&self) -> derive_wizard::interview::Interview {
57 #interview_with_defaults_code
58 }
59
60 fn from_answers(answers: &derive_wizard::backend::Answers) -> Result<Self, derive_wizard::backend::BackendError> {
61 #from_answers_code
62 }
63 }
64 })
65}
66
67fn build_interview(input: &syn::DeriveInput) -> Interview {
68 let sections = match &input.data {
69 Data::Struct(data) => {
70 if let Fields::Named(fields) = &data.fields {
71 let questions = fields
72 .named
73 .iter()
74 .map(|f| build_question(f, None))
75 .collect();
76 vec![Section::Sequence(Sequence {
77 sequence: questions,
78 })]
79 } else {
80 vec![]
81 }
82 }
83 Data::Enum(data) => {
84 let alternatives = data
85 .variants
86 .iter()
87 .map(|variant| {
88 let section = match &variant.fields {
89 Fields::Unit => Section::Empty,
90 Fields::Unnamed(fields) => {
91 let questions = fields
92 .unnamed
93 .iter()
94 .enumerate()
95 .map(|(i, f)| build_question(f, Some(i)))
96 .collect();
97 Section::Sequence(Sequence {
98 sequence: questions,
99 })
100 }
101 Fields::Named(fields) => {
102 let questions = fields
103 .named
104 .iter()
105 .map(|f| build_question(f, None))
106 .collect();
107 Section::Sequence(Sequence {
108 sequence: questions,
109 })
110 }
111 };
112 Alternative {
113 name: variant.ident.to_string(),
114 section,
115 }
116 })
117 .collect();
118 vec![Section::Alternatives(0, alternatives)]
119 }
120 Data::Union(_) => vec![],
121 };
122
123 Interview { sections }
124}
125
126fn build_question(field: &syn::Field, idx: Option<usize>) -> Question {
127 let field_name = idx
128 .map(|i| format!("field_{i}"))
129 .or_else(|| field.ident.as_ref().map(Ident::to_string))
130 .unwrap();
131
132 let attrs = FieldAttrs::extract(&field.attrs, &field_name);
133 let kind = determine_question_kind(&field.ty, &attrs);
134
135 Question::new(Some(field_name.clone()), field_name, attrs.prompt, kind)
136}
137
138struct FieldAttrs {
139 prompt: String,
140 mask: bool,
141 editor: bool,
142 validate_on_key: Option<String>,
143 validate_on_submit: Option<String>,
144 min_int: Option<i64>,
145 max_int: Option<i64>,
146 min_float: Option<f64>,
147 max_float: Option<f64>,
148}
149
150impl FieldAttrs {
151 fn extract(attrs: &[syn::Attribute], field_name: &str) -> Self {
152 let validate = extract_string_attr(attrs, "validate");
153 Self {
154 prompt: extract_string_attr(attrs, "prompt")
155 .unwrap_or_else(|| format!("Enter {field_name}:")),
156 mask: has_attr(attrs, "mask"),
157 editor: has_attr(attrs, "editor"),
158 validate_on_key: extract_string_attr(attrs, "validate_on_key").or(validate.clone()),
159 validate_on_submit: extract_string_attr(attrs, "validate_on_submit").or(validate),
160 min_int: extract_int_attr(attrs, "min"),
161 max_int: extract_int_attr(attrs, "max"),
162 min_float: extract_float_attr(attrs, "min"),
163 max_float: extract_float_attr(attrs, "max"),
164 }
165 }
166}
167
168fn determine_question_kind(ty: &Type, attrs: &FieldAttrs) -> QuestionKind {
169 if attrs.mask {
170 return QuestionKind::Masked(MaskedQuestion {
171 mask: Some('*'),
172 validate_on_key: attrs.validate_on_key.clone(),
173 validate_on_submit: attrs.validate_on_submit.clone(),
174 });
175 }
176
177 if attrs.editor {
178 return QuestionKind::Multiline(MultilineQuestion {
179 default: None,
180 validate_on_key: attrs.validate_on_key.clone(),
181 validate_on_submit: attrs.validate_on_submit.clone(),
182 });
183 }
184
185 match quote!(#ty).to_string().as_str() {
186 "String" => QuestionKind::Input(InputQuestion {
187 default: None,
188 validate_on_key: attrs.validate_on_key.clone(),
189 validate_on_submit: attrs.validate_on_submit.clone(),
190 }),
191 "bool" => QuestionKind::Confirm(ConfirmQuestion { default: false }),
192 "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
193 | "usize" => QuestionKind::Int(IntQuestion {
194 default: None,
195 min: attrs.min_int,
196 max: attrs.max_int,
197 validate_on_key: attrs.validate_on_key.clone(),
198 validate_on_submit: attrs.validate_on_submit.clone(),
199 }),
200 "f32" | "f64" => QuestionKind::Float(FloatQuestion {
201 default: None,
202 min: attrs.min_float,
203 max: attrs.max_float,
204 validate_on_key: attrs.validate_on_key.clone(),
205 validate_on_submit: attrs.validate_on_submit.clone(),
206 }),
207 "PathBuf" => QuestionKind::Input(InputQuestion {
208 default: None,
209 validate_on_key: attrs.validate_on_key.clone(),
210 validate_on_submit: attrs.validate_on_submit.clone(),
211 }),
212 type_path => QuestionKind::Nested(NestedQuestion {
213 type_path: type_path.to_string(),
214 }),
215 }
216}
217
218fn extract_string_attr(attrs: &[syn::Attribute], name: &str) -> Option<String> {
219 attrs.iter().find_map(|attr| {
220 if !attr.path().is_ident(name) {
221 return None;
222 }
223
224 match &attr.meta {
225 Meta::List(list) => syn::parse2::<Lit>(list.tokens.clone())
226 .ok()
227 .and_then(|lit| {
228 if let Lit::Str(s) = lit {
229 Some(s.value())
230 } else {
231 None
232 }
233 }),
234 Meta::NameValue(nv) => {
235 if let syn::Expr::Lit(expr) = &nv.value {
236 if let Lit::Str(s) = &expr.lit {
237 Some(s.value())
238 } else {
239 None
240 }
241 } else {
242 None
243 }
244 }
245 Meta::Path(_) => None,
246 }
247 })
248}
249
250fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
251 attrs.iter().any(|attr| attr.path().is_ident(name))
252}
253
254fn extract_int_attr(attrs: &[syn::Attribute], name: &str) -> Option<i64> {
255 attrs.iter().find_map(|attr| {
256 if !attr.path().is_ident(name) {
257 return None;
258 }
259
260 let parse_lit = |lit: &Lit| match lit {
261 Lit::Int(i) => i.base10_parse().ok(),
262 _ => None,
263 };
264
265 match &attr.meta {
266 Meta::List(list) => syn::parse2::<Lit>(list.tokens.clone())
267 .ok()
268 .and_then(|lit| parse_lit(&lit)),
269 Meta::NameValue(nv) => {
270 if let syn::Expr::Lit(expr) = &nv.value {
271 parse_lit(&expr.lit)
272 } else {
273 None
274 }
275 }
276 Meta::Path(_) => None,
277 }
278 })
279}
280
281fn extract_float_attr(attrs: &[syn::Attribute], name: &str) -> Option<f64> {
282 attrs.iter().find_map(|attr| {
283 if !attr.path().is_ident(name) {
284 return None;
285 }
286
287 let parse_lit = |lit: &Lit| match lit {
288 Lit::Float(f) => f.base10_parse().ok(),
289 Lit::Int(i) => i.base10_parse::<i64>().ok().map(|v| v as f64),
290 _ => None,
291 };
292
293 match &attr.meta {
294 Meta::List(list) => syn::parse2::<Lit>(list.tokens.clone())
295 .ok()
296 .and_then(|lit| parse_lit(&lit)),
297 Meta::NameValue(nv) => {
298 if let syn::Expr::Lit(expr) = &nv.value {
299 parse_lit(&expr.lit)
300 } else {
301 None
302 }
303 }
304 Meta::Path(_) => None,
305 }
306 })
307}
308
309fn generate_interview_code(interview: &Interview) -> proc_macro2::TokenStream {
310 let has_nested = interview.sections.iter().any(|section| {
311 matches!(section, Section::Sequence(seq) if seq.sequence.iter()
312 .any(|q| matches!(q.kind(), QuestionKind::Nested(_))))
313 });
314
315 if !has_nested {
316 let sections = interview.sections.iter().map(generate_section_code);
317 return quote! {
318 derive_wizard::interview::Interview {
319 sections: vec![#(#sections),*],
320 }
321 };
322 }
323
324 let mut builders = Vec::new();
326 for section in &interview.sections {
327 if let Section::Sequence(seq) = section {
328 let mut batch = Vec::new();
329
330 for question in &seq.sequence {
331 if let QuestionKind::Nested(nested) = question.kind() {
332 if !batch.is_empty() {
333 let questions = batch.iter().map(generate_question_code);
334 builders.push(quote! {
335 sections.push(derive_wizard::interview::Section::Sequence(
336 derive_wizard::interview::Sequence { sequence: vec![#(#questions),*] }
337 ));
338 });
339 batch.clear();
340 }
341 let type_ident = syn::parse_str::<syn::Ident>(&nested.type_path).unwrap();
342 builders.push(quote! {
343 sections.extend(#type_ident::interview().sections);
344 });
345 } else {
346 batch.push(question.clone());
347 }
348 }
349
350 if !batch.is_empty() {
351 let questions = batch.iter().map(generate_question_code);
352 builders.push(quote! {
353 sections.push(derive_wizard::interview::Section::Sequence(
354 derive_wizard::interview::Sequence { sequence: vec![#(#questions),*] }
355 ));
356 });
357 }
358 } else {
359 let section_code = generate_section_code(section);
360 builders.push(quote! { sections.push(#section_code); });
361 }
362 }
363
364 quote! {{
365 let mut sections = Vec::new();
366 #(#builders)*
367 derive_wizard::interview::Interview { sections }
368 }}
369}
370
371fn generate_section_code(section: &Section) -> proc_macro2::TokenStream {
372 match section {
373 Section::Empty => quote! { derive_wizard::interview::Section::Empty },
374 Section::Sequence(seq) => {
375 let questions = seq.sequence.iter().map(generate_question_code);
376 quote! {
377 derive_wizard::interview::Section::Sequence(
378 derive_wizard::interview::Sequence { sequence: vec![#(#questions),*] }
379 )
380 }
381 }
382 Section::Alternatives(idx, alts) => {
383 let alternatives = alts.iter().map(|alt| {
384 let name = &alt.name;
385 let section = generate_section_code(&alt.section);
386 quote! {
387 derive_wizard::interview::Alternative {
388 name: #name.to_string(),
389 section: #section,
390 }
391 }
392 });
393 quote! {
394 derive_wizard::interview::Section::Alternatives(#idx, vec![#(#alternatives),*])
395 }
396 }
397 }
398}
399
400fn generate_question_code(question: &Question) -> proc_macro2::TokenStream {
401 let id = question
402 .id()
403 .map_or_else(|| quote!(None), |id| quote! { Some(#id.to_string()) });
404 let name = question.name();
405 let prompt = question.prompt();
406 let kind = generate_question_kind_code(question.kind());
407
408 quote! {
409 derive_wizard::question::Question::new(#id, #name.to_string(), #prompt.to_string(), #kind)
410 }
411}
412
413fn generate_question_kind_code(kind: &QuestionKind) -> proc_macro2::TokenStream {
414 macro_rules! opt_str {
415 ($opt:expr) => {
416 match $opt {
417 Some(v) => quote! { Some(#v.to_string()) },
418 None => quote! { None },
419 }
420 };
421 }
422
423 match kind {
424 QuestionKind::Input(q) => {
425 let default = opt_str!(&q.default);
426 let validate_on_key = opt_str!(&q.validate_on_key);
427 let validate_on_submit = opt_str!(&q.validate_on_submit);
428 quote! {
429 derive_wizard::question::QuestionKind::Input(derive_wizard::question::InputQuestion {
430 default: #default,
431 validate_on_key: #validate_on_key,
432 validate_on_submit: #validate_on_submit,
433 })
434 }
435 }
436 QuestionKind::Multiline(q) => {
437 let default = opt_str!(&q.default);
438 let validate_on_key = opt_str!(&q.validate_on_key);
439 let validate_on_submit = opt_str!(&q.validate_on_submit);
440 quote! {
441 derive_wizard::question::QuestionKind::Multiline(derive_wizard::question::MultilineQuestion {
442 default: #default,
443 validate_on_key: #validate_on_key,
444 validate_on_submit: #validate_on_submit,
445 })
446 }
447 }
448 QuestionKind::Masked(q) => {
449 let mask = q.mask.map_or_else(|| quote!(None), |v| quote! { Some(#v) });
450 let validate_on_key = opt_str!(&q.validate_on_key);
451 let validate_on_submit = opt_str!(&q.validate_on_submit);
452 quote! {
453 derive_wizard::question::QuestionKind::Masked(derive_wizard::question::MaskedQuestion {
454 mask: #mask,
455 validate_on_key: #validate_on_key,
456 validate_on_submit: #validate_on_submit,
457 })
458 }
459 }
460 QuestionKind::Int(q) => {
461 let default = q
462 .default
463 .map_or_else(|| quote!(None), |v| quote! { Some(#v) });
464 let min = q.min.map_or_else(|| quote!(None), |v| quote! { Some(#v) });
465 let max = q.max.map_or_else(|| quote!(None), |v| quote! { Some(#v) });
466 let validate_on_key = match &q.validate_on_key {
467 Some(v) => quote! { Some(#v.to_string()) },
468 None => quote! { None },
469 };
470 let validate_on_submit = match &q.validate_on_submit {
471 Some(v) => quote! { Some(#v.to_string()) },
472 None => quote! { None },
473 };
474 quote! {
475 derive_wizard::question::QuestionKind::Int(derive_wizard::question::IntQuestion {
476 default: #default,
477 min: #min,
478 max: #max,
479 validate_on_key: #validate_on_key,
480 validate_on_submit: #validate_on_submit,
481 })
482 }
483 }
484 QuestionKind::Float(q) => {
485 let default = q
486 .default
487 .map_or_else(|| quote!(None), |v| quote! { Some(#v) });
488 let min = q.min.map_or_else(|| quote!(None), |v| quote! { Some(#v) });
489 let max = q.max.map_or_else(|| quote!(None), |v| quote! { Some(#v) });
490 let validate_on_key = opt_str!(&q.validate_on_key);
491 let validate_on_submit = opt_str!(&q.validate_on_submit);
492 quote! {
493 derive_wizard::question::QuestionKind::Float(derive_wizard::question::FloatQuestion {
494 default: #default,
495 min: #min,
496 max: #max,
497 validate_on_key: #validate_on_key,
498 validate_on_submit: #validate_on_submit,
499 })
500 }
501 }
502 QuestionKind::Confirm(q) => {
503 let default = q.default;
504 quote! {
505 derive_wizard::question::QuestionKind::Confirm(derive_wizard::question::ConfirmQuestion {
506 default: #default,
507 })
508 }
509 }
510 QuestionKind::Nested(q) => {
511 let type_path = &q.type_path;
512 quote! {
513 derive_wizard::question::QuestionKind::Nested(derive_wizard::question::NestedQuestion {
514 type_path: #type_path.to_string(),
515 })
516 }
517 }
518 }
519}
520
521fn generate_from_answers_struct(
522 name: &syn::Ident,
523 data: &syn::DataStruct,
524) -> proc_macro2::TokenStream {
525 let Fields::Named(fields) = &data.fields else {
526 return quote! { unimplemented!("from_answers for non-named struct fields") };
527 };
528
529 let field_assignments = fields.named.iter().map(|field| {
530 let field_name = field.ident.as_ref().unwrap();
531 let field_name_str = field_name.to_string();
532 let extraction = generate_answer_extraction(&field.ty, &field_name_str);
533 quote! { #field_name: #extraction }
534 });
535
536 quote! {
537 Ok(#name { #(#field_assignments),* })
538 }
539}
540
541fn generate_from_answers_enum(name: &syn::Ident, data: &syn::DataEnum) -> proc_macro2::TokenStream {
542 let match_arms = data.variants.iter().map(|variant| {
543 let variant_name = &variant.ident;
544 let variant_str = variant_name.to_string();
545
546 match &variant.fields {
547 Fields::Unit => quote! {
548 #variant_str => Ok(#name::#variant_name),
549 },
550 Fields::Unnamed(fields) => {
551 let constructions = fields.unnamed.iter().enumerate().map(|(i, field)| {
552 let field_name = format!("field_{i}");
553 generate_answer_extraction(&field.ty, &field_name)
554 });
555 quote! {
556 #variant_str => Ok(#name::#variant_name(#(#constructions),*)),
557 }
558 }
559 Fields::Named(fields) => {
560 let constructions = fields.named.iter().map(|field| {
561 let field_name = field.ident.as_ref().unwrap();
562 let field_str = field_name.to_string();
563 let extraction = generate_answer_extraction(&field.ty, &field_str);
564 quote! { #field_name: #extraction }
565 });
566 quote! {
567 #variant_str => Ok(#name::#variant_name { #(#constructions),* }),
568 }
569 }
570 }
571 });
572
573 quote! {
574 let selected = answers.as_string("selected_alternative")?;
575 match selected.as_str() {
576 #(#match_arms)*
577 _ => Err(derive_wizard::backend::BackendError::ExecutionError(
578 format!("Unknown variant: {}", selected)
579 ))
580 }
581 }
582}
583
584fn generate_answer_extraction(ty: &Type, field_name: &str) -> proc_macro2::TokenStream {
585 match quote!(#ty).to_string().as_str() {
586 "String" => quote! { answers.as_string(#field_name)? },
587 "bool" => quote! { answers.as_bool(#field_name)? },
588 "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
589 | "usize" => {
590 quote! { answers.as_int(#field_name)? as #ty }
591 }
592 "f32" | "f64" => quote! { answers.as_float(#field_name)? as #ty },
593 "PathBuf" => quote! { std::path::PathBuf::from(answers.as_string(#field_name)?) },
594 type_str => {
595 let type_ident = syn::parse_str::<syn::Ident>(type_str).unwrap();
596 quote! { #type_ident::from_answers(answers)? }
597 }
598 }
599}
600
601fn generate_interview_with_defaults_struct(
602 data: &syn::DataStruct,
603 base_interview: &Interview,
604) -> proc_macro2::TokenStream {
605 let Fields::Named(fields) = &data.fields else {
606 return quote! { Self::interview() };
607 };
608
609 let Section::Sequence(seq) = &base_interview.sections[0] else {
611 return quote! { Self::interview() };
612 };
613
614 let questions_with_defaults: Vec<_> = fields
615 .named
616 .iter()
617 .zip(&seq.sequence)
618 .map(|(field, question)| {
619 let field_name = field.ident.as_ref().unwrap();
620 let field_type = &field.ty;
621
622 generate_question_with_default_code(question, field_name, field_type)
623 })
624 .collect();
625
626 quote! {{
627 let mut interview = Self::interview();
628 if let Some(derive_wizard::interview::Section::Sequence(seq)) = interview.sections.get_mut(0) {
629 seq.sequence = vec![#(#questions_with_defaults),*];
630 }
631 interview
632 }}
633}
634
635fn generate_question_with_default_code(
636 question: &Question,
637 field_name: &syn::Ident,
638 field_type: &Type,
639) -> proc_macro2::TokenStream {
640 let id = question
641 .id()
642 .map_or_else(|| quote!(None), |id| quote! { Some(#id.to_string()) });
643 let name = question.name();
644 let prompt = question.prompt();
645
646 let kind_with_default = match question.kind() {
647 QuestionKind::Input(q) => {
648 let validate_on_key = match &q.validate_on_key {
649 Some(v) => quote! { Some(#v.to_string()) },
650 None => quote! { None },
651 };
652 let validate_on_submit = match &q.validate_on_submit {
653 Some(v) => quote! { Some(#v.to_string()) },
654 None => quote! { None },
655 };
656
657 match quote!(#field_type).to_string().as_str() {
658 "String" => quote! {
659 derive_wizard::question::QuestionKind::Input(derive_wizard::question::InputQuestion {
660 default: Some(self.#field_name.clone()),
661 validate_on_key: #validate_on_key,
662 validate_on_submit: #validate_on_submit,
663 })
664 },
665 "PathBuf" => quote! {
666 derive_wizard::question::QuestionKind::Input(derive_wizard::question::InputQuestion {
667 default: Some(self.#field_name.display().to_string()),
668 validate_on_key: #validate_on_key,
669 validate_on_submit: #validate_on_submit,
670 })
671 },
672 _ => generate_question_kind_code(question.kind()),
673 }
674 }
675 QuestionKind::Multiline(q) => {
676 let validate_on_key = match &q.validate_on_key {
677 Some(v) => quote! { Some(#v.to_string()) },
678 None => quote! { None },
679 };
680 let validate_on_submit = match &q.validate_on_submit {
681 Some(v) => quote! { Some(#v.to_string()) },
682 None => quote! { None },
683 };
684 quote! {
685 derive_wizard::question::QuestionKind::Multiline(derive_wizard::question::MultilineQuestion {
686 default: Some(self.#field_name.clone()),
687 validate_on_key: #validate_on_key,
688 validate_on_submit: #validate_on_submit,
689 })
690 }
691 }
692 QuestionKind::Int(q) => {
693 let min = q.min.map_or_else(|| quote!(None), |v| quote! { Some(#v) });
694 let max = q.max.map_or_else(|| quote!(None), |v| quote! { Some(#v) });
695 let validate_on_key = match &q.validate_on_key {
696 Some(v) => quote! { Some(#v.to_string()) },
697 None => quote! { None },
698 };
699 let validate_on_submit = match &q.validate_on_submit {
700 Some(v) => quote! { Some(#v.to_string()) },
701 None => quote! { None },
702 };
703 quote! {
704 derive_wizard::question::QuestionKind::Int(derive_wizard::question::IntQuestion {
705 default: Some(self.#field_name as i64),
706 min: #min,
707 max: #max,
708 validate_on_key: #validate_on_key,
709 validate_on_submit: #validate_on_submit,
710 })
711 }
712 }
713 QuestionKind::Float(q) => {
714 let min = q.min.map_or_else(|| quote!(None), |v| quote! { Some(#v) });
715 let max = q.max.map_or_else(|| quote!(None), |v| quote! { Some(#v) });
716 let validate_on_key = match &q.validate_on_key {
717 Some(v) => quote! { Some(#v.to_string()) },
718 None => quote! { None },
719 };
720 let validate_on_submit = match &q.validate_on_submit {
721 Some(v) => quote! { Some(#v.to_string()) },
722 None => quote! { None },
723 };
724 quote! {
725 derive_wizard::question::QuestionKind::Float(derive_wizard::question::FloatQuestion {
726 default: Some(self.#field_name as f64),
727 min: #min,
728 max: #max,
729 validate_on_key: #validate_on_key,
730 validate_on_submit: #validate_on_submit,
731 })
732 }
733 }
734 QuestionKind::Confirm(q) => {
735 let _ = q;
736 quote! {
737 derive_wizard::question::QuestionKind::Confirm(derive_wizard::question::ConfirmQuestion {
738 default: self.#field_name,
739 })
740 }
741 }
742 _ => generate_question_kind_code(question.kind()),
743 };
744
745 quote! {
746 derive_wizard::question::Question::new(#id, #name.to_string(), #prompt.to_string(), #kind_with_default)
747 }
748}