derive_wizard_macro/
lib.rs

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            // For enums, we can't easily provide defaults, so just return the base interview
45            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    // Handle nested types dynamically
325    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    // Get the questions from the base interview
610    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}