use std::collections::HashSet;
use crate::codegen::traits::file_writer::FileInfo;
use crate::ir::types::{
IrEnum, IrEnumValueType, IrIntersection, IrObject, IrPrimitive, IrSchema, IrSchemaKind, IrSpec,
IrTaggedUnion, IrTypeExpr, IrUnion, TaggingStyle,
};
use heck::{ToPascalCase, ToSnakeCase};
use sigil_stitch::prelude::{CodeBlock, sigil_quote};
use sigil_stitch::spec::file_spec::FileSpec;
use sigil_stitch::spec::import_spec::ImportSpec;
use sigil_stitch::type_name::TypeName;
use super::config::{ExtraDeriveConfig, RustGeneratorConfig};
pub fn generate_model_files(
ir: &IrSpec,
header: &str,
config: &RustGeneratorConfig,
) -> Result<Vec<FileInfo>, String> {
let mut files = Vec::new();
let mut mod_entries = Vec::new();
let utoipa_enabled = config.utoipa.as_ref().is_some_and(|u| u.enabled);
let inlined_schemas: HashSet<&str> = if utoipa_enabled {
HashSet::new()
} else {
let inlined_candidates: HashSet<&str> = ir
.schemas
.values()
.filter_map(|s| match &s.kind {
IrSchemaKind::TaggedUnion(tu)
if matches!(
tu.tagging,
TaggingStyle::Internal | TaggingStyle::Adjacent { .. }
) =>
{
Some(tu.variants.iter().filter_map(|v| {
if let IrTypeExpr::Named(name) = &v.content_type
&& ir
.schemas
.get(name)
.is_some_and(|s| matches!(s.kind, IrSchemaKind::Object(_)))
{
return Some(name.as_str());
}
None
}))
}
_ => None,
})
.flatten()
.collect();
let referenced_by_name: HashSet<&str> = ir
.schemas
.values()
.flat_map(|s| collect_named_type_refs(s))
.collect();
inlined_candidates
.difference(&referenced_by_name)
.copied()
.collect()
};
for (_name, schema) in &ir.schemas {
if inlined_schemas.contains(schema.name.as_str()) {
continue;
}
let Some(file_spec) = emit_model_file(schema, config, ir) else {
continue;
};
let stem = schema.name.to_snake_case();
let filename = format!("{stem}.rs");
mod_entries.push(stem);
let rendered = file_spec
.render(100)
.map_err(|e| format!("render error for {}: {e}", schema.name))?;
let mut content = String::with_capacity(header.len() + rendered.len());
content.push_str(header);
content.push_str(&rendered);
files.push(FileInfo::model(filename, content));
}
let mut mod_content = String::from(header);
for entry in &mod_entries {
mod_content.push_str(&format!("mod {entry};\npub use {entry}::*;\n"));
}
files.push(FileInfo::model("mod.rs".to_string(), mod_content));
Ok(files)
}
fn emit_model_file(
schema: &IrSchema,
config: &RustGeneratorConfig,
ir: &IrSpec,
) -> Option<FileSpec> {
let extra = config.extra_derives.as_ref();
let utoipa_enabled = config.utoipa.as_ref().is_some_and(|u| u.enabled);
let per_type_cfg = extra
.and_then(|e| e.per_type.as_ref())
.and_then(|m| m.get(&schema.name));
match &schema.kind {
IrSchemaKind::Object(obj) => {
let derives = merge_utoipa(
per_type_cfg.or_else(|| extra.and_then(|e| e.structs.as_ref())),
utoipa_enabled,
);
emit_object(schema, obj, derives.as_ref())
}
IrSchemaKind::Enum(en) => {
let derives = merge_utoipa(
per_type_cfg.or_else(|| extra.and_then(|e| e.enums.as_ref())),
utoipa_enabled,
);
emit_enum(schema, en, derives.as_ref())
}
IrSchemaKind::Alias(expr) => {
let derives = merge_utoipa(
per_type_cfg.or_else(|| extra.and_then(|e| e.structs.as_ref())),
utoipa_enabled,
);
emit_alias(schema, expr, derives.as_ref())
}
IrSchemaKind::Union(u) => emit_union(schema, u, per_type_cfg, utoipa_enabled),
IrSchemaKind::Intersection(i) => {
let derives = merge_utoipa(
per_type_cfg.or_else(|| extra.and_then(|e| e.structs.as_ref())),
utoipa_enabled,
);
emit_intersection(schema, i, derives.as_ref())
}
IrSchemaKind::TaggedUnion(tu) => {
let derives = per_type_cfg
.or_else(|| extra.and_then(|e| e.unions.as_ref()))
.cloned();
emit_tagged_union(schema, tu, derives.as_ref(), ir, utoipa_enabled)
}
}
}
fn derive_attr(base: &str, extra: Option<&ExtraDeriveConfig>) -> String {
match extra {
Some(cfg) if !cfg.derives.is_empty() => {
let base_set: Vec<&str> = base.split(", ").collect();
let unique_extra: Vec<&str> = cfg
.derives
.iter()
.map(|s| s.as_str())
.filter(|d| !base_set.contains(d))
.collect();
if unique_extra.is_empty() {
format!("derive({base})")
} else {
format!("derive({base}, {})", unique_extra.join(", "))
}
}
_ => format!("derive({base})"),
}
}
fn merge_utoipa(
base: Option<&ExtraDeriveConfig>,
utoipa_enabled: bool,
) -> Option<ExtraDeriveConfig> {
if !utoipa_enabled {
return base.cloned();
}
let mut cfg = base.cloned().unwrap_or_default();
if !cfg.derives.iter().any(|d| d.contains("utoipa")) {
cfg.derives.push("utoipa::ToSchema".to_string());
}
Some(cfg)
}
fn emit_object(
schema: &IrSchema,
obj: &IrObject,
extra: Option<&ExtraDeriveConfig>,
) -> Option<FileSpec> {
let name = schema.name.to_pascal_case();
let stem = schema.name.to_snake_case();
let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
let body = sigil_quote!(RustLang {
$if(schema.description.is_some()) {
$L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
}
$attr(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
pub struct $N(name.as_str()) {
$for((json_name, prop) in obj.properties.iter()) {
$if(prop.description.is_some()) {
$L(doc_comment_block(prop.description.as_deref().unwrap()).trim_end())
}
$if(escape_rust_keyword(&json_name.to_snake_case()) != *json_name) {
#[serde(rename = $S(json_name))]
}
$if(!prop.required || prop.nullable) {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub $L("@{escape_rust_keyword(&json_name.to_snake_case())}: Option<@{rust_type_str_model(&prop.type_expr)}>,")
} $else {
pub $L("@{escape_rust_keyword(&json_name.to_snake_case())}: @{rust_type_str_model(&prop.type_expr)},")
}
}
$if(obj.additional_properties.is_some()) {
#[serde(flatten)]
pub $L("additional_properties: std::collections::HashMap<String, @{rust_type_str_model(obj.additional_properties.as_ref().unwrap())}>,")
}
}
})
.ok()?;
fsb = fsb.add_code(body);
fsb.build().ok()
}
fn emit_enum(
schema: &IrSchema,
en: &IrEnum,
extra: Option<&ExtraDeriveConfig>,
) -> Option<FileSpec> {
let name = schema.name.to_pascal_case();
match en.value_type {
IrEnumValueType::Mixed | IrEnumValueType::Number => {
return emit_type_alias_file(schema, "serde_json::Value");
}
IrEnumValueType::Integer => {
return emit_integer_enum(schema, en, extra);
}
IrEnumValueType::String => {}
}
let stem = schema.name.to_snake_case();
let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
let mut variants: Vec<(String, String)> = Vec::new();
for v in &en.values {
let s = v.value.as_str()?;
variants.push((s.to_pascal_case(), s.to_string()));
}
let body = sigil_quote!(RustLang {
$if(schema.description.is_some()) {
$L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
}
$attr(derive_attr("Debug, Clone, PartialEq, Eq, Serialize, Deserialize", extra))
pub enum $N(name.as_str()) {
$for((variant, wire) in variants.iter()) {
$if(variant != wire) {
#[serde(rename = $S(wire))]
}
$N(variant.as_str()),
}
}
})
.ok()?;
fsb = fsb.add_code(body);
if let Some(display_block) = build_string_enum_display(&name, &variants) {
fsb = fsb.add_code(display_block);
}
fsb.build().ok()
}
fn emit_integer_enum(
schema: &IrSchema,
en: &IrEnum,
extra: Option<&ExtraDeriveConfig>,
) -> Option<FileSpec> {
let name = schema.name.to_pascal_case();
let stem = schema.name.to_snake_case();
let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
fsb = fsb.add_import(ImportSpec::named("serde_repr", "Deserialize_repr"));
fsb = fsb.add_import(ImportSpec::named("serde_repr", "Serialize_repr"));
let int_variants: Vec<(String, i64)> = en
.values
.iter()
.map(|v| {
let n = v.value.as_i64()?;
let variant_name = if n < 0 {
format!("Neg{}", n.unsigned_abs())
} else {
format!("N{n}")
};
Some((variant_name, n))
})
.collect::<Option<Vec<_>>>()?;
let body = sigil_quote!(RustLang {
$if(schema.description.is_some()) {
$L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
}
$attr(derive_attr("Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr", extra))
#[repr(i64)]
pub enum $N(name.as_str()) {
$for((variant_name, n) in int_variants.iter()) {
$N(variant_name.as_str()) = $L(n.to_string()),
}
}
})
.ok()?;
fsb = fsb.add_code(body);
if let Some(display_block) = build_integer_enum_display(&name) {
fsb = fsb.add_code(display_block);
}
fsb.build().ok()
}
fn emit_alias(
schema: &IrSchema,
expr: &IrTypeExpr,
extra: Option<&ExtraDeriveConfig>,
) -> Option<FileSpec> {
if let IrTypeExpr::Named(n) = expr
&& n.to_pascal_case() == schema.name.to_pascal_case()
{
return None;
}
if schema.name.to_pascal_case() == rust_type_str_model(expr) {
return None;
}
let name = schema.name.to_pascal_case();
let stem = schema.name.to_snake_case();
let rhs = rust_type_str_model(expr);
let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
let has_extra = extra.is_some_and(|cfg| !cfg.derives.is_empty());
if has_extra {
fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
let rhs_type = TypeName::raw(&rhs);
let block = sigil_quote!(RustLang {
$if(schema.description.is_some()) {
$L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
}
$attr(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
#[serde(transparent)]
pub struct $N(name.as_str())(pub $T(rhs_type));
})
.ok()?;
fsb = fsb.add_code(block);
} else {
let rhs_type = TypeName::raw(&rhs);
let block = sigil_quote!(RustLang {
$if(schema.description.is_some()) {
$L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
}
pub type $N(name.as_str()) = $T(rhs_type);
})
.ok()?;
fsb = fsb.add_code(block);
}
fsb.build().ok()
}
fn emit_type_alias_file(schema: &IrSchema, rhs_str: &str) -> Option<FileSpec> {
let name = schema.name.to_pascal_case();
let stem = schema.name.to_snake_case();
let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
let rhs_type = TypeName::raw(rhs_str);
let block = sigil_quote!(RustLang {
$if(schema.description.is_some()) {
$L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
}
pub type $N(name.as_str()) = $T(rhs_type);
})
.ok()?;
fsb = fsb.add_code(block);
fsb.build().ok()
}
fn emit_union(
schema: &IrSchema,
union: &IrUnion,
extra: Option<&ExtraDeriveConfig>,
utoipa_enabled: bool,
) -> Option<FileSpec> {
let name = schema.name.to_pascal_case();
let stem = schema.name.to_snake_case();
let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
let variants: Vec<(String, String)> = union
.members
.iter()
.enumerate()
.map(|(i, member)| {
let variant_name = union_variant_name(member, i);
let rust_type = rust_type_str_model(member);
(variant_name, rust_type)
})
.collect();
let body = sigil_quote!(RustLang {
$if(schema.description.is_some()) {
$L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
}
$attr(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
#[serde(untagged)]
pub enum $N(name.as_str()) {
$for((variant_name, rust_type) in variants.iter()) {
$N(variant_name.as_str())($L(rust_type.as_str())),
}
}
})
.ok()?;
fsb = fsb.add_code(body);
if utoipa_enabled {
let variant_types: Vec<String> = variants
.iter()
.map(|(_, rust_type)| rust_type.clone())
.collect();
let impl_block = build_utoipa_one_of_impl(&name, &variant_types)?;
fsb = fsb.add_code(impl_block);
}
fsb.build().ok()
}
fn union_variant_name(expr: &IrTypeExpr, index: usize) -> String {
match expr {
IrTypeExpr::Named(n) => n.to_pascal_case(),
IrTypeExpr::Primitive(p) => primitive_variant_name(p),
IrTypeExpr::Array(_) => format!("Array{index}"),
IrTypeExpr::Map(_) => format!("Map{index}"),
_ => format!("Variant{index}"),
}
}
fn primitive_variant_name(p: &IrPrimitive) -> String {
match p {
IrPrimitive::String | IrPrimitive::StringWithFormat(_) => "String".to_string(),
IrPrimitive::Integer | IrPrimitive::IntegerWithFormat(_) => "Integer".to_string(),
IrPrimitive::Number | IrPrimitive::NumberWithFormat(_) => "Number".to_string(),
IrPrimitive::Boolean => "Boolean".to_string(),
IrPrimitive::Binary => "Binary".to_string(),
IrPrimitive::Date => "Date".to_string(),
IrPrimitive::DateTime => "DateTime".to_string(),
IrPrimitive::Uuid => "Uuid".to_string(),
}
}
fn emit_tagged_union(
schema: &IrSchema,
tu: &IrTaggedUnion,
extra: Option<&ExtraDeriveConfig>,
ir: &IrSpec,
utoipa_enabled: bool,
) -> Option<FileSpec> {
if tu.variants.is_empty() {
return None;
}
let name = schema.name.to_pascal_case();
let stem = schema.name.to_snake_case();
let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
let serde_tag_attr = match &tu.tagging {
TaggingStyle::Internal => {
format!("serde(tag = \"{}\")", escape_str(&tu.discriminator_field))
}
TaggingStyle::Adjacent { content_field } => {
format!(
"serde(tag = \"{}\", content = \"{}\")",
escape_str(&tu.discriminator_field),
escape_str(content_field)
)
}
TaggingStyle::External => String::new(),
};
let inline_fields = matches!(
tu.tagging,
TaggingStyle::Internal | TaggingStyle::Adjacent { .. }
);
let mut variant_blocks: Vec<CodeBlock> = Vec::new();
for variant in &tu.variants {
let variant_name = variant.discriminator_value.to_pascal_case();
let rename_attr = if variant_name != variant.discriminator_value {
format!(
"serde(rename = \"{}\")",
escape_str(&variant.discriminator_value)
)
} else {
String::new()
};
let variant_block = if inline_fields {
if let Some(obj) = resolve_object(&variant.content_type, ir) {
let non_tag_fields: Vec<_> = obj
.properties
.iter()
.filter(|(json_name, _)| *json_name != &tu.discriminator_field)
.collect();
let has_additional = obj.additional_properties.is_some();
if non_tag_fields.is_empty() && !has_additional {
sigil_quote!(RustLang {
$if(!rename_attr.is_empty()) {
$attr(rename_attr.as_str())
}
$N(variant_name.as_str()),
})
.ok()?
} else {
let field_blocks: Vec<CodeBlock> = non_tag_fields
.iter()
.map(|(json_name, prop)| {
let field_name = escape_rust_keyword(&json_name.to_snake_case());
let ty = rust_type_str_model(&prop.type_expr);
let needs_rename = field_name != **json_name;
let optional = !prop.required || prop.nullable;
sigil_quote!(RustLang {
$if(needs_rename) {
#[serde(rename = $S(json_name.as_str()))]
}
$if(optional) {
#[serde(skip_serializing_if = "Option::is_none", default)]
$L(format!("{field_name}: Option<{ty}>,"))
}
$if(!optional) {
$L(format!("{field_name}: {ty},"))
}
})
.ok()
})
.collect::<Option<_>>()?;
let additional_blocks: Vec<CodeBlock> = if let Some(ap) =
&obj.additional_properties
{
let ty = rust_type_str_model(ap);
vec![sigil_quote!(RustLang {
#[serde(flatten)]
$L(format!("additional_properties: std::collections::HashMap<String, {ty}>,"))
})
.ok()?]
} else {
vec![]
};
let mut cb = CodeBlock::builder();
if !rename_attr.is_empty() {
cb.add(&format!("#[{rename_attr}]"), ());
cb.add_line();
}
cb.add(&format!("{variant_name} {{\n%>"), ());
for fb in &field_blocks {
cb.add_code(fb.clone());
}
for ab in &additional_blocks {
cb.add_code(ab.clone());
}
cb.add("%<},", ());
cb.build().ok()?
}
} else {
let content_ty = rust_type_str_model(&variant.content_type);
sigil_quote!(RustLang {
$if(!rename_attr.is_empty()) {
$attr(rename_attr.as_str())
}
$N(variant_name.as_str())($L(content_ty.as_str())),
})
.ok()?
}
} else {
let content_ty = rust_type_str_model(&variant.content_type);
sigil_quote!(RustLang {
$if(!rename_attr.is_empty()) {
$attr(rename_attr.as_str())
}
$N(variant_name.as_str())($L(content_ty.as_str())),
})
.ok()?
};
variant_blocks.push(variant_block);
}
let body = sigil_quote!(RustLang {
$if(schema.description.is_some()) {
$L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
}
$attr(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
$if(!serde_tag_attr.is_empty()) {
$attr(serde_tag_attr.as_str())
}
pub enum $N(name.as_str()) {
$C_each(variant_blocks);
}
})
.ok()?;
fsb = fsb.add_code(body);
if utoipa_enabled {
let variant_types: Vec<String> = tu
.variants
.iter()
.map(|v| rust_type_str_model(&v.content_type))
.collect();
let impl_block = build_utoipa_one_of_impl(&name, &variant_types)?;
fsb = fsb.add_code(impl_block);
}
fsb.build().ok()
}
fn emit_intersection(
schema: &IrSchema,
inter: &IrIntersection,
extra: Option<&ExtraDeriveConfig>,
) -> Option<FileSpec> {
let name = schema.name.to_pascal_case();
let stem = schema.name.to_snake_case();
let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
fsb = fsb.add_import(ImportSpec::named("serde", "Deserialize"));
fsb = fsb.add_import(ImportSpec::named("serde", "Serialize"));
let fields: Vec<(String, String)> = inter
.members
.iter()
.enumerate()
.map(|(i, member)| {
let raw_name = match member {
IrTypeExpr::Named(n) => n.to_snake_case(),
_ => format!("member_{i}"),
};
let field_name = escape_rust_keyword(&raw_name);
let rust_type = rust_type_str_model(member);
(field_name, rust_type)
})
.collect();
let body = sigil_quote!(RustLang {
$if(schema.description.is_some()) {
$L(doc_comment_block(schema.description.as_deref().unwrap()).trim_end())
}
$attr(derive_attr("Debug, Clone, Serialize, Deserialize", extra))
pub struct $N(name.as_str()) {
$for((field_name, rust_type) in fields.iter()) {
#[serde(flatten)]
pub $N(field_name.as_str()): $L(rust_type.as_str()),
}
}
})
.ok()?;
fsb = fsb.add_code(body);
fsb.build().ok()
}
fn build_utoipa_one_of_impl(name: &str, variant_types: &[String]) -> Option<CodeBlock> {
let items: Vec<String> = variant_types
.iter()
.map(|rt| format!(" .item(<{rt} as utoipa::PartialSchema>::schema())"))
.collect();
sigil_quote!(RustLang {
impl utoipa::PartialSchema for $N(name) {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
$L("utoipa::openapi::schema::Schema::OneOf(utoipa::openapi::schema::OneOfBuilder::new()")
$for(item in items.iter()) {
$L(item.as_str())
}
$L(" .build()).into()")
}
}
impl utoipa::ToSchema for $N(name) {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed($S(name))
}
}
})
.ok()
}
fn build_integer_enum_display(name: &str) -> Option<CodeBlock> {
sigil_quote!(RustLang {
impl std::fmt::Display for $N(name) {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", *self as i64)
}
}
})
.ok()
}
fn build_string_enum_display(name: &str, variants: &[(String, String)]) -> Option<CodeBlock> {
sigil_quote!(RustLang {
impl std::fmt::Display for $N(name) {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
$for((variant, wire_value) in variants.iter()) {
$N(name)::$N(variant.as_str()) => write!(f, $S(wire_value)),
}
}
}
}
})
.ok()
}
fn doc_comment_block(doc: &str) -> String {
let mut out = String::new();
for line in doc.lines() {
if line.is_empty() {
out.push_str("///\n");
} else {
out.push_str(&format!("/// {line}\n"));
}
}
out
}
fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> {
if let IrTypeExpr::Named(name) = expr
&& let Some(schema) = ir.schemas.get(name)
&& let IrSchemaKind::Object(obj) = &schema.kind
{
return Some(obj);
}
None
}
pub fn rust_type_str(expr: &IrTypeExpr) -> String {
match expr {
IrTypeExpr::Named(name) => name.to_pascal_case(),
IrTypeExpr::Primitive(p) => rust_primitive(p).to_string(),
IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str(inner)),
IrTypeExpr::Map(inner) => format!(
"std::collections::HashMap<String, {}>",
rust_type_str(inner)
),
IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str(inner)),
IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => "String".to_string(),
IrTypeExpr::Union(_) | IrTypeExpr::Any => "serde_json::Value".to_string(),
}
}
pub fn rust_type_str_qualified(expr: &IrTypeExpr, ir: &IrSpec) -> String {
match expr {
IrTypeExpr::Named(name) => {
if let Some(schema) = ir.schemas.get(name)
&& let IrSchemaKind::Alias(inner) = &schema.kind
&& name.to_pascal_case() == rust_type_str_model(inner)
{
return rust_type_str(inner);
}
format!("crate::models::{}", name.to_pascal_case())
}
IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str_qualified(inner, ir)),
IrTypeExpr::Map(inner) => format!(
"std::collections::HashMap<String, {}>",
rust_type_str_qualified(inner, ir)
),
IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str_qualified(inner, ir)),
other => rust_type_str(other),
}
}
fn rust_type_str_model(expr: &IrTypeExpr) -> String {
match expr {
IrTypeExpr::Named(name) => format!("super::{}", name.to_pascal_case()),
IrTypeExpr::Array(inner) => format!("Vec<{}>", rust_type_str_model(inner)),
IrTypeExpr::Map(inner) => format!(
"std::collections::HashMap<String, {}>",
rust_type_str_model(inner)
),
IrTypeExpr::Nullable(inner) => format!("Option<{}>", rust_type_str_model(inner)),
other => rust_type_str(other),
}
}
fn rust_primitive(p: &IrPrimitive) -> &'static str {
match p {
IrPrimitive::String
| IrPrimitive::Date
| IrPrimitive::DateTime
| IrPrimitive::Uuid
| IrPrimitive::StringWithFormat(_) => "String",
IrPrimitive::Binary => "Vec<u8>",
IrPrimitive::Integer => "i64",
IrPrimitive::IntegerWithFormat(format) => match format.as_str() {
"int32" => "i32",
"int64" => "i64",
"uint32" => "u32",
"uint64" => "u64",
_ => "i64",
},
IrPrimitive::Number => "f64",
IrPrimitive::NumberWithFormat(format) => match format.as_str() {
"float" => "f32",
_ => "f64",
},
IrPrimitive::Boolean => "bool",
}
}
fn escape_str(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
fn escape_rust_keyword(name: &str) -> String {
const KEYWORDS: &[&str] = &[
"as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
"extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
"mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
"true", "type", "union", "unsafe", "use", "where", "while", "yield",
];
if KEYWORDS.contains(&name) {
format!("r#{name}")
} else {
name.to_string()
}
}
fn collect_named_type_refs(schema: &IrSchema) -> Vec<&str> {
let mut refs = Vec::new();
match &schema.kind {
IrSchemaKind::Object(obj) => {
for prop in obj.properties.values() {
collect_named_from_expr(&prop.type_expr, &mut refs);
}
if let Some(ap) = &obj.additional_properties {
collect_named_from_expr(ap, &mut refs);
}
}
IrSchemaKind::TaggedUnion(tu) => {
let uses_tuple_variants = matches!(tu.tagging, TaggingStyle::External);
if uses_tuple_variants {
for v in &tu.variants {
collect_named_from_expr(&v.content_type, &mut refs);
}
}
}
IrSchemaKind::Union(u) => {
for member in &u.members {
collect_named_from_expr(member, &mut refs);
}
}
IrSchemaKind::Alias(expr) => {
collect_named_from_expr(expr, &mut refs);
}
IrSchemaKind::Intersection(inter) => {
for member in &inter.members {
collect_named_from_expr(member, &mut refs);
}
}
IrSchemaKind::Enum(_) => {}
}
refs
}
fn collect_named_from_expr<'a>(expr: &'a IrTypeExpr, refs: &mut Vec<&'a str>) {
match expr {
IrTypeExpr::Named(name) => refs.push(name.as_str()),
IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => {
collect_named_from_expr(inner, refs);
}
IrTypeExpr::Union(members) => {
for m in members {
collect_named_from_expr(m, refs);
}
}
_ => {}
}
}