1use std::collections::HashSet;
14
15use crate::codegen::traits::file_writer::FileInfo;
16use crate::ir::types::{
17 IrEnum, IrEnumValueType, IrIntersection, IrObject, IrPrimitive, IrSchema, IrSchemaKind, IrSpec,
18 IrTaggedUnion, IrTypeExpr, IrUnion, TaggingStyle,
19};
20use heck::{ToPascalCase, ToSnakeCase};
21use sigil_stitch::prelude::{CodeBlock, sigil_quote};
22use sigil_stitch::spec::file_spec::FileSpec;
23use sigil_stitch::spec::import_spec::ImportSpec;
24use sigil_stitch::type_name::TypeName;
25
26use super::config::{ExtraDeriveConfig, RustGeneratorConfig};
27
28pub fn generate_model_files(
30 ir: &IrSpec,
31 header: &str,
32 config: &RustGeneratorConfig,
33) -> Result<Vec<FileInfo>, String> {
34 let mut files = Vec::new();
35 let mut mod_entries = Vec::new();
36
37 let utoipa_enabled = config.utoipa.as_ref().is_some_and(|u| u.enabled);
38
39 let inlined_schemas: HashSet<&str> = if utoipa_enabled {
43 HashSet::new()
44 } else {
45 let inlined_candidates: HashSet<&str> = ir
46 .schemas
47 .values()
48 .filter_map(|s| match &s.kind {
49 IrSchemaKind::TaggedUnion(tu)
50 if matches!(
51 tu.tagging,
52 TaggingStyle::Internal | TaggingStyle::Adjacent { .. }
53 ) =>
54 {
55 Some(tu.variants.iter().filter_map(|v| {
56 if let IrTypeExpr::Named(name) = &v.content_type
57 && ir
58 .schemas
59 .get(name)
60 .is_some_and(|s| matches!(s.kind, IrSchemaKind::Object(_)))
61 {
62 return Some(name.as_str());
63 }
64 None
65 }))
66 }
67 _ => None,
68 })
69 .flatten()
70 .collect();
71
72 let referenced_by_name: HashSet<&str> = ir
73 .schemas
74 .values()
75 .flat_map(|s| collect_named_type_refs(s))
76 .collect();
77
78 inlined_candidates
79 .difference(&referenced_by_name)
80 .copied()
81 .collect()
82 };
83
84 for (_name, schema) in &ir.schemas {
85 if inlined_schemas.contains(schema.name.as_str()) {
86 continue;
87 }
88 let Some(file_spec) = emit_model_file(schema, config, ir) else {
89 continue;
90 };
91 let stem = schema.name.to_snake_case();
92 let filename = format!("{stem}.rs");
93 mod_entries.push(stem);
94
95 let rendered = file_spec
96 .render(100)
97 .map_err(|e| format!("render error for {}: {e}", schema.name))?;
98
99 let mut content = String::with_capacity(header.len() + rendered.len());
100 content.push_str(header);
101 content.push_str(&rendered);
102 files.push(FileInfo::model(filename, content));
103 }
104
105 let mut mod_content = String::from(header);
107 for entry in &mod_entries {
108 mod_content.push_str(&format!("mod {entry};\npub use {entry}::*;\n"));
109 }
110 files.push(FileInfo::model("mod.rs".to_string(), mod_content));
111
112 Ok(files)
113}
114
115fn emit_model_file(
116 schema: &IrSchema,
117 config: &RustGeneratorConfig,
118 ir: &IrSpec,
119) -> Option<FileSpec> {
120 let extra = config.extra_derives.as_ref();
121 let utoipa_enabled = config.utoipa.as_ref().is_some_and(|u| u.enabled);
122 let per_type_cfg = extra
123 .and_then(|e| e.per_type.as_ref())
124 .and_then(|m| m.get(&schema.name));
125 match &schema.kind {
126 IrSchemaKind::Object(obj) => {
127 let derives = merge_utoipa(
128 per_type_cfg.or_else(|| extra.and_then(|e| e.structs.as_ref())),
129 utoipa_enabled,
130 );
131 emit_object(schema, obj, derives.as_ref())
132 }
133 IrSchemaKind::Enum(en) => {
134 let derives = merge_utoipa(
135 per_type_cfg.or_else(|| extra.and_then(|e| e.enums.as_ref())),
136 utoipa_enabled,
137 );
138 emit_enum(schema, en, derives.as_ref())
139 }
140 IrSchemaKind::Alias(expr) => {
141 let derives = merge_utoipa(
142 per_type_cfg.or_else(|| extra.and_then(|e| e.structs.as_ref())),
143 utoipa_enabled,
144 );
145 emit_alias(schema, expr, derives.as_ref())
146 }
147 IrSchemaKind::Union(u) => emit_union(schema, u, per_type_cfg, utoipa_enabled),
148 IrSchemaKind::Intersection(i) => {
149 let derives = merge_utoipa(
150 per_type_cfg.or_else(|| extra.and_then(|e| e.structs.as_ref())),
151 utoipa_enabled,
152 );
153 emit_intersection(schema, i, derives.as_ref())
154 }
155 IrSchemaKind::TaggedUnion(tu) => {
156 let derives = per_type_cfg
157 .or_else(|| extra.and_then(|e| e.unions.as_ref()))
158 .cloned();
159 emit_tagged_union(schema, tu, derives.as_ref(), ir, utoipa_enabled)
160 }
161 }
162}
163
164fn derive_attr(base: &str, extra: Option<&ExtraDeriveConfig>) -> String {
169 match extra {
170 Some(cfg) if !cfg.derives.is_empty() => {
171 let base_set: Vec<&str> = base.split(", ").collect();
172 let unique_extra: Vec<&str> = cfg
173 .derives
174 .iter()
175 .map(|s| s.as_str())
176 .filter(|d| !base_set.contains(d))
177 .collect();
178 if unique_extra.is_empty() {
179 format!("derive({base})")
180 } else {
181 format!("derive({base}, {})", unique_extra.join(", "))
182 }
183 }
184 _ => format!("derive({base})"),
185 }
186}
187
188fn merge_utoipa(
189 base: Option<&ExtraDeriveConfig>,
190 utoipa_enabled: bool,
191) -> Option<ExtraDeriveConfig> {
192 if !utoipa_enabled {
193 return base.cloned();
194 }
195 let mut cfg = base.cloned().unwrap_or_default();
196 if !cfg.derives.iter().any(|d| d.contains("utoipa")) {
197 cfg.derives.push("utoipa::ToSchema".to_string());
198 }
199 Some(cfg)
200}
201
202fn emit_object(
207 schema: &IrSchema,
208 obj: &IrObject,
209 extra: Option<&ExtraDeriveConfig>,
210) -> Option<FileSpec> {
211 let name = schema.name.to_pascal_case();
212 let stem = schema.name.to_snake_case();
213
214 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
215 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
216 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
217
218 let body = sigil_quote!(RustLang {
219 $if(schema.description.is_some()) {
220 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
221 }
222 $attr(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
223 pub struct $N(name.as_str()) {
224 $for((json_name, prop) in obj.properties.iter()) {
225 $if(prop.description.is_some()) {
226 $L(doc_comment_block(prop.description.as_deref().unwrap()).trim_end())
227 }
228 $if(escape_rust_keyword(&json_name.to_snake_case()) != *json_name) {
229 #[serde(rename = $S(json_name))]
230 }
231 $if(!prop.required || prop.nullable) {
232 #[serde(skip_serializing_if = "Option::is_none", default)]
233 pub $L("@{escape_rust_keyword(&json_name.to_snake_case())}: Option<@{rust_type_str_model(&prop.type_expr)}>,")
234 } $else {
235 pub $L("@{escape_rust_keyword(&json_name.to_snake_case())}: @{rust_type_str_model(&prop.type_expr)},")
236 }
237 }
238 $if(obj.additional_properties.is_some()) {
239 #[serde(flatten)]
240 pub $L("additional_properties: std::collections::HashMap<String, @{rust_type_str_model(obj.additional_properties.as_ref().unwrap())}>,")
241 }
242 }
243 })
244 .ok()?;
245
246 fsb = fsb.add_code(body);
247 fsb.build().ok()
248}
249
250fn emit_enum(
255 schema: &IrSchema,
256 en: &IrEnum,
257 extra: Option<&ExtraDeriveConfig>,
258) -> Option<FileSpec> {
259 let name = schema.name.to_pascal_case();
260
261 match en.value_type {
262 IrEnumValueType::Mixed | IrEnumValueType::Number => {
263 return emit_type_alias_file(schema, "serde_json::Value");
264 }
265 IrEnumValueType::Integer => {
266 return emit_integer_enum(schema, en, extra);
267 }
268 IrEnumValueType::String => {}
269 }
270
271 let stem = schema.name.to_snake_case();
272 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
273 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
274 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
275
276 let mut variants: Vec<(String, String)> = Vec::new();
277 for v in &en.values {
278 let s = v.value.as_str()?;
279 variants.push((s.to_pascal_case(), s.to_string()));
280 }
281
282 let body = sigil_quote!(RustLang {
283 $if(schema.description.is_some()) {
284 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
285 }
286 $attr(derive_attr("Debug, Clone, PartialEq, Eq, Serialize, Deserialize", extra))
287 pub enum $N(name.as_str()) {
288 $for((variant, wire) in variants.iter()) {
289 $if(variant != wire) {
290 #[serde(rename = $S(wire))]
291 }
292 $N(variant.as_str()),
293 }
294 }
295 })
296 .ok()?;
297
298 fsb = fsb.add_code(body);
299
300 if let Some(display_block) = build_string_enum_display(&name, &variants) {
302 fsb = fsb.add_code(display_block);
303 }
304
305 fsb.build().ok()
306}
307
308fn emit_integer_enum(
309 schema: &IrSchema,
310 en: &IrEnum,
311 extra: Option<&ExtraDeriveConfig>,
312) -> Option<FileSpec> {
313 let name = schema.name.to_pascal_case();
314 let stem = schema.name.to_snake_case();
315
316 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
317 fsb = fsb.add_import(ImportSpec::named("serde_repr", "Deserialize_repr"));
318 fsb = fsb.add_import(ImportSpec::named("serde_repr", "Serialize_repr"));
319
320 let int_variants: Vec<(String, i64)> = en
321 .values
322 .iter()
323 .map(|v| {
324 let n = v.value.as_i64()?;
325 let variant_name = if n < 0 {
326 format!("Neg{}", n.unsigned_abs())
327 } else {
328 format!("N{n}")
329 };
330 Some((variant_name, n))
331 })
332 .collect::<Option<Vec<_>>>()?;
333
334 let body = sigil_quote!(RustLang {
335 $if(schema.description.is_some()) {
336 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
337 }
338 $attr(derive_attr("Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr", extra))
339 #[repr(i64)]
340 pub enum $N(name.as_str()) {
341 $for((variant_name, n) in int_variants.iter()) {
342 $N(variant_name.as_str()) = $L(n.to_string()),
343 }
344 }
345 })
346 .ok()?;
347
348 fsb = fsb.add_code(body);
349
350 if let Some(display_block) = build_integer_enum_display(&name) {
352 fsb = fsb.add_code(display_block);
353 }
354
355 fsb.build().ok()
356}
357
358fn emit_alias(
363 schema: &IrSchema,
364 expr: &IrTypeExpr,
365 extra: Option<&ExtraDeriveConfig>,
366) -> Option<FileSpec> {
367 if let IrTypeExpr::Named(n) = expr
368 && n.to_pascal_case() == schema.name.to_pascal_case()
369 {
370 return None;
371 }
372 if schema.name.to_pascal_case() == rust_type_str_model(expr) {
374 return None;
375 }
376
377 let name = schema.name.to_pascal_case();
378 let stem = schema.name.to_snake_case();
379 let rhs = rust_type_str_model(expr);
380
381 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
382
383 let has_extra = extra.is_some_and(|cfg| !cfg.derives.is_empty());
384
385 if has_extra {
386 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
387 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
388
389 let rhs_type = TypeName::raw(&rhs);
390 let block = sigil_quote!(RustLang {
391 $if(schema.description.is_some()) {
392 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
393 }
394 $attr(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
395 #[serde(transparent)]
396 pub struct $N(name.as_str())(pub $T(rhs_type));
397 })
398 .ok()?;
399 fsb = fsb.add_code(block);
400 } else {
401 let rhs_type = TypeName::raw(&rhs);
402 let block = sigil_quote!(RustLang {
403 $if(schema.description.is_some()) {
404 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
405 }
406 pub type $N(name.as_str()) = $T(rhs_type);
407 })
408 .ok()?;
409 fsb = fsb.add_code(block);
410 }
411
412 fsb.build().ok()
413}
414
415fn emit_type_alias_file(schema: &IrSchema, rhs_str: &str) -> Option<FileSpec> {
416 let name = schema.name.to_pascal_case();
417 let stem = schema.name.to_snake_case();
418
419 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
420
421 let rhs_type = TypeName::raw(rhs_str);
422 let block = sigil_quote!(RustLang {
423 $if(schema.description.is_some()) {
424 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
425 }
426 pub type $N(name.as_str()) = $T(rhs_type);
427 })
428 .ok()?;
429 fsb = fsb.add_code(block);
430
431 fsb.build().ok()
432}
433
434fn emit_union(
439 schema: &IrSchema,
440 union: &IrUnion,
441 extra: Option<&ExtraDeriveConfig>,
442 utoipa_enabled: bool,
443) -> Option<FileSpec> {
444 let name = schema.name.to_pascal_case();
445 let stem = schema.name.to_snake_case();
446
447 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
448 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
449 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
450
451 let variants: Vec<(String, String)> = union
452 .members
453 .iter()
454 .enumerate()
455 .map(|(i, member)| {
456 let variant_name = union_variant_name(member, i);
457 let rust_type = rust_type_str_model(member);
458 (variant_name, rust_type)
459 })
460 .collect();
461
462 let body = sigil_quote!(RustLang {
463 $if(schema.description.is_some()) {
464 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
465 }
466 $attr(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
467 #[serde(untagged)]
468 pub enum $N(name.as_str()) {
469 $for((variant_name, rust_type) in variants.iter()) {
470 $N(variant_name.as_str())($L(rust_type.as_str())),
471 }
472 }
473 })
474 .ok()?;
475
476 fsb = fsb.add_code(body);
477
478 if utoipa_enabled {
479 let variant_types: Vec<String> = variants
480 .iter()
481 .map(|(_, rust_type)| rust_type.clone())
482 .collect();
483 let impl_block = build_utoipa_one_of_impl(&name, &variant_types)?;
484 fsb = fsb.add_code(impl_block);
485 }
486
487 fsb.build().ok()
488}
489
490fn union_variant_name(expr: &IrTypeExpr, index: usize) -> String {
491 match expr {
492 IrTypeExpr::Named(n) => n.to_pascal_case(),
493 IrTypeExpr::Primitive(p) => primitive_variant_name(p),
494 IrTypeExpr::Array(_) => format!("Array{index}"),
495 IrTypeExpr::Map(_) => format!("Map{index}"),
496 _ => format!("Variant{index}"),
497 }
498}
499
500fn primitive_variant_name(p: &IrPrimitive) -> String {
501 match p {
502 IrPrimitive::String | IrPrimitive::StringWithFormat(_) => "String".to_string(),
503 IrPrimitive::Integer | IrPrimitive::IntegerWithFormat(_) => "Integer".to_string(),
504 IrPrimitive::Number | IrPrimitive::NumberWithFormat(_) => "Number".to_string(),
505 IrPrimitive::Boolean => "Boolean".to_string(),
506 IrPrimitive::Binary => "Binary".to_string(),
507 IrPrimitive::Date => "Date".to_string(),
508 IrPrimitive::DateTime => "DateTime".to_string(),
509 IrPrimitive::Uuid => "Uuid".to_string(),
510 }
511}
512
513fn emit_tagged_union(
518 schema: &IrSchema,
519 tu: &IrTaggedUnion,
520 extra: Option<&ExtraDeriveConfig>,
521 ir: &IrSpec,
522 utoipa_enabled: bool,
523) -> Option<FileSpec> {
524 if tu.variants.is_empty() {
525 return None;
526 }
527
528 let name = schema.name.to_pascal_case();
529 let stem = schema.name.to_snake_case();
530
531 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
532 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
533 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
534
535 let serde_tag_attr = match &tu.tagging {
536 TaggingStyle::Internal => {
537 format!("serde(tag = \"{}\")", escape_str(&tu.discriminator_field))
538 }
539 TaggingStyle::Adjacent { content_field } => {
540 format!(
541 "serde(tag = \"{}\", content = \"{}\")",
542 escape_str(&tu.discriminator_field),
543 escape_str(content_field)
544 )
545 }
546 TaggingStyle::External => String::new(),
547 };
548
549 let inline_fields = matches!(
550 tu.tagging,
551 TaggingStyle::Internal | TaggingStyle::Adjacent { .. }
552 );
553
554 let mut variant_blocks: Vec<CodeBlock> = Vec::new();
555 for variant in &tu.variants {
556 let variant_name = variant.discriminator_value.to_pascal_case();
557
558 let rename_attr = if variant_name != variant.discriminator_value {
559 format!(
560 "serde(rename = \"{}\")",
561 escape_str(&variant.discriminator_value)
562 )
563 } else {
564 String::new()
565 };
566
567 let variant_block = if inline_fields {
568 if let Some(obj) = resolve_object(&variant.content_type, ir) {
569 let non_tag_fields: Vec<_> = obj
570 .properties
571 .iter()
572 .filter(|(json_name, _)| *json_name != &tu.discriminator_field)
573 .collect();
574 let has_additional = obj.additional_properties.is_some();
575
576 if non_tag_fields.is_empty() && !has_additional {
577 sigil_quote!(RustLang {
578 $if(!rename_attr.is_empty()) {
579 $attr(rename_attr.as_str())
580 }
581 $N(variant_name.as_str()),
582 })
583 .ok()?
584 } else {
585 let field_blocks: Vec<CodeBlock> = non_tag_fields
586 .iter()
587 .map(|(json_name, prop)| {
588 let field_name = escape_rust_keyword(&json_name.to_snake_case());
589 let ty = rust_type_str_model(&prop.type_expr);
590 let needs_rename = field_name != **json_name;
591 let optional = !prop.required || prop.nullable;
592 sigil_quote!(RustLang {
593 $if(needs_rename) {
594 #[serde(rename = $S(json_name.as_str()))]
595 }
596 $if(optional) {
597 #[serde(skip_serializing_if = "Option::is_none", default)]
598 $L(format!("{field_name}: Option<{ty}>,"))
599 }
600 $if(!optional) {
601 $L(format!("{field_name}: {ty},"))
602 }
603 })
604 .ok()
605 })
606 .collect::<Option<_>>()?;
607
608 let additional_blocks: Vec<CodeBlock> = if let Some(ap) =
609 &obj.additional_properties
610 {
611 let ty = rust_type_str_model(ap);
612 vec![sigil_quote!(RustLang {
613 #[serde(flatten)]
614 $L(format!("additional_properties: std::collections::HashMap<String, {ty}>,"))
615 })
616 .ok()?]
617 } else {
618 vec![]
619 };
620
621 let mut cb = CodeBlock::builder();
622 if !rename_attr.is_empty() {
623 cb.add(&format!("#[{rename_attr}]"), ());
624 cb.add_line();
625 }
626 cb.add(&format!("{variant_name} {{\n%>"), ());
627 for fb in &field_blocks {
628 cb.add_code(fb.clone());
629 }
630 for ab in &additional_blocks {
631 cb.add_code(ab.clone());
632 }
633 cb.add("%<},", ());
634 cb.build().ok()?
635 }
636 } else {
637 let content_ty = rust_type_str_model(&variant.content_type);
638 sigil_quote!(RustLang {
639 $if(!rename_attr.is_empty()) {
640 $attr(rename_attr.as_str())
641 }
642 $N(variant_name.as_str())($L(content_ty.as_str())),
643 })
644 .ok()?
645 }
646 } else {
647 let content_ty = rust_type_str_model(&variant.content_type);
648 sigil_quote!(RustLang {
649 $if(!rename_attr.is_empty()) {
650 $attr(rename_attr.as_str())
651 }
652 $N(variant_name.as_str())($L(content_ty.as_str())),
653 })
654 .ok()?
655 };
656
657 variant_blocks.push(variant_block);
658 }
659
660 let body = sigil_quote!(RustLang {
661 $if(schema.description.is_some()) {
662 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
663 }
664 $attr(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
665 $if(!serde_tag_attr.is_empty()) {
666 $attr(serde_tag_attr.as_str())
667 }
668 pub enum $N(name.as_str()) {
669 $C_each(variant_blocks);
670 }
671 })
672 .ok()?;
673
674 fsb = fsb.add_code(body);
675
676 if utoipa_enabled {
677 let variant_types: Vec<String> = tu
678 .variants
679 .iter()
680 .map(|v| rust_type_str_model(&v.content_type))
681 .collect();
682 let impl_block = build_utoipa_one_of_impl(&name, &variant_types)?;
683 fsb = fsb.add_code(impl_block);
684 }
685
686 fsb.build().ok()
687}
688
689fn emit_intersection(
694 schema: &IrSchema,
695 inter: &IrIntersection,
696 extra: Option<&ExtraDeriveConfig>,
697) -> Option<FileSpec> {
698 let name = schema.name.to_pascal_case();
699 let stem = schema.name.to_snake_case();
700
701 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
702 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
703 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
704
705 let fields: Vec<(String, String)> = inter
706 .members
707 .iter()
708 .enumerate()
709 .map(|(i, member)| {
710 let raw_name = match member {
711 IrTypeExpr::Named(n) => n.to_snake_case(),
712 _ => format!("member_{i}"),
713 };
714 let field_name = escape_rust_keyword(&raw_name);
715 let rust_type = rust_type_str_model(member);
716 (field_name, rust_type)
717 })
718 .collect();
719
720 let body = sigil_quote!(RustLang {
721 $if(schema.description.is_some()) {
722 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
723 }
724 $attr(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
725 pub struct $N(name.as_str()) {
726 $for((field_name, rust_type) in fields.iter()) {
727 #[serde(flatten)]
728 pub $N(field_name.as_str()): $L(rust_type.as_str()),
729 }
730 }
731 })
732 .ok()?;
733
734 fsb = fsb.add_code(body);
735 fsb.build().ok()
736}
737
738fn build_utoipa_one_of_impl(name: &str, variant_types: &[String]) -> Option<CodeBlock> {
743 let items: Vec<String> = variant_types
744 .iter()
745 .map(|rt| format!(" .item(<{rt} as utoipa::PartialSchema>::schema())"))
746 .collect();
747
748 sigil_quote!(RustLang {
749 impl utoipa::PartialSchema for $N(name) {
750 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
751 $L("utoipa::openapi::schema::Schema::OneOf(utoipa::openapi::schema::OneOfBuilder::new()")
752 $for(item in items.iter()) {
753 $L(item.as_str())
754 }
755 $L(" .build()).into()")
756 }
757 }
758
759 impl utoipa::ToSchema for $N(name) {
760 fn name() -> std::borrow::Cow<'static, str> {
761 std::borrow::Cow::Borrowed($S(name))
762 }
763 }
764 })
765 .ok()
766}
767
768fn build_integer_enum_display(name: &str) -> Option<CodeBlock> {
773 sigil_quote!(RustLang {
774 impl std::fmt::Display for $N(name) {
775 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
776 write!(f, "{}", *self as i64)
777 }
778 }
779 })
780 .ok()
781}
782
783fn build_string_enum_display(name: &str, variants: &[(String, String)]) -> Option<CodeBlock> {
784 sigil_quote!(RustLang {
785 impl std::fmt::Display for $N(name) {
786 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
787 match self {
788 $for((variant, wire_value) in variants.iter()) {
789 $N(name)::$N(variant.as_str()) => write!(f, $S(wire_value)),
790 }
791 }
792 }
793 }
794 })
795 .ok()
796}
797
798fn doc_comment_block(doc: &str) -> String {
804 let mut out = String::new();
805 for line in doc.lines() {
806 if line.is_empty() {
807 out.push_str("///\n");
808 } else {
809 out.push_str(&format!("/// {line}\n"));
810 }
811 }
812 out
813}
814
815fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> {
820 if let IrTypeExpr::Named(name) = expr
821 && let Some(schema) = ir.schemas.get(name)
822 && let IrSchemaKind::Object(obj) = &schema.kind
823 {
824 return Some(obj);
825 }
826 None
827}
828
829pub fn rust_type_str(expr: &IrTypeExpr) -> String {
834 match expr {
835 IrTypeExpr::Named(name) => name.to_pascal_case(),
836 IrTypeExpr::Primitive(p) => rust_primitive(p).to_string(),
837 IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str(inner)),
838 IrTypeExpr::Map(inner) => format!(
839 "std::collections::HashMap<String, {}>",
840 rust_type_str(inner)
841 ),
842 IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str(inner)),
843 IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => "String".to_string(),
844 IrTypeExpr::Union(_) | IrTypeExpr::Any => "serde_json::Value".to_string(),
845 }
846}
847
848pub fn rust_type_str_qualified(expr: &IrTypeExpr, ir: &IrSpec) -> String {
851 match expr {
852 IrTypeExpr::Named(name) => {
853 if let Some(schema) = ir.schemas.get(name)
854 && let IrSchemaKind::Alias(inner) = &schema.kind
855 && name.to_pascal_case() == rust_type_str_model(inner)
856 {
857 return rust_type_str(inner);
858 }
859 format!("crate::models::{}", name.to_pascal_case())
860 }
861 IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str_qualified(inner, ir)),
862 IrTypeExpr::Map(inner) => format!(
863 "std::collections::HashMap<String, {}>",
864 rust_type_str_qualified(inner, ir)
865 ),
866 IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str_qualified(inner, ir)),
867 other => rust_type_str(other),
868 }
869}
870
871fn rust_type_str_model(expr: &IrTypeExpr) -> String {
873 match expr {
874 IrTypeExpr::Named(name) => format!("super::{}", name.to_pascal_case()),
875 IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str_model(inner)),
876 IrTypeExpr::Map(inner) => format!(
877 "std::collections::HashMap<String, {}>",
878 rust_type_str_model(inner)
879 ),
880 IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str_model(inner)),
881 other => rust_type_str(other),
882 }
883}
884
885fn rust_primitive(p: &IrPrimitive) -> &'static str {
886 match p {
887 IrPrimitive::String
888 | IrPrimitive::Date
889 | IrPrimitive::DateTime
890 | IrPrimitive::Uuid
891 | IrPrimitive::StringWithFormat(_) => "String",
892 IrPrimitive::Binary => "Vec<u8>",
893 IrPrimitive::Integer => "i64",
894 IrPrimitive::IntegerWithFormat(format) => match format.as_str() {
895 "int32" => "i32",
896 "int64" => "i64",
897 "uint32" => "u32",
898 "uint64" => "u64",
899 _ => "i64",
900 },
901 IrPrimitive::Number => "f64",
902 IrPrimitive::NumberWithFormat(format) => match format.as_str() {
903 "float" => "f32",
904 _ => "f64",
905 },
906 IrPrimitive::Boolean => "bool",
907 }
908}
909
910fn escape_str(s: &str) -> String {
911 s.replace('\\', "\\\\").replace('"', "\\\"")
912}
913
914fn escape_rust_keyword(name: &str) -> String {
915 const KEYWORDS: &[&str] = &[
916 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
917 "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
918 "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
919 "true", "type", "union", "unsafe", "use", "where", "while", "yield",
920 ];
921 if KEYWORDS.contains(&name) {
922 format!("r#{name}")
923 } else {
924 name.to_string()
925 }
926}
927
928fn collect_named_type_refs(schema: &IrSchema) -> Vec<&str> {
931 let mut refs = Vec::new();
932 match &schema.kind {
933 IrSchemaKind::Object(obj) => {
934 for prop in obj.properties.values() {
935 collect_named_from_expr(&prop.type_expr, &mut refs);
936 }
937 if let Some(ap) = &obj.additional_properties {
938 collect_named_from_expr(ap, &mut refs);
939 }
940 }
941 IrSchemaKind::TaggedUnion(tu) => {
942 let uses_tuple_variants = matches!(tu.tagging, TaggingStyle::External);
943 if uses_tuple_variants {
944 for v in &tu.variants {
945 collect_named_from_expr(&v.content_type, &mut refs);
946 }
947 }
948 }
949 IrSchemaKind::Union(u) => {
950 for member in &u.members {
951 collect_named_from_expr(member, &mut refs);
952 }
953 }
954 IrSchemaKind::Alias(expr) => {
955 collect_named_from_expr(expr, &mut refs);
956 }
957 IrSchemaKind::Intersection(inter) => {
958 for member in &inter.members {
959 collect_named_from_expr(member, &mut refs);
960 }
961 }
962 IrSchemaKind::Enum(_) => {}
963 }
964 refs
965}
966
967fn collect_named_from_expr<'a>(expr: &'a IrTypeExpr, refs: &mut Vec<&'a str>) {
968 match expr {
969 IrTypeExpr::Named(name) => refs.push(name.as_str()),
970 IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => {
971 collect_named_from_expr(inner, refs);
972 }
973 IrTypeExpr::Union(members) => {
974 for m in members {
975 collect_named_from_expr(m, refs);
976 }
977 }
978 _ => {}
979 }
980}