Skip to main content

openapi_model_generator/
generator.rs

1use std::sync::OnceLock;
2
3use crate::{
4    models::{
5        CompositionModel, EnumModel, Model, ModelType, RequestModel, ResponseModel, TypeAliasModel,
6        UnionModel, UnionType,
7    },
8    Result,
9};
10use indexmap::IndexMap;
11
12bitflags::bitflags! {
13    struct RequiredUses: u8 {
14        const UUID = 0b00000001;
15        const DATETIME = 0b00000010;
16        const DATE = 0b00000100;
17    }
18
19    /// Choose what type of structs you want to generate:
20    ///  - Models (generated always)
21    ///  - Requests (optional)
22    ///  - Responses (optional)
23    pub struct GenerateMode: u8 {
24        /// Models will be always include to output
25        const MODELS = 0;
26        /// Additional includes request structs to output
27        const REQUESTS = 1 << 0;
28        /// Additional includes response structs to output
29        const RESPONSES = 1 << 1;
30        /// Outputs all possible structs: models, request and response structs
31        const ALL = Self::REQUESTS.bits() | Self::RESPONSES.bits();
32    }
33}
34
35impl Default for GenerateMode {
36    fn default() -> Self {
37        Self::ALL
38    }
39}
40
41static HDR: OnceLock<String> = OnceLock::new();
42
43fn create_header() -> String {
44    HDR.get_or_init(|| {
45        format!(
46            r#"
47//!
48//! Generated from an OAS specification by {}(v{})
49//!
50
51"#,
52            option_env!("CARGO_PKG_NAME").unwrap_or("openapi-model-generator"),
53            option_env!("CARGO_PKG_VERSION").unwrap_or("unknown")
54        )
55    })
56    .clone()
57}
58
59const RUST_RESERVED_KEYWORDS: &[&str] = &[
60    "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for",
61    "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
62    "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
63    "while", "abstract", "become", "box", "do", "final", "gen", "macro", "override", "priv", "try",
64    "typeof", "unsized", "virtual", "yield",
65];
66
67const EMPTY_RESPONSE_NAME: &str = "UnknownResponse";
68const EMPTY_REQUEST_NAME: &str = "UnknownRequest";
69
70fn is_reserved_word(string_to_check: &str) -> bool {
71    RUST_RESERVED_KEYWORDS.contains(&string_to_check.to_lowercase().as_str())
72}
73
74fn generate_description_docs(
75    description: &Option<String>,
76    fallback_str: &str,
77    indent: &str,
78) -> String {
79    let mut output = String::new();
80    if let Some(desc) = description {
81        for line in desc.lines() {
82            output.push_str(&format!("{}/// {}\n", indent, line.trim()));
83        }
84    } else if !fallback_str.is_empty() {
85        output.push_str(&format!("{}/// {}\n", indent, fallback_str));
86    }
87
88    output
89}
90
91fn to_snake_case(name: &str) -> String {
92    let cleaned: String = name
93        .chars()
94        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
95        .collect();
96
97    let mut snake = String::new();
98
99    for (i, c) in cleaned.chars().enumerate() {
100        if c.is_ascii_uppercase() {
101            if i != 0 {
102                snake.push('_');
103            }
104            snake.push(c.to_ascii_lowercase());
105        } else {
106            snake.push(c);
107        }
108    }
109    snake = snake.replace("__", "_");
110
111    if snake == "self" {
112        snake.push('_');
113    }
114
115    if snake
116        .chars()
117        .next()
118        .map(|c| c.is_ascii_digit())
119        .unwrap_or(false)
120    {
121        snake = format!("_{snake}");
122    }
123
124    snake
125}
126
127/// Checks if custom attributes contain a derive attribute
128fn has_custom_derive(custom_attrs: &Option<Vec<String>>) -> bool {
129    if let Some(attrs) = custom_attrs {
130        attrs
131            .iter()
132            .any(|attr| attr.trim().starts_with("#[derive("))
133    } else {
134        false
135    }
136}
137
138/// Checks if custom attributes contain a serde attribute
139fn has_custom_serde(custom_attrs: &Option<Vec<String>>) -> bool {
140    if let Some(attrs) = custom_attrs {
141        attrs.iter().any(|attr| attr.trim().starts_with("#[serde("))
142    } else {
143        false
144    }
145}
146
147/// Generates a `impl std::fmt::Display` block for a named type, unless custom_attrs
148/// already contains a Display derive. Structs use `{:?}` (Debug) as a fallback;
149/// for enums and unions the caller supplies the match body via `match_arms`.
150fn generate_display_impl(name: &str, custom_attrs: &Option<Vec<String>>, body: &str) -> String {
151    // TODO: `.contains("Display")` is a loose heuristic - it correctly catches
152    // `derive_more::Display` and `#[display(...)]` format attrs, but could
153    // false-positive on unrelated attribute strings containing "Display".
154    // The proper fix is a dedicated spec extension (`x-rust-display: false`)
155    // that explicitly opts a type out of Display generation, rather than
156    // inferring intent from x-rust-attrs content.
157    let has_display = custom_attrs
158        .as_ref()
159        .is_some_and(|attrs| attrs.iter().any(|a| a.contains("Display")));
160    if has_display {
161        return String::new();
162    }
163    format!(
164        "impl std::fmt::Display for {name} {{\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{\n{body}    }}\n}}\n"
165    )
166}
167
168/// Generates custom attributes from x-rust-attrs
169fn generate_custom_attrs(custom_attrs: &Option<Vec<String>>) -> String {
170    if let Some(attrs) = custom_attrs {
171        attrs
172            .iter()
173            .map(|attr| format!("{attr}\n"))
174            .collect::<String>()
175    } else {
176        String::new()
177    }
178}
179
180pub fn generate_models(
181    models: &[ModelType],
182    requests: &[RequestModel],
183    responses: &[ResponseModel],
184    mode: GenerateMode,
185    display: bool,
186) -> Result<String> {
187    // First, generate all model code to determine which imports are needed
188    let mut models_code = String::new();
189    let mut required_uses = RequiredUses::empty();
190    let mut needs_validator = false;
191    let mut pattern_registry: IndexMap<String, String> = IndexMap::new();
192
193    for model_type in models {
194        match model_type {
195            ModelType::Struct(model) => {
196                models_code.push_str(&generate_model(
197                    model,
198                    &mut required_uses,
199                    &mut needs_validator,
200                    &mut pattern_registry,
201                    display,
202                )?);
203            }
204            ModelType::Union(union) => {
205                models_code.push_str(&generate_union(union, display)?);
206            }
207            ModelType::Composition(comp) => {
208                models_code.push_str(&generate_composition(comp, &mut required_uses, display)?);
209            }
210            ModelType::Enum(enum_model) => {
211                models_code.push_str(&generate_enum(enum_model, display)?);
212            }
213            ModelType::TypeAlias(type_alias) => {
214                models_code.push_str(&generate_type_alias(type_alias)?);
215            }
216        }
217    }
218
219    if mode.contains(GenerateMode::REQUESTS) {
220        for request in requests {
221            models_code.push_str(&generate_request_model(request)?);
222        }
223    }
224
225    if mode.contains(GenerateMode::RESPONSES) {
226        for response in responses {
227            models_code.push_str(&generate_response_model(response)?);
228        }
229    }
230
231    // Determine which imports are actually needed
232    let needs_uuid = required_uses.contains(RequiredUses::UUID);
233    let needs_datetime = required_uses.contains(RequiredUses::DATETIME);
234    let needs_date = required_uses.contains(RequiredUses::DATE);
235
236    // Build final output with only necessary imports
237    let mut output = create_header();
238    output.push_str("use serde::{Serialize, Deserialize};\n");
239
240    if needs_uuid {
241        output.push_str("use uuid::Uuid;\n");
242    }
243
244    if needs_validator {
245        output.push_str("use validator::Validate;\n");
246    }
247
248    if !pattern_registry.is_empty() {
249        output.push_str("use regex::Regex;\n");
250        output.push_str("use std::sync::LazyLock;\n");
251    }
252
253    if needs_datetime || needs_date {
254        output.push_str("use chrono::{");
255        let mut chrono_imports = Vec::new();
256        if needs_datetime {
257            chrono_imports.push("DateTime");
258        }
259        if needs_date {
260            chrono_imports.push("NaiveDate");
261        }
262        if needs_datetime {
263            chrono_imports.push("Utc");
264        }
265        output.push_str(&chrono_imports.join(", "));
266        output.push_str("};\n");
267    }
268
269    output.push('\n');
270
271    // Static regex objects, one per unique pattern across all models.
272    if !pattern_registry.is_empty() {
273        for (pattern, name) in &pattern_registry {
274            output.push_str(&format!(
275                "static {name}: LazyLock<Regex> = LazyLock::new(|| Regex::new(r\"{pattern}\").unwrap());\n"
276            ));
277        }
278        output.push('\n');
279    }
280
281    output.push_str(&models_code);
282
283    Ok(output)
284}
285
286/// Format f64 as a Rust float literal, ensuring `0` becomes `0.0`.
287fn format_f64_literal(v: f64) -> String {
288    let s = v.to_string();
289    if s.contains('.') || s.contains('e') || s.contains("inf") || s.contains("NaN") {
290        s
291    } else {
292        format!("{v}.0")
293    }
294}
295
296/// Generate validator attributes based on validation rules
297/// Register a regex pattern and return the name of the static that holds its compiled `Regex`.
298/// Patterns are deduplicated: the same pattern always maps to the same static.
299fn register_pattern(pattern: &str, registry: &mut IndexMap<String, String>) -> String {
300    if let Some(existing) = registry.get(pattern) {
301        return existing.clone();
302    }
303    let name = format!("RE_{}", registry.len() + 1);
304    registry.insert(pattern.to_string(), name.clone());
305    name
306}
307
308fn generate_validator_attrs(
309    rules: &crate::models::ValidationRules,
310    field_type: &str,
311    pattern_registry: &mut IndexMap<String, String>,
312) -> String {
313    let mut attrs = String::new();
314
315    match field_type {
316        "String" | "str" | "Option<String>" | "Option<str>" => {
317            let mut length_attrs = Vec::new();
318            if let Some(min) = rules.min_length {
319                length_attrs.push(format!("min = {}", min));
320            }
321            if let Some(max) = rules.max_length {
322                length_attrs.push(format!("max = {}", max));
323            }
324            if !length_attrs.is_empty() {
325                attrs.push_str(&format!(
326                    "    #[validate(length({}))]\n",
327                    length_attrs.join(", ")
328                ));
329            }
330
331            if rules.email {
332                attrs.push_str("    #[validate(email)]\n");
333            }
334
335            if rules.url {
336                attrs.push_str("    #[validate(url)]\n");
337            }
338
339            if let Some(pattern) = &rules.pattern {
340                let static_name = register_pattern(pattern, pattern_registry);
341                attrs.push_str(&format!("    #[validate(regex(path = {static_name}))]\n"));
342            }
343        }
344        "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "Option<i8>"
345        | "Option<i16>" | "Option<i32>" | "Option<i64>" | "Option<u8>" | "Option<u16>"
346        | "Option<u32>" | "Option<u64>" => {
347            let mut range_attrs = Vec::new();
348            if let Some(min) = rules.minimum {
349                range_attrs.push(format!("min = {}", min as i64));
350            }
351            if let Some(max) = rules.maximum {
352                range_attrs.push(format!("max = {}", max as i64));
353            }
354            if rules.exclusive_minimum || rules.exclusive_maximum {
355                range_attrs.push("exclusive = true".to_string());
356            }
357            if !range_attrs.is_empty() {
358                attrs.push_str(&format!(
359                    "    #[validate(range({}))]\n",
360                    range_attrs.join(", ")
361                ));
362            }
363        }
364        "f32" | "f64" | "Option<f32>" | "Option<f64>" => {
365            let mut range_attrs = Vec::new();
366            if let Some(min) = rules.minimum {
367                range_attrs.push(format!("min = {}", format_f64_literal(min)));
368            }
369            if let Some(max) = rules.maximum {
370                range_attrs.push(format!("max = {}", format_f64_literal(max)));
371            }
372            if rules.exclusive_minimum || rules.exclusive_maximum {
373                range_attrs.push("exclusive = true".to_string());
374            }
375            if !range_attrs.is_empty() {
376                attrs.push_str(&format!(
377                    "    #[validate(range({}))]\n",
378                    range_attrs.join(", ")
379                ));
380            }
381        }
382        _ if field_type.contains("Vec<") => {
383            let mut length_attrs = Vec::new();
384            if let Some(min) = rules.min_items {
385                length_attrs.push(format!("min = {}", min));
386            }
387            if let Some(max) = rules.max_items {
388                length_attrs.push(format!("max = {}", max));
389            }
390            if !length_attrs.is_empty() {
391                attrs.push_str(&format!(
392                    "    #[validate(length({}))]\n",
393                    length_attrs.join(", ")
394                ));
395            }
396        }
397        _ => {}
398    }
399
400    attrs
401}
402
403fn generate_model(
404    model: &Model,
405    required_uses: &mut RequiredUses,
406    needs_validator: &mut bool,
407    pattern_registry: &mut IndexMap<String, String>,
408    display: bool,
409) -> Result<String> {
410    let mut output = String::new();
411
412    output.push_str(&generate_description_docs(
413        &model.description,
414        &model.name,
415        "",
416    ));
417
418    output.push_str(&generate_custom_attrs(&model.custom_attrs));
419
420    // First pass over fields: resolve types and generate field bodies, tracking
421    // whether any #[validate(...)] attrs are needed. This lets us emit the correct
422    // derive line once without fragile byte-range patching.
423    struct FieldOutput {
424        body: String,
425        needs_validate: bool,
426    }
427    let mut field_outputs: Vec<FieldOutput> = Vec::with_capacity(model.fields.len());
428
429    for field in &model.fields {
430        let field_type = match field.field_type.as_str() {
431            "DateTime" | "DateTime<Utc>" => {
432                *required_uses |= RequiredUses::DATETIME;
433                "DateTime<Utc>"
434            }
435            "Date" => {
436                *required_uses |= RequiredUses::DATE;
437                "NaiveDate"
438            }
439            "Uuid" => {
440                *required_uses |= RequiredUses::UUID;
441                "Uuid"
442            }
443            _ => &field.field_type,
444        };
445
446        let mut lowercased_name = to_snake_case(field.name.as_str());
447        if is_reserved_word(&lowercased_name) {
448            lowercased_name = format!("r#{lowercased_name}")
449        }
450
451        let is_optional = !field.is_required || field.is_nullable;
452        let base_type = if field.is_array_ref {
453            format!("Vec<{field_type}>")
454        } else {
455            field_type.to_string()
456        };
457        let full_field_type = if is_optional {
458            format!("Option<{base_type}>")
459        } else {
460            base_type
461        };
462
463        let mut field_body = String::new();
464        field_body.push_str(&generate_description_docs(&field.description, "", "    "));
465
466        if let Some(attrs) = &field.custom_attrs {
467            for attr in attrs {
468                field_body.push_str(&format!("    {attr}\n"));
469            }
470        }
471
472        let mut needs_validate = false;
473        if let Some(rules) = &field.validation_rules {
474            let attrs = generate_validator_attrs(rules, &full_field_type, pattern_registry);
475            if !attrs.is_empty() {
476                needs_validate = true;
477                field_body.push_str(&attrs);
478            }
479        }
480
481        if lowercased_name != field.name {
482            field_body.push_str(&format!("    #[serde(rename = \"{}\")]\n", field.name));
483        }
484        if field.should_flatten() {
485            field_body.push_str("    #[serde(flatten)]\n");
486        }
487        field_body.push_str(&format!("    pub {lowercased_name}: {full_field_type},\n"));
488
489        field_outputs.push(FieldOutput {
490            body: field_body,
491            needs_validate,
492        });
493    }
494
495    let any_validate_attrs = field_outputs.iter().any(|f| f.needs_validate);
496
497    if !has_custom_derive(&model.custom_attrs) {
498        if any_validate_attrs {
499            *needs_validator = true;
500            output.push_str("#[derive(Debug, Clone, Serialize, Deserialize, Validate)]\n");
501        } else {
502            output.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
503        }
504    }
505
506    output.push_str(&format!("pub struct {} {{\n", model.name));
507    for fo in field_outputs {
508        output.push_str(&fo.body);
509    }
510
511    output.push_str("}\n");
512    if display {
513        output.push_str(&generate_display_impl(
514            &model.name,
515            &model.custom_attrs,
516            "        write!(f, \"{:?}\", self)\n",
517        ));
518    }
519    output.push('\n');
520    Ok(output)
521}
522
523fn generate_request_model(request: &RequestModel) -> Result<String> {
524    let mut output = String::new();
525
526    if request.name.is_empty() || request.name == EMPTY_REQUEST_NAME {
527        return Ok(String::new());
528    }
529
530    output.push_str(&format!("/// {}\n", request.name));
531    output.push_str("#[derive(Debug, Clone, Serialize)]\n");
532    output.push_str(&format!("pub struct {} {{\n", request.name));
533    output.push_str(&format!("    pub body: {},\n", request.schema));
534    output.push_str("}\n");
535    Ok(output)
536}
537
538fn generate_response_model(response: &ResponseModel) -> Result<String> {
539    if response.name.is_empty() || response.name == EMPTY_RESPONSE_NAME {
540        return Ok(String::new());
541    }
542
543    let type_name = format!("{}{}", response.name, response.status_code);
544
545    let mut output = String::new();
546
547    output.push_str(&generate_description_docs(
548        &response.description,
549        &type_name,
550        "",
551    ));
552
553    output.push_str("#[derive(Debug, Clone, Deserialize)]\n");
554    output.push_str(&format!("pub struct {type_name} {{\n"));
555    output.push_str(&format!("    pub body: {},\n", response.schema));
556    output.push_str("}\n");
557
558    Ok(output)
559}
560
561fn generate_union(union: &UnionModel, display: bool) -> Result<String> {
562    let mut output = String::new();
563
564    output.push_str(&format!(
565        "/// {} ({})\n",
566        union.name,
567        match union.union_type {
568            UnionType::OneOf => "oneOf",
569            UnionType::AnyOf => "anyOf",
570        }
571    ));
572    output.push_str(&generate_custom_attrs(&union.custom_attrs));
573
574    // Only add default derive if custom_attrs doesn't already contain a derive
575    if !has_custom_derive(&union.custom_attrs) {
576        output.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
577    }
578
579    // Only add default serde(untagged) if custom_attrs doesn't already contain a serde attribute
580    if !has_custom_serde(&union.custom_attrs) {
581        output.push_str("#[serde(untagged)]\n");
582    }
583
584    output.push_str(&format!("pub enum {} {{\n", union.name));
585
586    for variant in &union.variants {
587        match &variant.primitive_type {
588            Some(t) => output.push_str(&format!("    {}({}),\n", variant.name, t)),
589            None => output.push_str(&format!("    {}({}),\n", variant.name, variant.name)),
590        }
591    }
592
593    output.push_str("}\n");
594
595    if display {
596        let match_arms = union
597            .variants
598            .iter()
599            .map(|v| {
600                format!(
601                    "            Self::{}(inner) => write!(f, \"{{}}\", inner),\n",
602                    v.name
603                )
604            })
605            .collect::<String>();
606        output.push_str(&generate_display_impl(
607            &union.name,
608            &union.custom_attrs,
609            &format!("        match self {{\n{match_arms}        }}\n"),
610        ));
611    }
612
613    Ok(output)
614}
615
616fn generate_composition(
617    comp: &CompositionModel,
618    required_uses: &mut RequiredUses,
619    display: bool,
620) -> Result<String> {
621    let mut output = String::new();
622
623    output.push_str(&format!("/// {} (allOf composition)\n", comp.name));
624    output.push_str(&generate_custom_attrs(&comp.custom_attrs));
625
626    // Only add default derive if custom_attrs doesn't already contain a derive
627    if !has_custom_derive(&comp.custom_attrs) {
628        output.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
629    }
630
631    output.push_str(&format!("pub struct {} {{\n", comp.name));
632
633    for field in &comp.all_fields {
634        let field_type = match field.field_type.as_str() {
635            "String" => "String",
636            "f64" => "f64",
637            "i64" => "i64",
638            "bool" => "bool",
639            "DateTime" => {
640                *required_uses |= RequiredUses::DATETIME;
641                "DateTime<Utc>"
642            }
643            "Date" => {
644                *required_uses |= RequiredUses::DATE;
645                "NaiveDate"
646            }
647            "Uuid" => {
648                *required_uses |= RequiredUses::UUID;
649                "Uuid"
650            }
651            _ => &field.field_type,
652        };
653
654        let mut lowercased_name = to_snake_case(field.name.as_str());
655        if is_reserved_word(&lowercased_name) {
656            lowercased_name = format!("r#{lowercased_name}");
657        }
658
659        // Only add serde rename if the Rust field name differs from the original field name
660        if lowercased_name != field.name {
661            output.push_str(&format!("    #[serde(rename = \"{}\")]\n", field.name));
662        }
663
664        // Field-level custom attributes (e.g. #[serde(rename = "...")])
665        if let Some(attrs) = &field.custom_attrs {
666            for attr in attrs {
667                output.push_str(&format!("    {attr}\n"));
668            }
669        }
670
671        // If field references an array, wrap it in Vec<>
672        if field.is_array_ref {
673            if field.is_required && !field.is_nullable {
674                output.push_str(&format!("    pub {lowercased_name}: Vec<{field_type}>,\n",));
675            } else {
676                output.push_str(&format!(
677                    "    pub {lowercased_name}: Option<Vec<{field_type}>>,\n",
678                ));
679            }
680        } else if field.is_required && !field.is_nullable {
681            output.push_str(&format!("    pub {lowercased_name}: {field_type},\n",));
682        } else {
683            output.push_str(&format!(
684                "    pub {lowercased_name}: Option<{field_type}>,\n",
685            ));
686        }
687    }
688
689    output.push_str("}\n");
690    if display {
691        output.push_str(&generate_display_impl(
692            &comp.name,
693            &comp.custom_attrs,
694            "        write!(f, \"{:?}\", self)\n",
695        ));
696    }
697    Ok(output)
698}
699
700fn generate_enum(enum_model: &EnumModel, display: bool) -> Result<String> {
701    let mut output = String::new();
702
703    output.push_str(&generate_description_docs(
704        &enum_model.description,
705        &enum_model.name,
706        "",
707    ));
708
709    output.push_str(&generate_custom_attrs(&enum_model.custom_attrs));
710
711    // Only add default derive if custom_attrs doesn't already contain a derive
712    if !has_custom_derive(&enum_model.custom_attrs) {
713        output.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
714    }
715
716    output.push_str(&format!("pub enum {} {{\n", enum_model.name));
717
718    // Collect (rust_name, display_value) pairs for the Display impl below.
719    let mut variant_display: Vec<(String, String)> = Vec::new();
720
721    for (i, variant) in enum_model.variants.iter().enumerate() {
722        let original = variant.clone();
723
724        let mut rust_name = crate::parser::to_pascal_case(variant);
725
726        let serde_rename = if is_reserved_word(&rust_name) {
727            rust_name.push_str("Value");
728            Some(original.clone())
729        } else if rust_name != original {
730            Some(original.clone())
731        } else {
732            None
733        };
734
735        let display_value = serde_rename
736            .as_deref()
737            .unwrap_or(&original)
738            .replace('\\', "\\\\")
739            .replace('"', "\\\"");
740        variant_display.push((rust_name.clone(), display_value));
741
742        if let Some(rename) = serde_rename {
743            output.push_str(&format!("    #[serde(rename = \"{rename}\")]\n"));
744        }
745
746        if i + 1 == enum_model.variants.len() {
747            output.push_str(&format!("    {rust_name}\n"));
748        } else {
749            output.push_str(&format!("    {rust_name},\n"));
750        }
751    }
752
753    output.push_str("}\n");
754
755    if display {
756        let match_arms = variant_display
757            .iter()
758            .map(|(rust_name, display_value)| {
759                format!("            Self::{rust_name} => write!(f, \"{display_value}\"),\n")
760            })
761            .collect::<String>();
762        output.push_str(&generate_display_impl(
763            &enum_model.name,
764            &enum_model.custom_attrs,
765            &format!("        match self {{\n{match_arms}        }}\n"),
766        ));
767    }
768
769    Ok(output)
770}
771
772fn generate_type_alias(type_alias: &TypeAliasModel) -> Result<String> {
773    let mut output = String::new();
774
775    output.push_str(&generate_description_docs(
776        &type_alias.description,
777        &type_alias.name,
778        "",
779    ));
780
781    output.push_str(&generate_custom_attrs(&type_alias.custom_attrs));
782    output.push_str(&format!(
783        "pub type {} = {};\n\n",
784        type_alias.name, type_alias.target_type
785    ));
786
787    Ok(output)
788}
789
790pub fn generate_rust_code(models: &[Model]) -> Result<String> {
791    let mut code = create_header();
792
793    code.push_str("use serde::{Serialize, Deserialize};\n");
794    code.push_str("use uuid::Uuid;\n");
795    code.push_str("use chrono::{DateTime, NaiveDate, Utc};\n\n");
796
797    for model in models {
798        code.push_str(&format!("/// {}\n", model.name));
799        code.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
800        code.push_str(&format!("pub struct {} {{\n", model.name));
801
802        for field in &model.fields {
803            let field_type = match field.field_type.as_str() {
804                "String" => "String",
805                "f64" => "f64",
806                "i64" => "i64",
807                "bool" => "bool",
808                "DateTime" => "DateTime<Utc>",
809                "Date" => "NaiveDate",
810                "Uuid" => "Uuid",
811                _ => &field.field_type,
812            };
813
814            let mut lowercased_name = to_snake_case(field.name.as_str());
815            if is_reserved_word(&lowercased_name) {
816                lowercased_name = format!("r#{lowercased_name}")
817            }
818
819            // Only add serde rename if the Rust field name differs from the original field name
820            if lowercased_name != field.name {
821                code.push_str(&format!("    #[serde(rename = \"{}\")]\n", field.name));
822            }
823
824            if field.is_required {
825                code.push_str(&format!("    pub {lowercased_name}: {field_type},\n",));
826            } else {
827                code.push_str(&format!(
828                    "    pub {lowercased_name}: Option<{field_type}>,\n",
829                ));
830            }
831        }
832
833        code.push_str("}\n\n");
834    }
835
836    Ok(code)
837}
838
839pub fn generate_lib() -> Result<String> {
840    let mut code = create_header();
841    code.push_str("pub mod models;\n");
842
843    Ok(code)
844}
845
846#[cfg(test)]
847mod tests {
848    use super::*;
849    use crate::models::EnumModel;
850
851    fn make_enum(variants: Vec<&str>) -> EnumModel {
852        EnumModel {
853            name: "TestEnum".to_string(),
854            description: None,
855            variants: variants.into_iter().map(String::from).collect(),
856            custom_attrs: None,
857        }
858    }
859
860    #[test]
861    fn test_enum_display_escapes_quotes_and_backslashes() {
862        // Enum values containing " or \ must be escaped in the generated write! string literal.
863        let model = make_enum(vec!["normal", r#"with"quote"#, r"with\backslash"]);
864        let output = generate_enum(&model, true).expect("generate_enum failed");
865
866        assert!(
867            output.contains(r#"write!(f, "with\"quote")"#),
868            "double quote should be escaped in Display impl:\n{output}"
869        );
870        assert!(
871            output.contains(r#"write!(f, "with\\backslash")"#),
872            "backslash should be escaped in Display impl:\n{output}"
873        );
874        assert!(
875            output.contains(r#"write!(f, "normal")"#),
876            "plain value should be unmodified:\n{output}"
877        );
878    }
879
880    #[test]
881    fn test_enum_no_display_when_flag_off() {
882        let model = make_enum(vec!["foo", "bar"]);
883        let output = generate_enum(&model, false).expect("generate_enum failed");
884        assert!(
885            !output.contains("impl std::fmt::Display"),
886            "Display impl should not be generated when display=false:\n{output}"
887        );
888    }
889
890    #[test]
891    fn test_enum_no_display_when_custom_attrs_has_display() {
892        let mut model = make_enum(vec!["foo"]);
893        model.custom_attrs = Some(vec![
894            "#[derive(derive_more::Display, Debug, Clone)]".to_string()
895        ]);
896        let output = generate_enum(&model, true).expect("generate_enum failed");
897        assert!(
898            !output.contains("impl std::fmt::Display"),
899            "Display impl should be skipped when x-rust-attrs already has Display:\n{output}"
900        );
901    }
902
903    #[test]
904    fn test_float_range_uses_float_literals() {
905        // Regression test for issue #66: f64 fields must get float literals
906        // (e.g. `min = 0.0`) so the `Validate` derive infers f64, not i32.
907        let rules = crate::models::ValidationRules {
908            minimum: Some(0.0),
909            maximum: Some(100.0),
910            ..Default::default()
911        };
912
913        // Non-optional f64
914        let mut registry = IndexMap::new();
915        let attrs = generate_validator_attrs(&rules, "f64", &mut registry);
916        assert!(
917            attrs.contains("min = 0.0"),
918            "f64 min should be a float literal:\n{attrs}"
919        );
920        assert!(
921            attrs.contains("max = 100.0"),
922            "f64 max should be a float literal:\n{attrs}"
923        );
924
925        // Optional f64
926        let mut registry = IndexMap::new();
927        let attrs = generate_validator_attrs(&rules, "Option<f64>", &mut registry);
928        assert!(
929            attrs.contains("min = 0.0") && attrs.contains("max = 100.0"),
930            "Option<f64> range should use float literals:\n{attrs}"
931        );
932    }
933
934    #[test]
935    fn test_integer_range_uses_integer_literals() {
936        // Integer fields should keep integer literals (no `.0`).
937        let rules = crate::models::ValidationRules {
938            minimum: Some(0.0),
939            maximum: Some(100.0),
940            ..Default::default()
941        };
942
943        let mut registry = IndexMap::new();
944        let attrs = generate_validator_attrs(&rules, "i64", &mut registry);
945        assert!(
946            attrs.contains("min = 0") && !attrs.contains("min = 0.0"),
947            "i64 min should be an integer literal:\n{attrs}"
948        );
949        assert!(
950            attrs.contains("max = 100") && !attrs.contains("max = 100.0"),
951            "i64 max should be an integer literal:\n{attrs}"
952        );
953    }
954
955    #[test]
956    fn test_pattern_generates_validate_regex_with_static() {
957        // Regression test for issue #67: pattern must emit `#[validate(regex(path = ...))]`
958        // referencing a registered static, not the invalid `#[regex(...)]` attribute.
959        let rules = crate::models::ValidationRules {
960            pattern: Some(r"^[a-f0-9]{64}$".to_string()),
961            ..Default::default()
962        };
963
964        let mut registry = IndexMap::new();
965        let attrs = generate_validator_attrs(&rules, "String", &mut registry);
966
967        assert!(
968            attrs.contains("#[validate(regex(path = RE_1))]"),
969            "should emit validate(regex(path = ...)):\n{attrs}"
970        );
971        assert!(
972            !attrs.contains("#[regex("),
973            "must not emit the invalid #[regex(...)] attribute:\n{attrs}"
974        );
975        assert_eq!(registry.len(), 1, "pattern should be registered");
976        assert_eq!(registry.get(r"^[a-f0-9]{64}$"), Some(&"RE_1".to_string()));
977    }
978
979    #[test]
980    fn test_identical_patterns_deduplicated() {
981        // Two fields with the same pattern must share one static.
982        let rules = crate::models::ValidationRules {
983            pattern: Some(r"^[a-z]+$".to_string()),
984            ..Default::default()
985        };
986
987        let mut registry = IndexMap::new();
988        let _ = generate_validator_attrs(&rules, "String", &mut registry);
989        let attrs2 = generate_validator_attrs(&rules, "String", &mut registry);
990
991        assert_eq!(
992            registry.len(),
993            1,
994            "identical patterns must share one static"
995        );
996        assert!(
997            attrs2.contains("#[validate(regex(path = RE_1))]"),
998            "second field should reuse RE_1:\n{attrs2}"
999        );
1000    }
1001
1002    #[test]
1003    fn test_distinct_patterns_get_distinct_statics() {
1004        let rules_a = crate::models::ValidationRules {
1005            pattern: Some(r"^[a-z]+$".to_string()),
1006            ..Default::default()
1007        };
1008        let rules_b = crate::models::ValidationRules {
1009            pattern: Some(r"^[0-9]+$".to_string()),
1010            ..Default::default()
1011        };
1012
1013        let mut registry = IndexMap::new();
1014        let attrs_a = generate_validator_attrs(&rules_a, "String", &mut registry);
1015        let attrs_b = generate_validator_attrs(&rules_b, "String", &mut registry);
1016
1017        assert!(
1018            attrs_a.contains("#[validate(regex(path = RE_1))]")
1019                && attrs_b.contains("#[validate(regex(path = RE_2))]"),
1020            "distinct patterns must get distinct statics:\n{attrs_a}\n{attrs_b}"
1021        );
1022    }
1023}