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 inlined_candidates: HashSet<&str> = ir
40 .schemas
41 .values()
42 .filter_map(|s| match &s.kind {
43 IrSchemaKind::TaggedUnion(tu)
44 if matches!(
45 tu.tagging,
46 TaggingStyle::Internal | TaggingStyle::Adjacent { .. }
47 ) =>
48 {
49 Some(tu.variants.iter().filter_map(|v| {
50 if let IrTypeExpr::Named(name) = &v.content_type
51 && ir
52 .schemas
53 .get(name)
54 .is_some_and(|s| matches!(s.kind, IrSchemaKind::Object(_)))
55 {
56 return Some(name.as_str());
57 }
58 None
59 }))
60 }
61 _ => None,
62 })
63 .flatten()
64 .collect();
65
66 let referenced_by_name: HashSet<&str> = ir
67 .schemas
68 .values()
69 .flat_map(|s| collect_named_type_refs(s))
70 .collect();
71
72 let inlined_schemas: HashSet<&str> = inlined_candidates
73 .difference(&referenced_by_name)
74 .copied()
75 .collect();
76
77 for (_name, schema) in &ir.schemas {
78 if inlined_schemas.contains(schema.name.as_str()) {
79 continue;
80 }
81 let Some(file_spec) = emit_model_file(schema, config, ir) else {
82 continue;
83 };
84 let stem = schema.name.to_snake_case();
85 let filename = format!("{stem}.rs");
86 mod_entries.push(stem);
87
88 let rendered = file_spec
89 .render(100)
90 .map_err(|e| format!("render error for {}: {e}", schema.name))?;
91
92 let mut content = String::with_capacity(header.len() + rendered.len());
93 content.push_str(header);
94 content.push_str(&rendered);
95 files.push(FileInfo::model(filename, content));
96 }
97
98 let mut mod_content = String::from(header);
100 for entry in &mod_entries {
101 mod_content.push_str(&format!("mod {entry};\npub use {entry}::*;\n"));
102 }
103 files.push(FileInfo::model("mod.rs".to_string(), mod_content));
104
105 Ok(files)
106}
107
108fn emit_model_file(
109 schema: &IrSchema,
110 config: &RustGeneratorConfig,
111 ir: &IrSpec,
112) -> Option<FileSpec> {
113 let extra = config.extra_derives.as_ref();
114 let per_type_cfg = extra
115 .and_then(|e| e.per_type.as_ref())
116 .and_then(|m| m.get(&schema.name));
117 match &schema.kind {
118 IrSchemaKind::Object(obj) => {
119 let derives = per_type_cfg.or_else(|| extra.and_then(|e| e.structs.as_ref()));
120 emit_object(schema, obj, derives)
121 }
122 IrSchemaKind::Enum(en) => {
123 let derives = per_type_cfg.or_else(|| extra.and_then(|e| e.enums.as_ref()));
124 emit_enum(schema, en, derives)
125 }
126 IrSchemaKind::Alias(expr) => {
127 let derives = per_type_cfg.or_else(|| extra.and_then(|e| e.structs.as_ref()));
128 emit_alias(schema, expr, derives)
129 }
130 IrSchemaKind::Union(u) => emit_union(schema, u, per_type_cfg),
131 IrSchemaKind::Intersection(i) => {
132 let derives = per_type_cfg.or_else(|| extra.and_then(|e| e.structs.as_ref()));
133 emit_intersection(schema, i, derives)
134 }
135 IrSchemaKind::TaggedUnion(tu) => {
136 let derives = per_type_cfg.or_else(|| extra.and_then(|e| e.unions.as_ref()));
137 emit_tagged_union(schema, tu, derives, ir)
138 }
139 }
140}
141
142fn derive_attr(base: &str, extra: Option<&ExtraDeriveConfig>) -> String {
147 match extra {
148 Some(cfg) if !cfg.derives.is_empty() => {
149 format!("#[derive({base}, {})]", cfg.derives.join(", "))
150 }
151 _ => format!("#[derive({base})]"),
152 }
153}
154
155fn emit_object(
160 schema: &IrSchema,
161 obj: &IrObject,
162 extra: Option<&ExtraDeriveConfig>,
163) -> Option<FileSpec> {
164 let name = schema.name.to_pascal_case();
165 let stem = schema.name.to_snake_case();
166
167 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
168 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
169 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
170
171 let body = sigil_quote!(RustLang {
172 $if(schema.description.is_some()) {
173 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
174 }
175 $L(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
176 pub struct $N(name.as_str()) {
177 $for((json_name, prop) in obj.properties.iter()) {
178 $if(prop.description.is_some()) {
179 $L(doc_comment_block(prop.description.as_deref().unwrap()).trim_end())
180 }
181 $if(escape_rust_keyword(&json_name.to_snake_case()) != *json_name) {
182 $L(format!("#[serde(rename = \"{json_name}\")]"))
183 }
184 $if(!prop.required || prop.nullable) {
185 #[serde(skip_serializing_if = "Option::is_none", default)]
186 $L(format!("pub {}: Option<{}>,", escape_rust_keyword(&json_name.to_snake_case()), rust_type_str_model(&prop.type_expr)))
187 } $else {
188 $L(format!("pub {}: {},", escape_rust_keyword(&json_name.to_snake_case()), rust_type_str_model(&prop.type_expr)))
189 }
190 }
191 $if(obj.additional_properties.is_some()) {
192 #[serde(flatten)]
193 $L(format!("pub additional_properties: std::collections::HashMap<String, {}>,", rust_type_str_model(obj.additional_properties.as_ref().unwrap())))
194 }
195 }
196 })
197 .ok()?;
198
199 fsb = fsb.add_code(body);
200 fsb.build().ok()
201}
202
203fn emit_enum(
208 schema: &IrSchema,
209 en: &IrEnum,
210 extra: Option<&ExtraDeriveConfig>,
211) -> Option<FileSpec> {
212 let name = schema.name.to_pascal_case();
213
214 match en.value_type {
215 IrEnumValueType::Mixed | IrEnumValueType::Number => {
216 return emit_type_alias_file(schema, "serde_json::Value");
217 }
218 IrEnumValueType::Integer => {
219 return emit_integer_enum(schema, en, extra);
220 }
221 IrEnumValueType::String => {}
222 }
223
224 let stem = schema.name.to_snake_case();
225 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
226 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
227 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
228
229 let mut variants: Vec<(String, String)> = Vec::new();
230 for v in &en.values {
231 let s = v.value.as_str()?;
232 variants.push((s.to_pascal_case(), s.to_string()));
233 }
234
235 let body = sigil_quote!(RustLang {
236 $if(schema.description.is_some()) {
237 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
238 }
239 $L(derive_attr("Debug, Clone, PartialEq, Eq, Serialize, Deserialize", extra))
240 pub enum $N(name.as_str()) {
241 $for((variant, wire) in variants.iter()) {
242 $if(variant != wire) {
243 $L(format!("#[serde(rename = \"{}\")]", escape_str(wire)))
244 }
245 $L(format!("{variant},"))
246 }
247 }
248 })
249 .ok()?;
250
251 fsb = fsb.add_code(body);
252
253 if let Some(display_block) = build_string_enum_display(&name, &variants) {
255 fsb = fsb.add_code(display_block);
256 }
257
258 fsb.build().ok()
259}
260
261fn emit_integer_enum(
262 schema: &IrSchema,
263 en: &IrEnum,
264 extra: Option<&ExtraDeriveConfig>,
265) -> Option<FileSpec> {
266 let name = schema.name.to_pascal_case();
267 let stem = schema.name.to_snake_case();
268
269 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
270 fsb = fsb.add_import(ImportSpec::named("serde_repr", "Deserialize_repr"));
271 fsb = fsb.add_import(ImportSpec::named("serde_repr", "Serialize_repr"));
272
273 let int_variants: Vec<(String, i64)> = en
274 .values
275 .iter()
276 .map(|v| {
277 let n = v.value.as_i64()?;
278 let variant_name = if n < 0 {
279 format!("Neg{}", n.unsigned_abs())
280 } else {
281 format!("N{n}")
282 };
283 Some((variant_name, n))
284 })
285 .collect::<Option<Vec<_>>>()?;
286
287 let body = sigil_quote!(RustLang {
288 $if(schema.description.is_some()) {
289 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
290 }
291 $L(derive_attr("Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr", extra))
292 #[repr(i64)]
293 pub enum $N(name.as_str()) {
294 $for((variant_name, n) in int_variants.iter()) {
295 $L(format!("{variant_name} = {n},"))
296 }
297 }
298 })
299 .ok()?;
300
301 fsb = fsb.add_code(body);
302
303 if let Some(display_block) = build_integer_enum_display(&name) {
305 fsb = fsb.add_code(display_block);
306 }
307
308 fsb.build().ok()
309}
310
311fn emit_alias(
316 schema: &IrSchema,
317 expr: &IrTypeExpr,
318 extra: Option<&ExtraDeriveConfig>,
319) -> Option<FileSpec> {
320 if let IrTypeExpr::Named(n) = expr
321 && n.to_pascal_case() == schema.name.to_pascal_case()
322 {
323 return None;
324 }
325 if schema.name.to_pascal_case() == rust_type_str_model(expr) {
327 return None;
328 }
329
330 let name = schema.name.to_pascal_case();
331 let stem = schema.name.to_snake_case();
332 let rhs = rust_type_str_model(expr);
333
334 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
335
336 let has_extra = extra.is_some_and(|cfg| !cfg.derives.is_empty());
337
338 if has_extra {
339 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
340 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
341
342 let rhs_type = TypeName::raw(&rhs);
343 let block = sigil_quote!(RustLang {
344 $if(schema.description.is_some()) {
345 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
346 }
347 $L(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
348 #[serde(transparent)]
349 pub struct $N(name.as_str())(pub $T(rhs_type));
350 })
351 .ok()?;
352 fsb = fsb.add_code(block);
353 } else {
354 let rhs_type = TypeName::raw(&rhs);
355 let block = sigil_quote!(RustLang {
356 $if(schema.description.is_some()) {
357 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
358 }
359 pub type $N(name.as_str()) = $T(rhs_type);
360 })
361 .ok()?;
362 fsb = fsb.add_code(block);
363 }
364
365 fsb.build().ok()
366}
367
368fn emit_type_alias_file(schema: &IrSchema, rhs_str: &str) -> Option<FileSpec> {
369 let name = schema.name.to_pascal_case();
370 let stem = schema.name.to_snake_case();
371
372 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
373
374 let rhs_type = TypeName::raw(rhs_str);
375 let block = sigil_quote!(RustLang {
376 $if(schema.description.is_some()) {
377 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
378 }
379 pub type $N(name.as_str()) = $T(rhs_type);
380 })
381 .ok()?;
382 fsb = fsb.add_code(block);
383
384 fsb.build().ok()
385}
386
387fn emit_union(
392 schema: &IrSchema,
393 union: &IrUnion,
394 extra: Option<&ExtraDeriveConfig>,
395) -> Option<FileSpec> {
396 let name = schema.name.to_pascal_case();
397 let stem = schema.name.to_snake_case();
398
399 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
400 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
401 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
402
403 let variants: Vec<(String, String)> = union
404 .members
405 .iter()
406 .enumerate()
407 .map(|(i, member)| {
408 let variant_name = union_variant_name(member, i);
409 let rust_type = rust_type_str_model(member);
410 (variant_name, rust_type)
411 })
412 .collect();
413
414 let body = sigil_quote!(RustLang {
415 $if(schema.description.is_some()) {
416 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
417 }
418 $L(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
419 #[serde(untagged)]
420 pub enum $N(name.as_str()) {
421 $for((variant_name, rust_type) in variants.iter()) {
422 $L(format!("{variant_name}({rust_type}),"))
423 }
424 }
425 })
426 .ok()?;
427
428 fsb = fsb.add_code(body);
429 fsb.build().ok()
430}
431
432fn union_variant_name(expr: &IrTypeExpr, index: usize) -> String {
433 match expr {
434 IrTypeExpr::Named(n) => n.to_pascal_case(),
435 IrTypeExpr::Primitive(p) => primitive_variant_name(p),
436 IrTypeExpr::Array(_) => format!("Array{index}"),
437 IrTypeExpr::Map(_) => format!("Map{index}"),
438 _ => format!("Variant{index}"),
439 }
440}
441
442fn primitive_variant_name(p: &IrPrimitive) -> String {
443 match p {
444 IrPrimitive::String | IrPrimitive::StringWithFormat(_) => "String".to_string(),
445 IrPrimitive::Integer | IrPrimitive::IntegerWithFormat(_) => "Integer".to_string(),
446 IrPrimitive::Number | IrPrimitive::NumberWithFormat(_) => "Number".to_string(),
447 IrPrimitive::Boolean => "Boolean".to_string(),
448 IrPrimitive::Binary => "Binary".to_string(),
449 IrPrimitive::Date => "Date".to_string(),
450 IrPrimitive::DateTime => "DateTime".to_string(),
451 IrPrimitive::Uuid => "Uuid".to_string(),
452 }
453}
454
455fn emit_tagged_union(
460 schema: &IrSchema,
461 tu: &IrTaggedUnion,
462 extra: Option<&ExtraDeriveConfig>,
463 ir: &IrSpec,
464) -> Option<FileSpec> {
465 if tu.variants.is_empty() {
466 return None;
467 }
468
469 let name = schema.name.to_pascal_case();
470 let stem = schema.name.to_snake_case();
471
472 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
473 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
474 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
475
476 let serde_tag_attr = match &tu.tagging {
477 TaggingStyle::Internal => {
478 format!(
479 "#[serde(tag = \"{}\")]",
480 escape_str(&tu.discriminator_field)
481 )
482 }
483 TaggingStyle::Adjacent { content_field } => {
484 format!(
485 "#[serde(tag = \"{}\", content = \"{}\")]",
486 escape_str(&tu.discriminator_field),
487 escape_str(content_field)
488 )
489 }
490 TaggingStyle::External => String::new(),
491 };
492
493 let inline_fields = matches!(
494 tu.tagging,
495 TaggingStyle::Internal | TaggingStyle::Adjacent { .. }
496 );
497
498 let mut variant_blocks: Vec<String> = Vec::new();
499 for variant in &tu.variants {
500 let variant_name = variant.discriminator_value.to_pascal_case();
501 let mut block = String::new();
502
503 if variant.discriminator_value.to_pascal_case() != variant.discriminator_value {
504 block.push_str(&format!(
505 "#[serde(rename = \"{}\")]\n",
506 escape_str(&variant.discriminator_value)
507 ));
508 }
509
510 if inline_fields {
511 if let Some(obj) = resolve_object(&variant.content_type, ir) {
512 block.push_str(&format!("{variant_name} {{"));
513 for (json_name, prop) in &obj.properties {
514 let field_name = escape_rust_keyword(&json_name.to_snake_case());
515 if field_name != *json_name {
516 block.push_str(&format!("\n #[serde(rename = \"{json_name}\")]"));
517 }
518 if !prop.required || prop.nullable {
519 block.push_str(
520 "\n #[serde(skip_serializing_if = \"Option::is_none\", default)]",
521 );
522 block.push_str(&format!(
523 "\n {field_name}: Option<{}>,",
524 rust_type_str_model(&prop.type_expr)
525 ));
526 } else {
527 block.push_str(&format!(
528 "\n {field_name}: {},",
529 rust_type_str_model(&prop.type_expr)
530 ));
531 }
532 }
533 if let Some(ap) = &obj.additional_properties {
534 block.push_str("\n #[serde(flatten)]");
535 block.push_str(&format!(
536 "\n additional_properties: std::collections::HashMap<String, {}>,",
537 rust_type_str_model(ap)
538 ));
539 }
540 block.push_str("\n},");
541 } else {
542 block.push_str(&format!(
543 "{}({}),",
544 variant_name,
545 rust_type_str_model(&variant.content_type)
546 ));
547 }
548 } else {
549 block.push_str(&format!(
550 "{}({}),",
551 variant_name,
552 rust_type_str_model(&variant.content_type)
553 ));
554 }
555
556 variant_blocks.push(block);
557 }
558
559 let body = sigil_quote!(RustLang {
560 $if(schema.description.is_some()) {
561 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
562 }
563 $L(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
564 $if(!serde_tag_attr.is_empty()) {
565 $L(serde_tag_attr.as_str())
566 }
567 pub enum $N(name.as_str()) {
568 $for(block in variant_blocks.iter()) {
569 $L(block.as_str())
570 }
571 }
572 })
573 .ok()?;
574
575 fsb = fsb.add_code(body);
576 fsb.build().ok()
577}
578
579fn emit_intersection(
584 schema: &IrSchema,
585 inter: &IrIntersection,
586 extra: Option<&ExtraDeriveConfig>,
587) -> Option<FileSpec> {
588 let name = schema.name.to_pascal_case();
589 let stem = schema.name.to_snake_case();
590
591 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
592 fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
593 fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
594
595 let fields: Vec<(String, String)> = inter
596 .members
597 .iter()
598 .enumerate()
599 .map(|(i, member)| {
600 let raw_name = match member {
601 IrTypeExpr::Named(n) => n.to_snake_case(),
602 _ => format!("member_{i}"),
603 };
604 let field_name = escape_rust_keyword(&raw_name);
605 let rust_type = rust_type_str_model(member);
606 (field_name, rust_type)
607 })
608 .collect();
609
610 let body = sigil_quote!(RustLang {
611 $if(schema.description.is_some()) {
612 $L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
613 }
614 $L(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
615 pub struct $N(name.as_str()) {
616 $for((field_name, rust_type) in fields.iter()) {
617 #[serde(flatten)]
618 $L(format!("pub {field_name}: {rust_type},"))
619 }
620 }
621 })
622 .ok()?;
623
624 fsb = fsb.add_code(body);
625 fsb.build().ok()
626}
627
628fn build_integer_enum_display(name: &str) -> Option<CodeBlock> {
633 sigil_quote!(RustLang {
634 impl std::fmt::Display for $N(name) {
635 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636 write!(f, "{}", *self as i64)
637 }
638 }
639 })
640 .ok()
641}
642
643fn build_string_enum_display(name: &str, variants: &[(String, String)]) -> Option<CodeBlock> {
644 sigil_quote!(RustLang {
645 impl std::fmt::Display for $N(name) {
646 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
647 match self {
648 $for((variant, wire_value) in variants.iter()) {
649 $L(format!("{name}::{variant} => write!(f, {wire_value:?}),"))
650 }
651 }
652 }
653 }
654 })
655 .ok()
656}
657
658fn doc_comment_block(doc: &str) -> String {
664 let mut out = String::new();
665 for line in doc.lines() {
666 if line.is_empty() {
667 out.push_str("///\n");
668 } else {
669 out.push_str(&format!("/// {line}\n"));
670 }
671 }
672 out
673}
674
675fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> {
680 if let IrTypeExpr::Named(name) = expr
681 && let Some(schema) = ir.schemas.get(name)
682 && let IrSchemaKind::Object(obj) = &schema.kind
683 {
684 return Some(obj);
685 }
686 None
687}
688
689pub fn rust_type_str(expr: &IrTypeExpr) -> String {
694 match expr {
695 IrTypeExpr::Named(name) => name.to_pascal_case(),
696 IrTypeExpr::Primitive(p) => rust_primitive(p).to_string(),
697 IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str(inner)),
698 IrTypeExpr::Map(inner) => format!(
699 "std::collections::HashMap<String, {}>",
700 rust_type_str(inner)
701 ),
702 IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str(inner)),
703 IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => "String".to_string(),
704 IrTypeExpr::Union(_) | IrTypeExpr::Any => "serde_json::Value".to_string(),
705 }
706}
707
708pub fn rust_type_str_qualified(expr: &IrTypeExpr, ir: &IrSpec) -> String {
711 match expr {
712 IrTypeExpr::Named(name) => {
713 if let Some(schema) = ir.schemas.get(name)
714 && let IrSchemaKind::Alias(inner) = &schema.kind
715 && name.to_pascal_case() == rust_type_str_model(inner)
716 {
717 return rust_type_str(inner);
718 }
719 format!("crate::models::{}", name.to_pascal_case())
720 }
721 IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str_qualified(inner, ir)),
722 IrTypeExpr::Map(inner) => format!(
723 "std::collections::HashMap<String, {}>",
724 rust_type_str_qualified(inner, ir)
725 ),
726 IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str_qualified(inner, ir)),
727 other => rust_type_str(other),
728 }
729}
730
731fn rust_type_str_model(expr: &IrTypeExpr) -> String {
733 match expr {
734 IrTypeExpr::Named(name) => format!("super::{}", name.to_pascal_case()),
735 IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str_model(inner)),
736 IrTypeExpr::Map(inner) => format!(
737 "std::collections::HashMap<String, {}>",
738 rust_type_str_model(inner)
739 ),
740 IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str_model(inner)),
741 other => rust_type_str(other),
742 }
743}
744
745fn rust_primitive(p: &IrPrimitive) -> &'static str {
746 match p {
747 IrPrimitive::String
748 | IrPrimitive::Date
749 | IrPrimitive::DateTime
750 | IrPrimitive::Uuid
751 | IrPrimitive::StringWithFormat(_) => "String",
752 IrPrimitive::Binary => "Vec<u8>",
753 IrPrimitive::Integer => "i64",
754 IrPrimitive::IntegerWithFormat(format) => match format.as_str() {
755 "int32" => "i32",
756 "int64" => "i64",
757 "uint32" => "u32",
758 "uint64" => "u64",
759 _ => "i64",
760 },
761 IrPrimitive::Number => "f64",
762 IrPrimitive::NumberWithFormat(format) => match format.as_str() {
763 "float" => "f32",
764 _ => "f64",
765 },
766 IrPrimitive::Boolean => "bool",
767 }
768}
769
770fn escape_str(s: &str) -> String {
771 s.replace('\\', "\\\\").replace('"', "\\\"")
772}
773
774fn escape_rust_keyword(name: &str) -> String {
775 const KEYWORDS: &[&str] = &[
776 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
777 "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
778 "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
779 "true", "type", "union", "unsafe", "use", "where", "while", "yield",
780 ];
781 if KEYWORDS.contains(&name) {
782 format!("r#{name}")
783 } else {
784 name.to_string()
785 }
786}
787
788fn collect_named_type_refs(schema: &IrSchema) -> Vec<&str> {
791 let mut refs = Vec::new();
792 match &schema.kind {
793 IrSchemaKind::Object(obj) => {
794 for prop in obj.properties.values() {
795 collect_named_from_expr(&prop.type_expr, &mut refs);
796 }
797 if let Some(ap) = &obj.additional_properties {
798 collect_named_from_expr(ap, &mut refs);
799 }
800 }
801 IrSchemaKind::TaggedUnion(tu) => {
802 let uses_tuple_variants = matches!(tu.tagging, TaggingStyle::External);
803 if uses_tuple_variants {
804 for v in &tu.variants {
805 collect_named_from_expr(&v.content_type, &mut refs);
806 }
807 }
808 }
809 IrSchemaKind::Union(u) => {
810 for member in &u.members {
811 collect_named_from_expr(member, &mut refs);
812 }
813 }
814 IrSchemaKind::Alias(expr) => {
815 collect_named_from_expr(expr, &mut refs);
816 }
817 IrSchemaKind::Intersection(inter) => {
818 for member in &inter.members {
819 collect_named_from_expr(member, &mut refs);
820 }
821 }
822 IrSchemaKind::Enum(_) => {}
823 }
824 refs
825}
826
827fn collect_named_from_expr<'a>(expr: &'a IrTypeExpr, refs: &mut Vec<&'a str>) {
828 match expr {
829 IrTypeExpr::Named(name) => refs.push(name.as_str()),
830 IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => {
831 collect_named_from_expr(inner, refs);
832 }
833 IrTypeExpr::Union(members) => {
834 for m in members {
835 collect_named_from_expr(m, refs);
836 }
837 }
838 _ => {}
839 }
840}