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 $L(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 $L(format!("#[serde(rename = \"{json_name}\")]"))
230 }
231 $if(!prop.required || prop.nullable) {
232 #[serde(skip_serializing_if = "Option::is_none", default)]
233 $L(format!("pub {}: Option<{}>,", escape_rust_keyword(&json_name.to_snake_case()), rust_type_str_model(&prop.type_expr)))
234 } $else {
235 $L(format!("pub {}: {},", 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 $L(format!("pub 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 $L(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 $L(format!("#[serde(rename = \"{}\")]", escape_str(wire)))
291 }
292 $L(format!("{variant},"))
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 $L(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 $L(format!("{variant_name} = {n},"))
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 $L(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 $L(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 $L(format!("{variant_name}({rust_type}),"))
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!(
538 "#[serde(tag = \"{}\")]",
539 escape_str(&tu.discriminator_field)
540 )
541 }
542 TaggingStyle::Adjacent { content_field } => {
543 format!(
544 "#[serde(tag = \"{}\", content = \"{}\")]",
545 escape_str(&tu.discriminator_field),
546 escape_str(content_field)
547 )
548 }
549 TaggingStyle::External => String::new(),
550 };
551
552 let inline_fields = matches!(
553 tu.tagging,
554 TaggingStyle::Internal | TaggingStyle::Adjacent { .. }
555 );
556
557 let mut variant_blocks: Vec<CodeBlock> = Vec::new();
558 for variant in &tu.variants {
559 let variant_name = variant.discriminator_value.to_pascal_case();
560
561 let rename_attr = if variant_name != variant.discriminator_value {
562 format!(
563 "#[serde(rename = \"{}\")]",
564 escape_str(&variant.discriminator_value)
565 )
566 } else {
567 String::new()
568 };
569
570 let variant_block = if inline_fields {
571 if let Some(obj) = resolve_object(&variant.content_type, ir) {
572 let non_tag_fields: Vec<_> = obj
573 .properties
574 .iter()
575 .filter(|(json_name, _)| *json_name != &tu.discriminator_field)
576 .collect();
577 let has_additional = obj.additional_properties.is_some();
578
579 if non_tag_fields.is_empty() && !has_additional {
580 sigil_quote!(RustLang {
581 $if(!rename_attr.is_empty()) {
582 $L(rename_attr.as_str())
583 }
584 $L(format!("{variant_name},"))
585 })
586 .ok()?
587 } else {
588 let field_blocks: Vec<CodeBlock> = non_tag_fields
589 .iter()
590 .map(|(json_name, prop)| {
591 let field_name = escape_rust_keyword(&json_name.to_snake_case());
592 let ty = rust_type_str_model(&prop.type_expr);
593 let needs_rename = field_name != **json_name;
594 let optional = !prop.required || prop.nullable;
595 sigil_quote!(RustLang {
596 $if(needs_rename) {
597 $L(format!("#[serde(rename = \"{json_name}\")]"))
598 }
599 $if(optional) {
600 #[serde(skip_serializing_if = "Option::is_none", default)]
601 $L(format!("{field_name}: Option<{ty}>,"))
602 }
603 $if(!optional) {
604 $L(format!("{field_name}: {ty},"))
605 }
606 })
607 .ok()
608 })
609 .collect::<Option<_>>()?;
610
611 let additional_blocks: Vec<CodeBlock> = if let Some(ap) =
612 &obj.additional_properties
613 {
614 let ty = rust_type_str_model(ap);
615 vec![sigil_quote!(RustLang {
616 #[serde(flatten)]
617 $L(format!("additional_properties: std::collections::HashMap<String, {ty}>,"))
618 })
619 .ok()?]
620 } else {
621 vec![]
622 };
623
624 let mut cb = CodeBlock::builder();
625 if !rename_attr.is_empty() {
626 cb.add(&rename_attr, ());
627 cb.add_line();
628 }
629 cb.add(&format!("{variant_name} {{\n%>"), ());
630 for fb in field_blocks {
631 cb.add_code(fb);
632 }
633 for ab in additional_blocks {
634 cb.add_code(ab);
635 }
636 cb.add("%<},", ());
637 cb.build().ok()?
638 }
639 } else {
640 let content_ty = rust_type_str_model(&variant.content_type);
641 sigil_quote!(RustLang {
642 $if(!rename_attr.is_empty()) {
643 $L(rename_attr.as_str())
644 }
645 $L(format!("{variant_name}({content_ty}),"))
646 })
647 .ok()?
648 }
649 } else {
650 let content_ty = rust_type_str_model(&variant.content_type);
651 sigil_quote!(RustLang {
652 $if(!rename_attr.is_empty()) {
653 $L(rename_attr.as_str())
654 }
655 $L(format!("{variant_name}({content_ty}),"))
656 })
657 .ok()?
658 };
659
660 variant_blocks.push(variant_block);
661 }
662
663 let body = sigil_quote!(RustLang {
664 $if(schema.description.is_some()) {
665 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
666 }
667 $L(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
668 $if(!serde_tag_attr.is_empty()) {
669 $L(serde_tag_attr.as_str())
670 }
671 pub enum $N(name.as_str()) {
672 $C_each(variant_blocks);
673 }
674 })
675 .ok()?;
676
677 fsb = fsb.add_code(body);
678
679 if utoipa_enabled {
680 let variant_types: Vec<String> = tu
681 .variants
682 .iter()
683 .map(|v| rust_type_str_model(&v.content_type))
684 .collect();
685 let impl_block = build_utoipa_one_of_impl(&name, &variant_types)?;
686 fsb = fsb.add_code(impl_block);
687 }
688
689 fsb.build().ok()
690}
691
692fn emit_intersection(
697 schema: &IrSchema,
698 inter: &IrIntersection,
699 extra: Option<&ExtraDeriveConfig>,
700) -> Option<FileSpec> {
701 let name = schema.name.to_pascal_case();
702 let stem = schema.name.to_snake_case();
703
704 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
705 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
706 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
707
708 let fields: Vec<(String, String)> = inter
709 .members
710 .iter()
711 .enumerate()
712 .map(|(i, member)| {
713 let raw_name = match member {
714 IrTypeExpr::Named(n) => n.to_snake_case(),
715 _ => format!("member_{i}"),
716 };
717 let field_name = escape_rust_keyword(&raw_name);
718 let rust_type = rust_type_str_model(member);
719 (field_name, rust_type)
720 })
721 .collect();
722
723 let body = sigil_quote!(RustLang {
724 $if(schema.description.is_some()) {
725 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
726 }
727 $L(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
728 pub struct $N(name.as_str()) {
729 $for((field_name, rust_type) in fields.iter()) {
730 #[serde(flatten)]
731 $L(format!("pub {field_name}: {rust_type},"))
732 }
733 }
734 })
735 .ok()?;
736
737 fsb = fsb.add_code(body);
738 fsb.build().ok()
739}
740
741fn build_utoipa_one_of_impl(name: &str, variant_types: &[String]) -> Option<CodeBlock> {
746 let items: Vec<String> = variant_types
747 .iter()
748 .map(|rt| format!(" .item(<{rt} as utoipa::PartialSchema>::schema())"))
749 .collect();
750
751 sigil_quote!(RustLang {
752 impl utoipa::PartialSchema for $N(name) {
753 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
754 $L("utoipa::openapi::schema::Schema::OneOf(utoipa::openapi::schema::OneOfBuilder::new()")
755 $for(item in items.iter()) {
756 $L(item.as_str())
757 }
758 $L(" .build()).into()")
759 }
760 }
761
762 impl utoipa::ToSchema for $N(name) {
763 fn name() -> std::borrow::Cow<'static, str> {
764 $L(format!("std::borrow::Cow::Borrowed(\"{name}\")"))
765 }
766 }
767 })
768 .ok()
769}
770
771fn build_integer_enum_display(name: &str) -> Option<CodeBlock> {
776 sigil_quote!(RustLang {
777 impl std::fmt::Display for $N(name) {
778 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
779 write!(f, "{}", *self as i64)
780 }
781 }
782 })
783 .ok()
784}
785
786fn build_string_enum_display(name: &str, variants: &[(String, String)]) -> Option<CodeBlock> {
787 sigil_quote!(RustLang {
788 impl std::fmt::Display for $N(name) {
789 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
790 match self {
791 $for((variant, wire_value) in variants.iter()) {
792 $L(format!("{name}::{variant} => write!(f, {wire_value:?}),"))
793 }
794 }
795 }
796 }
797 })
798 .ok()
799}
800
801fn doc_comment_block(doc: &str) -> String {
807 let mut out = String::new();
808 for line in doc.lines() {
809 if line.is_empty() {
810 out.push_str("///\n");
811 } else {
812 out.push_str(&format!("/// {line}\n"));
813 }
814 }
815 out
816}
817
818fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> {
823 if let IrTypeExpr::Named(name) = expr
824 && let Some(schema) = ir.schemas.get(name)
825 && let IrSchemaKind::Object(obj) = &schema.kind
826 {
827 return Some(obj);
828 }
829 None
830}
831
832pub fn rust_type_str(expr: &IrTypeExpr) -> String {
837 match expr {
838 IrTypeExpr::Named(name) => name.to_pascal_case(),
839 IrTypeExpr::Primitive(p) => rust_primitive(p).to_string(),
840 IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str(inner)),
841 IrTypeExpr::Map(inner) => format!(
842 "std::collections::HashMap<String, {}>",
843 rust_type_str(inner)
844 ),
845 IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str(inner)),
846 IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => "String".to_string(),
847 IrTypeExpr::Union(_) | IrTypeExpr::Any => "serde_json::Value".to_string(),
848 }
849}
850
851pub fn rust_type_str_qualified(expr: &IrTypeExpr, ir: &IrSpec) -> String {
854 match expr {
855 IrTypeExpr::Named(name) => {
856 if let Some(schema) = ir.schemas.get(name)
857 && let IrSchemaKind::Alias(inner) = &schema.kind
858 && name.to_pascal_case() == rust_type_str_model(inner)
859 {
860 return rust_type_str(inner);
861 }
862 format!("crate::models::{}", name.to_pascal_case())
863 }
864 IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str_qualified(inner, ir)),
865 IrTypeExpr::Map(inner) => format!(
866 "std::collections::HashMap<String, {}>",
867 rust_type_str_qualified(inner, ir)
868 ),
869 IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str_qualified(inner, ir)),
870 other => rust_type_str(other),
871 }
872}
873
874fn rust_type_str_model(expr: &IrTypeExpr) -> String {
876 match expr {
877 IrTypeExpr::Named(name) => format!("super::{}", name.to_pascal_case()),
878 IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str_model(inner)),
879 IrTypeExpr::Map(inner) => format!(
880 "std::collections::HashMap<String, {}>",
881 rust_type_str_model(inner)
882 ),
883 IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str_model(inner)),
884 other => rust_type_str(other),
885 }
886}
887
888fn rust_primitive(p: &IrPrimitive) -> &'static str {
889 match p {
890 IrPrimitive::String
891 | IrPrimitive::Date
892 | IrPrimitive::DateTime
893 | IrPrimitive::Uuid
894 | IrPrimitive::StringWithFormat(_) => "String",
895 IrPrimitive::Binary => "Vec<u8>",
896 IrPrimitive::Integer => "i64",
897 IrPrimitive::IntegerWithFormat(format) => match format.as_str() {
898 "int32" => "i32",
899 "int64" => "i64",
900 "uint32" => "u32",
901 "uint64" => "u64",
902 _ => "i64",
903 },
904 IrPrimitive::Number => "f64",
905 IrPrimitive::NumberWithFormat(format) => match format.as_str() {
906 "float" => "f32",
907 _ => "f64",
908 },
909 IrPrimitive::Boolean => "bool",
910 }
911}
912
913fn escape_str(s: &str) -> String {
914 s.replace('\\', "\\\\").replace('"', "\\\"")
915}
916
917fn escape_rust_keyword(name: &str) -> String {
918 const KEYWORDS: &[&str] = &[
919 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
920 "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
921 "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
922 "true", "type", "union", "unsafe", "use", "where", "while", "yield",
923 ];
924 if KEYWORDS.contains(&name) {
925 format!("r#{name}")
926 } else {
927 name.to_string()
928 }
929}
930
931fn collect_named_type_refs(schema: &IrSchema) -> Vec<&str> {
934 let mut refs = Vec::new();
935 match &schema.kind {
936 IrSchemaKind::Object(obj) => {
937 for prop in obj.properties.values() {
938 collect_named_from_expr(&prop.type_expr, &mut refs);
939 }
940 if let Some(ap) = &obj.additional_properties {
941 collect_named_from_expr(ap, &mut refs);
942 }
943 }
944 IrSchemaKind::TaggedUnion(tu) => {
945 let uses_tuple_variants = matches!(tu.tagging, TaggingStyle::External);
946 if uses_tuple_variants {
947 for v in &tu.variants {
948 collect_named_from_expr(&v.content_type, &mut refs);
949 }
950 }
951 }
952 IrSchemaKind::Union(u) => {
953 for member in &u.members {
954 collect_named_from_expr(member, &mut refs);
955 }
956 }
957 IrSchemaKind::Alias(expr) => {
958 collect_named_from_expr(expr, &mut refs);
959 }
960 IrSchemaKind::Intersection(inter) => {
961 for member in &inter.members {
962 collect_named_from_expr(member, &mut refs);
963 }
964 }
965 IrSchemaKind::Enum(_) => {}
966 }
967 refs
968}
969
970fn collect_named_from_expr<'a>(expr: &'a IrTypeExpr, refs: &mut Vec<&'a str>) {
971 match expr {
972 IrTypeExpr::Named(name) => refs.push(name.as_str()),
973 IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => {
974 collect_named_from_expr(inner, refs);
975 }
976 IrTypeExpr::Union(members) => {
977 for m in members {
978 collect_named_from_expr(m, refs);
979 }
980 }
981 _ => {}
982 }
983}