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<String> = Vec::new();
558 for variant in &tu.variants {
559 let variant_name = variant.discriminator_value.to_pascal_case();
560 let mut block = String::new();
561
562 if variant.discriminator_value.to_pascal_case() != variant.discriminator_value {
563 block.push_str(&format!(
564 "#[serde(rename = \"{}\")]\n",
565 escape_str(&variant.discriminator_value)
566 ));
567 }
568
569 if inline_fields {
570 if let Some(obj) = resolve_object(&variant.content_type, ir) {
571 block.push_str(&format!("{variant_name} {{"));
572 for (json_name, prop) in &obj.properties {
573 let field_name = escape_rust_keyword(&json_name.to_snake_case());
574 if field_name != *json_name {
575 block.push_str(&format!("\n #[serde(rename = \"{json_name}\")]"));
576 }
577 if !prop.required || prop.nullable {
578 block.push_str(
579 "\n #[serde(skip_serializing_if = \"Option::is_none\", default)]",
580 );
581 block.push_str(&format!(
582 "\n {field_name}: Option<{}>,",
583 rust_type_str_model(&prop.type_expr)
584 ));
585 } else {
586 block.push_str(&format!(
587 "\n {field_name}: {},",
588 rust_type_str_model(&prop.type_expr)
589 ));
590 }
591 }
592 if let Some(ap) = &obj.additional_properties {
593 block.push_str("\n #[serde(flatten)]");
594 block.push_str(&format!(
595 "\n additional_properties: std::collections::HashMap<String, {}>,",
596 rust_type_str_model(ap)
597 ));
598 }
599 block.push_str("\n},");
600 } else {
601 block.push_str(&format!(
602 "{}({}),",
603 variant_name,
604 rust_type_str_model(&variant.content_type)
605 ));
606 }
607 } else {
608 block.push_str(&format!(
609 "{}({}),",
610 variant_name,
611 rust_type_str_model(&variant.content_type)
612 ));
613 }
614
615 variant_blocks.push(block);
616 }
617
618 let body = sigil_quote!(RustLang {
619 $if(schema.description.is_some()) {
620 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
621 }
622 $L(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
623 $if(!serde_tag_attr.is_empty()) {
624 $L(serde_tag_attr.as_str())
625 }
626 pub enum $N(name.as_str()) {
627 $for(block in variant_blocks.iter()) {
628 $L(block.as_str())
629 }
630 }
631 })
632 .ok()?;
633
634 fsb = fsb.add_code(body);
635
636 if utoipa_enabled {
637 let variant_types: Vec<String> = tu
638 .variants
639 .iter()
640 .map(|v| rust_type_str_model(&v.content_type))
641 .collect();
642 let impl_block = build_utoipa_one_of_impl(&name, &variant_types)?;
643 fsb = fsb.add_code(impl_block);
644 }
645
646 fsb.build().ok()
647}
648
649fn emit_intersection(
654 schema: &IrSchema,
655 inter: &IrIntersection,
656 extra: Option<&ExtraDeriveConfig>,
657) -> Option<FileSpec> {
658 let name = schema.name.to_pascal_case();
659 let stem = schema.name.to_snake_case();
660
661 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
662 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
663 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
664
665 let fields: Vec<(String, String)> = inter
666 .members
667 .iter()
668 .enumerate()
669 .map(|(i, member)| {
670 let raw_name = match member {
671 IrTypeExpr::Named(n) => n.to_snake_case(),
672 _ => format!("member_{i}"),
673 };
674 let field_name = escape_rust_keyword(&raw_name);
675 let rust_type = rust_type_str_model(member);
676 (field_name, rust_type)
677 })
678 .collect();
679
680 let body = sigil_quote!(RustLang {
681 $if(schema.description.is_some()) {
682 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
683 }
684 $L(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
685 pub struct $N(name.as_str()) {
686 $for((field_name, rust_type) in fields.iter()) {
687 #[serde(flatten)]
688 $L(format!("pub {field_name}: {rust_type},"))
689 }
690 }
691 })
692 .ok()?;
693
694 fsb = fsb.add_code(body);
695 fsb.build().ok()
696}
697
698fn build_utoipa_one_of_impl(name: &str, variant_types: &[String]) -> Option<CodeBlock> {
703 let items: Vec<String> = variant_types
704 .iter()
705 .map(|rt| format!(" .item(<{rt} as utoipa::PartialSchema>::schema())"))
706 .collect();
707
708 sigil_quote!(RustLang {
709 impl utoipa::PartialSchema for $N(name) {
710 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
711 $L("utoipa::openapi::schema::Schema::OneOf(utoipa::openapi::schema::OneOfBuilder::new()")
712 $for(item in items.iter()) {
713 $L(item.as_str())
714 }
715 $L(" .build()).into()")
716 }
717 }
718
719 impl utoipa::ToSchema for $N(name) {
720 fn name() -> std::borrow::Cow<'static, str> {
721 $L(format!("std::borrow::Cow::Borrowed(\"{name}\")"))
722 }
723 }
724 })
725 .ok()
726}
727
728fn build_integer_enum_display(name: &str) -> Option<CodeBlock> {
733 sigil_quote!(RustLang {
734 impl std::fmt::Display for $N(name) {
735 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
736 write!(f, "{}", *self as i64)
737 }
738 }
739 })
740 .ok()
741}
742
743fn build_string_enum_display(name: &str, variants: &[(String, String)]) -> Option<CodeBlock> {
744 sigil_quote!(RustLang {
745 impl std::fmt::Display for $N(name) {
746 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
747 match self {
748 $for((variant, wire_value) in variants.iter()) {
749 $L(format!("{name}::{variant} => write!(f, {wire_value:?}),"))
750 }
751 }
752 }
753 }
754 })
755 .ok()
756}
757
758fn doc_comment_block(doc: &str) -> String {
764 let mut out = String::new();
765 for line in doc.lines() {
766 if line.is_empty() {
767 out.push_str("///\n");
768 } else {
769 out.push_str(&format!("/// {line}\n"));
770 }
771 }
772 out
773}
774
775fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> {
780 if let IrTypeExpr::Named(name) = expr
781 && let Some(schema) = ir.schemas.get(name)
782 && let IrSchemaKind::Object(obj) = &schema.kind
783 {
784 return Some(obj);
785 }
786 None
787}
788
789pub fn rust_type_str(expr: &IrTypeExpr) -> String {
794 match expr {
795 IrTypeExpr::Named(name) => name.to_pascal_case(),
796 IrTypeExpr::Primitive(p) => rust_primitive(p).to_string(),
797 IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str(inner)),
798 IrTypeExpr::Map(inner) => format!(
799 "std::collections::HashMap<String, {}>",
800 rust_type_str(inner)
801 ),
802 IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str(inner)),
803 IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => "String".to_string(),
804 IrTypeExpr::Union(_) | IrTypeExpr::Any => "serde_json::Value".to_string(),
805 }
806}
807
808pub fn rust_type_str_qualified(expr: &IrTypeExpr, ir: &IrSpec) -> String {
811 match expr {
812 IrTypeExpr::Named(name) => {
813 if let Some(schema) = ir.schemas.get(name)
814 && let IrSchemaKind::Alias(inner) = &schema.kind
815 && name.to_pascal_case() == rust_type_str_model(inner)
816 {
817 return rust_type_str(inner);
818 }
819 format!("crate::models::{}", name.to_pascal_case())
820 }
821 IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str_qualified(inner, ir)),
822 IrTypeExpr::Map(inner) => format!(
823 "std::collections::HashMap<String, {}>",
824 rust_type_str_qualified(inner, ir)
825 ),
826 IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str_qualified(inner, ir)),
827 other => rust_type_str(other),
828 }
829}
830
831fn rust_type_str_model(expr: &IrTypeExpr) -> String {
833 match expr {
834 IrTypeExpr::Named(name) => format!("super::{}", name.to_pascal_case()),
835 IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str_model(inner)),
836 IrTypeExpr::Map(inner) => format!(
837 "std::collections::HashMap<String, {}>",
838 rust_type_str_model(inner)
839 ),
840 IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str_model(inner)),
841 other => rust_type_str(other),
842 }
843}
844
845fn rust_primitive(p: &IrPrimitive) -> &'static str {
846 match p {
847 IrPrimitive::String
848 | IrPrimitive::Date
849 | IrPrimitive::DateTime
850 | IrPrimitive::Uuid
851 | IrPrimitive::StringWithFormat(_) => "String",
852 IrPrimitive::Binary => "Vec<u8>",
853 IrPrimitive::Integer => "i64",
854 IrPrimitive::IntegerWithFormat(format) => match format.as_str() {
855 "int32" => "i32",
856 "int64" => "i64",
857 "uint32" => "u32",
858 "uint64" => "u64",
859 _ => "i64",
860 },
861 IrPrimitive::Number => "f64",
862 IrPrimitive::NumberWithFormat(format) => match format.as_str() {
863 "float" => "f32",
864 _ => "f64",
865 },
866 IrPrimitive::Boolean => "bool",
867 }
868}
869
870fn escape_str(s: &str) -> String {
871 s.replace('\\', "\\\\").replace('"', "\\\"")
872}
873
874fn escape_rust_keyword(name: &str) -> String {
875 const KEYWORDS: &[&str] = &[
876 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
877 "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
878 "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
879 "true", "type", "union", "unsafe", "use", "where", "while", "yield",
880 ];
881 if KEYWORDS.contains(&name) {
882 format!("r#{name}")
883 } else {
884 name.to_string()
885 }
886}
887
888fn collect_named_type_refs(schema: &IrSchema) -> Vec<&str> {
891 let mut refs = Vec::new();
892 match &schema.kind {
893 IrSchemaKind::Object(obj) => {
894 for prop in obj.properties.values() {
895 collect_named_from_expr(&prop.type_expr, &mut refs);
896 }
897 if let Some(ap) = &obj.additional_properties {
898 collect_named_from_expr(ap, &mut refs);
899 }
900 }
901 IrSchemaKind::TaggedUnion(tu) => {
902 let uses_tuple_variants = matches!(tu.tagging, TaggingStyle::External);
903 if uses_tuple_variants {
904 for v in &tu.variants {
905 collect_named_from_expr(&v.content_type, &mut refs);
906 }
907 }
908 }
909 IrSchemaKind::Union(u) => {
910 for member in &u.members {
911 collect_named_from_expr(member, &mut refs);
912 }
913 }
914 IrSchemaKind::Alias(expr) => {
915 collect_named_from_expr(expr, &mut refs);
916 }
917 IrSchemaKind::Intersection(inter) => {
918 for member in &inter.members {
919 collect_named_from_expr(member, &mut refs);
920 }
921 }
922 IrSchemaKind::Enum(_) => {}
923 }
924 refs
925}
926
927fn collect_named_from_expr<'a>(expr: &'a IrTypeExpr, refs: &mut Vec<&'a str>) {
928 match expr {
929 IrTypeExpr::Named(name) => refs.push(name.as_str()),
930 IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => {
931 collect_named_from_expr(inner, refs);
932 }
933 IrTypeExpr::Union(members) => {
934 for m in members {
935 collect_named_from_expr(m, refs);
936 }
937 }
938 _ => {}
939 }
940}