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 pub struct GenerateMode: u8 {
24 const MODELS = 0;
26 const REQUESTS = 1 << 0;
28 const RESPONSES = 1 << 1;
30 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
127fn 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
138fn 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
147fn generate_display_impl(name: &str, custom_attrs: &Option<Vec<String>>, body: &str) -> String {
151 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
168fn 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 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 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 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 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
286fn 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
296fn 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 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 if !has_custom_derive(&union.custom_attrs) {
576 output.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
577 }
578
579 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 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 if lowercased_name != field.name {
661 output.push_str(&format!(" #[serde(rename = \"{}\")]\n", field.name));
662 }
663
664 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.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 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 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 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 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 let rules = crate::models::ValidationRules {
908 minimum: Some(0.0),
909 maximum: Some(100.0),
910 ..Default::default()
911 };
912
913 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 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 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 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 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}