use std::collections::BTreeMap;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use super::ast::{
EnumDef, FieldDef, OperationInfo, PathSegment, QueryParameter, RustPrimitive, RustType, StructDef, StructMethod,
StructMethodKind, TypeAliasDef, TypeRef, VariantContent, VariantDef,
};
use crate::{
generator::ast::StructKind,
reserved::{header_const_name, regex_const_name},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Visibility {
#[default]
Public,
Crate,
File,
}
impl Visibility {
pub fn parse(s: &str) -> Option<Self> {
match s {
"public" => Some(Visibility::Public),
"crate" => Some(Visibility::Crate),
"file" => Some(Visibility::File),
_ => None,
}
}
fn to_tokens(self) -> TokenStream {
match self {
Visibility::Public => quote! { pub },
Visibility::Crate => quote! { pub(crate) },
Visibility::File => quote! {},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TypeUsage {
RequestOnly,
ResponseOnly,
Bidirectional,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct RegexKey {
owner_type: String,
owner_variant: Option<String>,
field: String,
}
impl RegexKey {
fn for_struct(type_name: &str, field_name: &str) -> Self {
Self {
owner_type: type_name.to_string(),
owner_variant: None,
field: field_name.to_string(),
}
}
fn for_variant(type_name: &str, variant_name: &str, field_name: &str) -> Self {
Self {
owner_type: type_name.to_string(),
owner_variant: Some(variant_name.to_string()),
field: field_name.to_string(),
}
}
fn parts(&self) -> Vec<&str> {
let mut parts = vec![self.owner_type.as_str()];
if let Some(variant) = &self.owner_variant {
parts.push(variant.as_str());
}
parts.push(self.field.as_str());
parts
}
}
pub(crate) struct CodeGenerator;
impl CodeGenerator {
fn ensure_derive(derives: &mut Vec<String>, trait_name: &str) {
if !derives.iter().any(|existing| existing == trait_name) {
derives.push(trait_name.to_string());
}
}
pub(crate) fn build_type_usage_map(operations: &[OperationInfo]) -> BTreeMap<String, TypeUsage> {
let mut usage_map: BTreeMap<String, (bool, bool)> = BTreeMap::new();
for op in operations {
if let Some(ref req_type) = op.request_type {
let entry = usage_map.entry(req_type.clone()).or_insert((false, false));
entry.0 = true;
}
for body_type in &op.request_body_types {
let entry = usage_map.entry(body_type.clone()).or_insert((false, false));
entry.0 = true;
}
if let Some(ref resp_type) = op.response_type {
let entry = usage_map.entry(resp_type.clone()).or_insert((false, false));
entry.1 = true;
}
}
usage_map
.into_iter()
.map(|(type_name, (in_request, in_response))| {
let usage = match (in_request, in_response) {
(true, false) => TypeUsage::RequestOnly,
(false, true) => TypeUsage::ResponseOnly,
(true, true) | (false, false) => TypeUsage::Bidirectional,
};
(type_name, usage)
})
.collect()
}
pub(crate) fn generate(
types: &[RustType],
type_usage: &BTreeMap<String, TypeUsage>,
headers: &[&String],
visibility: Visibility,
) -> TokenStream {
let ordered = Self::ordered_types(types);
let (regex_consts, regex_lookup) = Self::generate_regex_constants(&ordered);
let header_consts = Self::generate_header_constants(headers);
let type_tokens: Vec<TokenStream> = ordered
.iter()
.map(|ty| Self::generate_type(ty, ®ex_lookup, type_usage, visibility))
.collect();
quote! {
use serde::{Deserialize, Serialize};
#regex_consts
#header_consts
#(#type_tokens)*
}
}
fn ordered_types<'a>(types: &'a [RustType]) -> Vec<&'a RustType> {
let mut map: BTreeMap<String, &'a RustType> = BTreeMap::new();
for ty in types {
let name = ty.type_name().to_string();
if let Some(existing) = map.get(&name) {
let existing_priority = Self::type_priority(existing);
let new_priority = Self::type_priority(ty);
if new_priority < existing_priority {
map.insert(name, ty);
}
} else {
map.insert(name, ty);
}
}
map.into_values().collect()
}
fn type_priority(rust_type: &RustType) -> u8 {
match rust_type {
RustType::DiscriminatedEnum(_) => 0, RustType::Enum(_) => 1, RustType::Struct(_) => 2, RustType::TypeAlias(_) => 3, }
}
fn generate_regex_constants(types: &[&RustType]) -> (TokenStream, BTreeMap<RegexKey, String>) {
let mut const_defs: BTreeMap<String, String> = BTreeMap::new();
let mut lookup: BTreeMap<RegexKey, String> = BTreeMap::new();
let mut pattern_to_const: BTreeMap<String, String> = BTreeMap::new();
for rust_type in types {
match rust_type {
RustType::Struct(def) => {
for field in &def.fields {
let Some(pattern) = &field.regex_validation else {
continue;
};
let key = RegexKey::for_struct(&def.name, &field.name);
let pattern_key = pattern.clone();
let const_name = if let Some(existing) = pattern_to_const.get(&pattern_key) {
existing.clone()
} else {
let name = regex_const_name(&key.parts());
pattern_to_const.insert(pattern_key.clone(), name.clone());
const_defs.insert(name.clone(), pattern_key);
name
};
lookup.insert(key, const_name);
}
}
RustType::Enum(def) => {
for variant in &def.variants {
if let VariantContent::Struct(fields) = &variant.content {
for field in fields {
let Some(pattern) = &field.regex_validation else {
continue;
};
let key = RegexKey::for_variant(&def.name, &variant.name, &field.name);
let pattern_key = pattern.clone();
let const_name = if let Some(existing) = pattern_to_const.get(&pattern_key) {
existing.clone()
} else {
let name = regex_const_name(&key.parts());
pattern_to_const.insert(pattern_key.clone(), name.clone());
const_defs.insert(name.clone(), pattern_key);
name
};
lookup.insert(key, const_name);
}
}
}
}
RustType::TypeAlias(_) | RustType::DiscriminatedEnum(_) => {}
}
}
if const_defs.is_empty() {
return (quote! {}, lookup);
}
let regex_defs: Vec<TokenStream> = const_defs
.into_iter()
.map(|(name, pattern)| {
let ident = format_ident!("{}", name);
quote! {
static #ident: std::sync::LazyLock<regex::Regex> =
std::sync::LazyLock::new(|| regex::Regex::new(#pattern).expect("invalid regex"));
}
})
.collect();
(quote! { #(#regex_defs)* }, lookup)
}
fn generate_header_constants(headers: &[&String]) -> TokenStream {
if headers.is_empty() {
return quote! {};
}
let const_tokens: Vec<TokenStream> = headers
.iter()
.map(|header| {
let const_name = header_const_name(header);
let ident = format_ident!("{}", const_name);
quote! {
pub const #ident: http::HeaderName = http::HeaderName::from_static(#header);
}
})
.collect();
quote! { #(#const_tokens)* }
}
fn json_value_to_rust_expr(value: &serde_json::Value, rust_type: &TypeRef) -> TokenStream {
if matches!(value, serde_json::Value::Null) {
return quote! { None };
}
let base_expr = Self::coerce_to_rust_type(value, &rust_type.base_type);
if rust_type.nullable {
quote! { Some(#base_expr) }
} else {
base_expr
}
}
fn coerce_to_rust_type(value: &serde_json::Value, rust_type: &RustPrimitive) -> TokenStream {
match rust_type {
RustPrimitive::String => Self::coerce_to_string(value),
RustPrimitive::I8
| RustPrimitive::I16
| RustPrimitive::I32
| RustPrimitive::I64
| RustPrimitive::I128
| RustPrimitive::Isize => Self::coerce_to_int(value),
RustPrimitive::U8
| RustPrimitive::U16
| RustPrimitive::U32
| RustPrimitive::U64
| RustPrimitive::U128
| RustPrimitive::Usize => Self::coerce_to_uint(value),
RustPrimitive::F32 | RustPrimitive::F64 => Self::coerce_to_float(value),
RustPrimitive::Bool => Self::coerce_to_bool(value),
_ => quote! { Default::default() },
}
}
fn coerce_to_string(value: &serde_json::Value) -> TokenStream {
match value {
serde_json::Value::String(s) if s.is_empty() => quote! { String::new() },
serde_json::Value::String(s) => quote! { #s.to_string() },
serde_json::Value::Number(n) => {
let n_str = n.to_string();
quote! { #n_str.to_string() }
}
serde_json::Value::Bool(b) => {
let b_str = b.to_string();
quote! { #b_str.to_string() }
}
_ => quote! { Default::default() },
}
}
fn coerce_to_int(value: &serde_json::Value) -> TokenStream {
match value {
serde_json::Value::Number(n) => n
.as_i64()
.map_or_else(|| quote! { Default::default() }, |i| quote! { #i }),
serde_json::Value::String(s) => s
.parse::<i64>()
.ok()
.map_or_else(|| quote! { Default::default() }, |i| quote! { #i }),
_ => quote! { Default::default() },
}
}
fn coerce_to_uint(value: &serde_json::Value) -> TokenStream {
match value {
serde_json::Value::Number(n) => n
.as_u64()
.map_or_else(|| quote! { Default::default() }, |u| quote! { #u }),
serde_json::Value::String(s) => s
.parse::<u64>()
.ok()
.map_or_else(|| quote! { Default::default() }, |u| quote! { #u }),
_ => quote! { Default::default() },
}
}
fn coerce_to_float(value: &serde_json::Value) -> TokenStream {
match value {
serde_json::Value::Number(n) => {
if let Some(f) = n.as_f64() {
quote! { #f }
} else if let Some(i) = n.as_i64() {
#[allow(clippy::cast_precision_loss)]
let f = i as f64;
quote! { #f }
} else {
quote! { Default::default() }
}
}
serde_json::Value::String(s) => s
.parse::<f64>()
.ok()
.map_or_else(|| quote! { Default::default() }, |f| quote! { #f }),
_ => quote! { Default::default() },
}
}
fn coerce_to_bool(value: &serde_json::Value) -> TokenStream {
match value {
serde_json::Value::Bool(b) => quote! { #b },
serde_json::Value::Number(n) => {
let b = n.as_i64().is_some_and(|i| i != 0);
quote! { #b }
}
serde_json::Value::String(s) => {
let b = matches!(s.to_lowercase().as_str(), "true" | "1" | "yes");
quote! { #b }
}
_ => quote! { Default::default() },
}
}
fn generate_type(
rust_type: &RustType,
regex_lookup: &BTreeMap<RegexKey, String>,
type_usage: &BTreeMap<String, TypeUsage>,
visibility: Visibility,
) -> TokenStream {
match rust_type {
RustType::Struct(def) => Self::generate_struct(def, regex_lookup, type_usage, visibility),
RustType::Enum(def) => Self::generate_enum(def, regex_lookup, visibility),
RustType::TypeAlias(def) => Self::generate_type_alias(def, visibility),
RustType::DiscriminatedEnum(def) => Self::generate_discriminated_enum(def, visibility),
}
}
fn generate_struct(
def: &StructDef,
regex_lookup: &BTreeMap<RegexKey, String>,
type_usage: &BTreeMap<String, TypeUsage>,
visibility: Visibility,
) -> TokenStream {
let name = format_ident!("{}", def.name);
let docs = Self::generate_docs(&def.docs);
let vis = visibility.to_tokens();
let derives = match def.kind {
StructKind::OperationRequest => {
let mut custom = vec!["Debug".to_string(), "Clone".to_string()];
custom.push("validator::Validate".to_string());
custom.push("oas3_gen_support::Default".to_string());
Self::generate_derives(&custom)
}
StructKind::RequestBody => {
let usage = type_usage.get(&def.name).unwrap_or(&TypeUsage::Bidirectional);
let mut custom = vec!["Debug".to_string(), "Clone".to_string()];
match usage {
TypeUsage::RequestOnly => {
custom.push("Serialize".to_string());
custom.push("validator::Validate".to_string());
}
TypeUsage::ResponseOnly => {
custom.push("Deserialize".to_string());
}
TypeUsage::Bidirectional => {
custom.push("Serialize".to_string());
custom.push("Deserialize".to_string());
custom.push("validator::Validate".to_string());
}
}
custom.push("oas3_gen_support::Default".to_string());
Self::generate_derives(&custom)
}
StructKind::Schema => {
let mut derives = def.derives.clone();
if let Some(usage) = type_usage.get(&def.name) {
match usage {
TypeUsage::RequestOnly => {
Self::ensure_derive(&mut derives, "Serialize");
Self::ensure_derive(&mut derives, "validator::Validate");
}
TypeUsage::ResponseOnly => {
Self::ensure_derive(&mut derives, "Deserialize");
}
TypeUsage::Bidirectional => {
Self::ensure_derive(&mut derives, "Serialize");
Self::ensure_derive(&mut derives, "Deserialize");
Self::ensure_derive(&mut derives, "validator::Validate");
}
}
}
Self::generate_derives(&derives)
}
};
let outer_attrs = Self::generate_outer_attrs(&def.outer_attrs);
let serde_attrs = Self::generate_serde_attrs(&def.serde_attrs);
let include_validation = match def.kind {
StructKind::RequestBody => !matches!(type_usage.get(&def.name), Some(TypeUsage::ResponseOnly)),
StructKind::OperationRequest | StructKind::Schema => true,
};
let fields = Self::generate_fields_with_visibility(
&def.name,
None,
&def.fields,
true,
include_validation,
regex_lookup,
visibility,
);
let struct_tokens = quote! {
#docs
#outer_attrs
#derives
#serde_attrs
#vis struct #name {
#(#fields),*
}
};
if def.methods.is_empty() {
struct_tokens
} else {
let methods: Vec<TokenStream> = def
.methods
.iter()
.map(|m| Self::generate_struct_method(m, visibility))
.collect();
quote! {
#struct_tokens
impl #name {
#(#methods)*
}
}
}
}
fn generate_struct_method(method: &StructMethod, visibility: Visibility) -> TokenStream {
let docs = Self::generate_docs(&method.docs);
let name = format_ident!("{}", method.name);
let attrs = Self::generate_outer_attrs(&method.attrs);
let vis = visibility.to_tokens();
let body = match &method.kind {
StructMethodKind::RenderPath { segments, query_params } => {
let mut format_string = String::new();
let mut fallback_string = String::new();
let mut args: Vec<TokenStream> = Vec::new();
for segment in segments {
match segment {
PathSegment::Literal(lit) => {
let escaped = lit.replace('{', "{{").replace('}', "}}");
format_string.push_str(&escaped);
fallback_string.push_str(lit);
}
PathSegment::Parameter { field } => {
format_string.push_str("{}");
fallback_string.push_str("{}");
let ident = format_ident!("{}", field);
args.push(quote! {
oas3_gen_support::percent_encode_path_segment(&self.#ident.to_string())
});
}
}
}
let path_expr = if args.is_empty() {
quote! { #fallback_string.to_string() }
} else {
let args_tokens = args;
quote! { format!(#format_string, #(#args_tokens),*) }
};
if query_params.is_empty() {
path_expr
} else {
let query_statements: Vec<TokenStream> =
query_params.iter().map(Self::generate_query_param_statement).collect();
quote! {
use std::fmt::Write as _;
let mut path = #path_expr;
let mut prefix = '\0';
#(#query_statements)*
path
}
}
}
};
quote! {
#docs
#attrs
#vis fn #name(&self) -> String {
#body
}
}
}
fn generate_query_param_statement(param: &QueryParameter) -> TokenStream {
let ident = format_ident!("{}", param.field);
let key = ¶m.encoded_name;
let param_equal = format!("{{prefix}}{key}={{}}");
if param.optional {
if param.is_array {
if param.explode {
quote! {
if let Some(values) = &self.#ident {
for value in values {
prefix = if prefix == '\0' { '?' } else { '&' };
write!(&mut path, #param_equal, oas3_gen_support::percent_encode_query_component(&value.to_string())).unwrap();
}
}
}
} else {
quote! {
if let Some(values) = &self.#ident && !values.is_empty() {
prefix = if prefix == '\0' { '?' } else { '&' };
let values = values.iter().map(|v| oas3_gen_support::percent_encode_query_component(&v)).collect::<Vec<_>>().join(",");
write!(&mut path, #param_equal, values).unwrap();
}
}
}
} else {
quote! {
if let Some(value) = &self.#ident {
prefix = if prefix == '\0' { '?' } else { '&' };
write!(&mut path, #param_equal, oas3_gen_support::percent_encode_query_component(&value.to_string())).unwrap();
}
}
}
} else if param.is_array {
if param.explode {
quote! {
for value in &self.#ident {
prefix = if prefix == '\0' { '?' } else { '&' };
write!(&mut path, #param_equal, oas3_gen_support::percent_encode_query_component(&value.to_string())).unwrap();
}
}
} else {
quote! {
if !self.#ident.is_empty() {
prefix = if prefix == '\0' { '?' } else { '&' };
let values = self.#ident.iter().map(|v| oas3_gen_support::percent_encode_query_component(&v)).collect::<Vec<_>>().join(",");
write!(&mut path, #param_equal, values).unwrap();
}
}
}
} else {
quote! {
prefix = if prefix == '\0' { '?' } else { '&' };
write!(&mut path, #param_equal, oas3_gen_support::percent_encode_query_component(&self.#ident.to_string())).unwrap();
}
}
}
fn generate_enum(def: &EnumDef, regex_lookup: &BTreeMap<RegexKey, String>, visibility: Visibility) -> TokenStream {
let name = format_ident!("{}", def.name);
let docs = Self::generate_docs(&def.docs);
let vis = visibility.to_tokens();
let derives = Self::generate_derives(&def.derives);
let outer_attrs = Self::generate_outer_attrs(&def.outer_attrs);
let serde_attrs = Self::generate_enum_serde_attrs(def);
let variants = Self::generate_variants(&def.name, &def.variants, regex_lookup);
quote! {
#docs
#outer_attrs
#derives
#serde_attrs
#vis enum #name {
#(#variants),*
}
}
}
fn generate_type_alias(def: &TypeAliasDef, visibility: Visibility) -> TokenStream {
let name = format_ident!("{}", def.name);
let docs = Self::generate_docs(&def.docs);
let vis = visibility.to_tokens();
let target = Self::parse_type_string(&def.target.to_rust_type());
quote! {
#docs
#vis type #name = #target;
}
}
fn generate_discriminated_enum(
def: &crate::generator::ast::DiscriminatedEnumDef,
visibility: Visibility,
) -> TokenStream {
let name = format_ident!("{}", def.name);
let disc_field = &def.discriminator_field;
let docs = Self::generate_docs(&def.docs);
let vis = visibility.to_tokens();
let variants: Vec<TokenStream> = def
.variants
.iter()
.map(|v| {
let disc_value = &v.discriminator_value;
let variant_name = format_ident!("{}", v.variant_name);
let type_name = Self::parse_type_string(&v.type_name);
quote! { (#disc_value, #variant_name(#type_name)) }
})
.collect();
if let Some(ref fallback) = def.fallback {
let fallback_variant = format_ident!("{}", fallback.variant_name);
let fallback_type = Self::parse_type_string(&fallback.type_name);
quote! {
#docs
oas3_gen_support::discriminated_enum! {
#vis enum #name {
discriminator: #disc_field,
variants: [
#(#variants),*
],
fallback: #fallback_variant(#fallback_type),
}
}
}
} else {
quote! {
#docs
oas3_gen_support::discriminated_enum! {
#vis enum #name {
discriminator: #disc_field,
variants: [
#(#variants),*
],
}
}
}
}
}
fn generate_docs(docs: &[String]) -> TokenStream {
if docs.is_empty() {
return quote! {};
}
let doc_lines: Vec<TokenStream> = docs
.iter()
.map(|line| {
let clean = line.strip_prefix("/// ").unwrap_or(line);
quote! { #[doc = #clean] }
})
.collect();
quote! { #(#doc_lines)* }
}
fn generate_derives(derives: &[String]) -> TokenStream {
if derives.is_empty() {
return quote! {};
}
let derive_idents = derives
.iter()
.map(|d| syn::parse_str(d).unwrap_or_else(|_| quote! {}))
.collect::<Vec<_>>();
quote! { #[derive(#(#derive_idents),*)] }
}
fn generate_outer_attrs(attrs: &[String]) -> TokenStream {
if attrs.is_empty() {
return quote! {};
}
let attr_tokens: Vec<TokenStream> = attrs
.iter()
.map(|attr| {
let trimmed = attr.trim();
if trimmed.is_empty() {
return quote! {};
}
let source = if trimmed.starts_with("#[") {
trimmed.to_string()
} else {
format!("#[{trimmed}]")
};
syn::parse_str::<TokenStream>(&source).unwrap_or_else(|_| quote! {})
})
.collect();
quote! { #(#attr_tokens)* }
}
fn generate_serde_attrs(attrs: &[String]) -> TokenStream {
if attrs.is_empty() {
return quote! {};
}
let attr_tokens: Vec<TokenStream> = attrs
.iter()
.map(|attr| {
let tokens: TokenStream = attr.as_str().parse().unwrap_or_else(|_| quote! {});
quote! { #[serde(#tokens)] }
})
.collect();
quote! { #(#attr_tokens)* }
}
fn generate_validation_attrs(regex_const: Option<&str>, attrs: &[String]) -> TokenStream {
if attrs.is_empty() && regex_const.is_none() {
return quote! {};
}
let mut combined = attrs.to_owned();
if let Some(const_name) = regex_const {
combined.push(format!("regex(path = \"{const_name}\")"));
}
let attr_tokens: Vec<TokenStream> = combined
.iter()
.map(|attr| attr.parse().unwrap_or_else(|_| quote! {}))
.collect();
quote! { #[validate(#(#attr_tokens),*)] }
}
fn generate_enum_serde_attrs(def: &EnumDef) -> TokenStream {
let mut attrs = Vec::new();
if let Some(ref discriminator) = def.discriminator {
attrs.push(quote! { tag = #discriminator });
}
for attr in &def.serde_attrs {
if let Ok(tokens) = attr.parse::<TokenStream>() {
attrs.push(tokens);
}
}
if attrs.is_empty() {
return quote! {};
}
quote! {
#[serde(#(#attrs),*)]
}
}
fn generate_fields_with_visibility(
type_name: &str,
variant_name: Option<&str>,
fields: &[FieldDef],
add_pub: bool,
include_validation: bool,
regex_lookup: &BTreeMap<RegexKey, String>,
visibility: Visibility,
) -> Vec<TokenStream> {
fields
.iter()
.map(|field| {
let name = format_ident!("{}", field.name);
let mut field_docs = field.docs.clone();
if let Some(ref multiple_of) = field.multiple_of {
field_docs.push(format!("/// Validation: Must be a multiple of {multiple_of}"));
}
let docs = Self::generate_docs(&field_docs);
let serde_attrs = Self::generate_serde_attrs(&field.serde_attrs);
let extra_attrs: Vec<TokenStream> = field
.extra_attrs
.iter()
.filter_map(|attr| attr.parse::<TokenStream>().ok())
.collect();
let regex_const = if include_validation && field.regex_validation.is_some() {
let key = match variant_name {
Some(variant) => RegexKey::for_variant(type_name, variant, &field.name),
None => RegexKey::for_struct(type_name, &field.name),
};
regex_lookup.get(&key).map(std::string::String::as_str)
} else {
None
};
let validation_attrs = if include_validation {
Self::generate_validation_attrs(regex_const, &field.validation_attrs)
} else {
quote! {}
};
let deprecated_attr = if field.deprecated {
quote! { #[deprecated] }
} else {
quote! {}
};
let default_attr = if add_pub && field.default_value.is_some() {
let default_expr = Self::json_value_to_rust_expr(field.default_value.as_ref().unwrap(), &field.rust_type);
quote! { #[default(#default_expr)] }
} else {
quote! {}
};
let type_tokens = Self::parse_type_string(&field.rust_type.to_rust_type());
if add_pub {
let vis = visibility.to_tokens();
quote! {
#(#extra_attrs)*
#docs
#deprecated_attr
#serde_attrs
#validation_attrs
#default_attr
#vis #name: #type_tokens
}
} else {
quote! {
#(#extra_attrs)*
#docs
#deprecated_attr
#serde_attrs
#validation_attrs
#name: #type_tokens
}
}
})
.collect()
}
fn generate_variants(
type_name: &str,
variants: &[VariantDef],
regex_lookup: &BTreeMap<RegexKey, String>,
) -> Vec<TokenStream> {
variants
.iter()
.enumerate()
.map(|(idx, variant)| {
let name = format_ident!("{}", variant.name);
let docs = Self::generate_docs(&variant.docs);
let serde_attrs = Self::generate_serde_attrs(&variant.serde_attrs);
let deprecated_attr = if variant.deprecated {
quote! { #[deprecated] }
} else {
quote! {}
};
let default_attr = if idx == 0 {
quote! { #[default] }
} else {
quote! {}
};
let content = match &variant.content {
VariantContent::Unit => quote! {},
VariantContent::Tuple(types) => {
let type_tokens: Vec<_> = types
.iter()
.map(|t| Self::parse_type_string(&t.to_rust_type()))
.collect();
quote! { ( #(#type_tokens),* ) }
}
VariantContent::Struct(fields) => {
let field_tokens = Self::generate_fields_with_visibility(
type_name,
Some(&variant.name),
fields,
false,
false,
regex_lookup,
Visibility::File,
);
quote! { { #(#field_tokens),* } }
}
};
quote! {
#docs
#deprecated_attr
#serde_attrs
#default_attr
#name #content
}
})
.collect()
}
fn parse_type_string(type_str: &str) -> TokenStream {
type_str.parse().unwrap_or_else(|_| quote! { serde_json::Value })
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::generator::ast::{FieldDef, StructDef, StructKind, TypeRef};
fn create_test_struct(name: &str, kind: StructKind) -> StructDef {
StructDef {
name: name.to_string(),
docs: vec![],
fields: vec![FieldDef {
name: "field1".to_string(),
docs: vec![],
rust_type: TypeRef::new("String"),
serde_attrs: vec![],
extra_attrs: vec![],
validation_attrs: vec!["length(min = 1)".to_string()],
regex_validation: None,
default_value: None,
read_only: false,
write_only: false,
deprecated: false,
multiple_of: None,
}],
derives: vec![
"Debug".to_string(),
"Clone".to_string(),
"Serialize".to_string(),
"Deserialize".to_string(),
],
serde_attrs: vec![],
outer_attrs: vec![],
methods: vec![],
kind,
}
}
fn contains_derive(tokens: &TokenStream, derive_name: &str) -> bool {
let code = tokens.to_string();
code.contains("derive (") && code.contains(derive_name)
}
fn contains_validation(tokens: &TokenStream) -> bool {
let code = tokens.to_string();
code.contains("validate") && (code.contains("# [validate") || code.contains("#[validate"))
}
#[test]
fn test_schema_struct_no_type_usage() {
let def = create_test_struct("TestSchema", StructKind::Schema);
let type_usage = BTreeMap::new();
let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Public);
let code = result.to_string();
assert!(contains_derive(&result, "Debug"), "Should contain Debug derive");
assert!(contains_derive(&result, "Clone"), "Should contain Clone derive");
assert!(contains_derive(&result, "Serialize"), "Should contain Serialize derive");
assert!(
contains_derive(&result, "Deserialize"),
"Should contain Deserialize derive"
);
assert!(contains_validation(&result), "Should include validation attributes");
assert!(code.contains("pub struct TestSchema"), "Should be public struct");
}
#[test]
fn test_schema_struct_with_request_only_usage() {
let def = create_test_struct("RequestSchema", StructKind::Schema);
let mut type_usage = BTreeMap::new();
type_usage.insert("RequestSchema".to_string(), TypeUsage::RequestOnly);
let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Public);
assert!(contains_derive(&result, "Serialize"), "Should contain Serialize derive");
assert!(
contains_derive(&result, "validator :: Validate"),
"Should contain Validate derive"
);
assert!(contains_validation(&result), "Should include validation attributes");
}
#[test]
fn test_schema_struct_with_response_only_usage() {
let def = create_test_struct("ResponseSchema", StructKind::Schema);
let mut type_usage = BTreeMap::new();
type_usage.insert("ResponseSchema".to_string(), TypeUsage::ResponseOnly);
let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Public);
assert!(
contains_derive(&result, "Deserialize"),
"Should contain Deserialize derive"
);
assert!(contains_validation(&result), "Should include validation attributes");
}
#[test]
fn test_schema_struct_with_bidirectional_usage() {
let def = create_test_struct("BidirectionalSchema", StructKind::Schema);
let mut type_usage = BTreeMap::new();
type_usage.insert("BidirectionalSchema".to_string(), TypeUsage::Bidirectional);
let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Public);
assert!(contains_derive(&result, "Serialize"), "Should contain Serialize derive");
assert!(
contains_derive(&result, "Deserialize"),
"Should contain Deserialize derive"
);
assert!(
contains_derive(&result, "validator :: Validate"),
"Should contain Validate derive"
);
assert!(contains_validation(&result), "Should include validation attributes");
}
#[test]
fn test_operation_request_struct() {
let def = create_test_struct("GetUsersRequest", StructKind::OperationRequest);
let mut type_usage = BTreeMap::new();
type_usage.insert("GetUsersRequest".to_string(), TypeUsage::RequestOnly);
let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Public);
let code = result.to_string();
assert!(contains_derive(&result, "Debug"), "Should contain Debug derive");
assert!(contains_derive(&result, "Clone"), "Should contain Clone derive");
assert!(
contains_derive(&result, "validator :: Validate"),
"Should contain Validate derive"
);
assert!(
contains_derive(&result, "oas3_gen_support :: Default"),
"Should contain Default derive"
);
assert!(!code.contains("Serialize"), "Should NOT contain Serialize derive");
assert!(!code.contains("Deserialize"), "Should NOT contain Deserialize derive");
assert!(contains_validation(&result), "Should include validation attributes");
}
#[test]
fn test_request_body_struct_request_only() {
let def = create_test_struct("CreateUserRequestBody", StructKind::RequestBody);
let mut type_usage = BTreeMap::new();
type_usage.insert("CreateUserRequestBody".to_string(), TypeUsage::RequestOnly);
let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Public);
let code = result.to_string();
assert!(contains_derive(&result, "Debug"), "Should contain Debug derive");
assert!(contains_derive(&result, "Clone"), "Should contain Clone derive");
assert!(contains_derive(&result, "Serialize"), "Should contain Serialize derive");
assert!(
contains_derive(&result, "validator :: Validate"),
"Should contain Validate derive"
);
assert!(
contains_derive(&result, "oas3_gen_support :: Default"),
"Should contain Default derive"
);
assert!(!code.contains("Deserialize"), "Should NOT contain Deserialize derive");
assert!(contains_validation(&result), "Should include validation attributes");
}
#[test]
fn test_request_body_struct_response_only() {
let def = create_test_struct("GetUserResponseBody", StructKind::RequestBody);
let mut type_usage = BTreeMap::new();
type_usage.insert("GetUserResponseBody".to_string(), TypeUsage::ResponseOnly);
let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Public);
let code = result.to_string();
assert!(contains_derive(&result, "Debug"), "Should contain Debug derive");
assert!(contains_derive(&result, "Clone"), "Should contain Clone derive");
assert!(
contains_derive(&result, "Deserialize"),
"Should contain Deserialize derive"
);
assert!(
contains_derive(&result, "oas3_gen_support :: Default"),
"Should contain Default derive"
);
assert!(!code.contains("Serialize"), "Should NOT contain Serialize derive");
assert!(
!code.contains("validator :: Validate"),
"Should NOT contain Validate derive"
);
assert!(
!contains_validation(&result),
"Should NOT include validation attributes"
);
}
#[test]
fn test_request_body_struct_bidirectional() {
let def = create_test_struct("UpdateUserRequestBody", StructKind::RequestBody);
let mut type_usage = BTreeMap::new();
type_usage.insert("UpdateUserRequestBody".to_string(), TypeUsage::Bidirectional);
let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Public);
assert!(contains_derive(&result, "Debug"), "Should contain Debug derive");
assert!(contains_derive(&result, "Clone"), "Should contain Clone derive");
assert!(contains_derive(&result, "Serialize"), "Should contain Serialize derive");
assert!(
contains_derive(&result, "Deserialize"),
"Should contain Deserialize derive"
);
assert!(
contains_derive(&result, "validator :: Validate"),
"Should contain Validate derive"
);
assert!(
contains_derive(&result, "oas3_gen_support :: Default"),
"Should contain Default derive"
);
assert!(contains_validation(&result), "Should include validation attributes");
}
#[test]
fn test_request_body_struct_no_usage_defaults_to_bidirectional() {
let def = create_test_struct("UnknownRequestBody", StructKind::RequestBody);
let type_usage = BTreeMap::new(); let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Public);
assert!(contains_derive(&result, "Serialize"), "Should contain Serialize derive");
assert!(
contains_derive(&result, "Deserialize"),
"Should contain Deserialize derive"
);
assert!(
contains_derive(&result, "validator :: Validate"),
"Should contain Validate derive"
);
}
#[test]
fn test_visibility_crate() {
let def = create_test_struct("CrateStruct", StructKind::Schema);
let type_usage = BTreeMap::new();
let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Crate);
let code = result.to_string();
assert!(
code.contains("pub (crate) struct CrateStruct"),
"Should be pub(crate) struct"
);
}
#[test]
fn test_visibility_file() {
let def = create_test_struct("FileStruct", StructKind::Schema);
let type_usage = BTreeMap::new();
let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::File);
let code = result.to_string();
assert!(code.contains("struct FileStruct"), "Should be file-private struct");
assert!(!code.contains("pub struct"), "Should NOT have pub modifier");
}
#[test]
fn test_struct_with_methods_generates_impl_block() {
use crate::generator::ast::{PathSegment, StructMethod, StructMethodKind};
let mut def = create_test_struct("RequestWithMethod", StructKind::OperationRequest);
def.methods.push(StructMethod {
name: "render_path".to_string(),
docs: vec!["/// Renders the path".to_string()],
kind: StructMethodKind::RenderPath {
segments: vec![PathSegment::Literal("/api/users".to_string())],
query_params: vec![],
},
attrs: vec!["must_use".to_string()],
});
let type_usage = BTreeMap::new();
let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Public);
let code = result.to_string();
assert!(code.contains("impl RequestWithMethod"), "Should generate impl block");
assert!(code.contains("fn render_path"), "Should include render_path method");
}
#[test]
fn test_regex_validation_lookup() {
let mut def = create_test_struct("RegexStruct", StructKind::Schema);
def.fields[0].regex_validation = Some("[a-z]+".to_string());
let type_usage = BTreeMap::new();
let mut regex_lookup = BTreeMap::new();
let key = RegexKey::for_struct("RegexStruct", "field1");
regex_lookup.insert(key, "REGEX_CONST_NAME".to_string());
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Public);
let code = result.to_string();
assert!(code.contains("regex"), "Should include regex validation");
assert!(code.contains("REGEX_CONST_NAME"), "Should reference the regex constant");
}
#[test]
fn test_operation_request_ignores_type_usage_for_derives() {
let def = create_test_struct("RequestIgnoresUsage", StructKind::OperationRequest);
let mut type_usage = BTreeMap::new();
type_usage.insert("RequestIgnoresUsage".to_string(), TypeUsage::ResponseOnly);
let regex_lookup = BTreeMap::new();
let result = CodeGenerator::generate_struct(&def, ®ex_lookup, &type_usage, Visibility::Public);
let code = result.to_string();
assert!(
contains_derive(&result, "validator :: Validate"),
"Should contain Validate derive"
);
assert!(!code.contains("Serialize"), "Should NOT have Serialize");
assert!(!code.contains("Deserialize"), "Should NOT have Deserialize");
assert!(contains_validation(&result), "Should include validation attributes");
}
fn assert_conversion(value: &serde_json::Value, rust_type: &TypeRef, expected: &str) {
let result = CodeGenerator::json_value_to_rust_expr(value, rust_type);
let code = result.to_string();
assert_eq!(code.trim(), expected.trim(), "Conversion mismatch");
}
#[test]
fn test_string_empty() {
let value = json!("");
let rust_type = TypeRef::new("String");
assert_conversion(&value, &rust_type, "String :: new ()");
}
#[test]
fn test_string_regular() {
let value = json!("hello");
let rust_type = TypeRef::new("String");
assert_conversion(&value, &rust_type, r#""hello" . to_string ()"#);
}
#[test]
fn test_string_with_spaces() {
let value = json!("hello world");
let rust_type = TypeRef::new("String");
assert_conversion(&value, &rust_type, r#""hello world" . to_string ()"#);
}
#[test]
fn test_number_to_string() {
let value = json!(42);
let rust_type = TypeRef::new("String");
assert_conversion(&value, &rust_type, r#""42" . to_string ()"#);
}
#[test]
fn test_float_to_string() {
let value = json!(2.5);
let rust_type = TypeRef::new("String");
assert_conversion(&value, &rust_type, r#""2.5" . to_string ()"#);
}
#[test]
fn test_bool_to_string() {
let value = json!(true);
let rust_type = TypeRef::new("String");
assert_conversion(&value, &rust_type, r#""true" . to_string ()"#);
}
#[test]
fn test_int_from_number() {
let value = json!(42);
let rust_type = TypeRef::new("i64");
assert_conversion(&value, &rust_type, "42i64");
}
#[test]
fn test_int_from_string() {
let value = json!("123");
let rust_type = TypeRef::new("i64");
assert_conversion(&value, &rust_type, "123i64");
}
#[test]
fn test_int_invalid_string() {
let value = json!("not_a_number");
let rust_type = TypeRef::new("i64");
assert_conversion(&value, &rust_type, "Default :: default ()");
}
#[test]
fn test_float_from_float() {
let value = json!(2.5);
let rust_type = TypeRef::new("f64");
assert_conversion(&value, &rust_type, "2.5f64");
}
#[test]
fn test_float_from_integer_coercion() {
let value = json!(10);
let rust_type = TypeRef::new("f64");
assert_conversion(&value, &rust_type, "10f64");
}
#[test]
fn test_float_from_string() {
let value = json!("2.718");
let rust_type = TypeRef::new("f64");
assert_conversion(&value, &rust_type, "2.718f64");
}
#[test]
fn test_float_invalid_string() {
let value = json!("not_a_float");
let rust_type = TypeRef::new("f64");
assert_conversion(&value, &rust_type, "Default :: default ()");
}
#[test]
fn test_bool_from_bool() {
let value = json!(true);
let rust_type = TypeRef::new("bool");
assert_conversion(&value, &rust_type, "true");
let value = json!(false);
assert_conversion(&value, &rust_type, "false");
}
#[test]
fn test_bool_from_number_nonzero() {
let value = json!(1);
let rust_type = TypeRef::new("bool");
assert_conversion(&value, &rust_type, "true");
let value = json!(42);
assert_conversion(&value, &rust_type, "true");
}
#[test]
fn test_bool_from_number_zero() {
let value = json!(0);
let rust_type = TypeRef::new("bool");
assert_conversion(&value, &rust_type, "false");
}
#[test]
fn test_bool_from_string_true() {
let value = json!("true");
let rust_type = TypeRef::new("bool");
assert_conversion(&value, &rust_type, "true");
let value = json!("True");
assert_conversion(&value, &rust_type, "true");
let value = json!("TRUE");
assert_conversion(&value, &rust_type, "true");
let value = json!("1");
assert_conversion(&value, &rust_type, "true");
let value = json!("yes");
assert_conversion(&value, &rust_type, "true");
let value = json!("Yes");
assert_conversion(&value, &rust_type, "true");
}
#[test]
fn test_bool_from_string_false() {
let value = json!("false");
let rust_type = TypeRef::new("bool");
assert_conversion(&value, &rust_type, "false");
let value = json!("no");
assert_conversion(&value, &rust_type, "false");
let value = json!("0");
assert_conversion(&value, &rust_type, "false");
let value = json!("anything");
assert_conversion(&value, &rust_type, "false");
}
#[test]
fn test_null_value() {
let value = json!(null);
let rust_type = TypeRef::new("String");
assert_conversion(&value, &rust_type, "None");
}
#[test]
fn test_nullable_string_with_value() {
let value = json!("hello");
let mut rust_type = TypeRef::new("String");
rust_type.nullable = true;
assert_conversion(&value, &rust_type, r#"Some ("hello" . to_string ())"#);
}
#[test]
fn test_nullable_int_with_value() {
let value = json!(42);
let mut rust_type = TypeRef::new("i64");
rust_type.nullable = true;
assert_conversion(&value, &rust_type, "Some (42i64)");
}
#[test]
fn test_nullable_float_with_value() {
let value = json!(2.5);
let mut rust_type = TypeRef::new("f64");
rust_type.nullable = true;
assert_conversion(&value, &rust_type, "Some (2.5f64)");
}
#[test]
fn test_nullable_bool_with_value() {
let value = json!(true);
let mut rust_type = TypeRef::new("bool");
rust_type.nullable = true;
assert_conversion(&value, &rust_type, "Some (true)");
}
#[test]
fn test_nullable_with_null() {
let value = json!(null);
let mut rust_type = TypeRef::new("String");
rust_type.nullable = true;
assert_conversion(&value, &rust_type, "None");
}
#[test]
fn test_array_defaults() {
let value = json!([1, 2, 3]);
let rust_type = TypeRef::new("Vec<i64>");
assert_conversion(&value, &rust_type, "Default :: default ()");
}
#[test]
fn test_object_defaults() {
let value = json!({"key": "value"});
let rust_type = TypeRef::new("CustomType");
assert_conversion(&value, &rust_type, "Default :: default ()");
}
#[test]
fn test_int_types_i32() {
let value = json!(100);
let rust_type = TypeRef::new("i32");
assert_conversion(&value, &rust_type, "100i64");
}
#[test]
fn test_int_types_i16() {
let value = json!(50);
let rust_type = TypeRef::new("i16");
assert_conversion(&value, &rust_type, "50i64");
}
#[test]
fn test_float_types_f32() {
let value = json!(1.5);
let rust_type = TypeRef::new("f32");
assert_conversion(&value, &rust_type, "1.5f64");
}
#[test]
fn test_negative_numbers() {
let value = json!(-42);
let rust_type = TypeRef::new("i64");
assert_conversion(&value, &rust_type, "- 42i64");
let value = json!(-2.5);
let rust_type = TypeRef::new("f64");
assert_conversion(&value, &rust_type, "- 2.5f64");
}
#[test]
fn test_zero_values() {
let value = json!(0);
let rust_type = TypeRef::new("i64");
assert_conversion(&value, &rust_type, "0i64");
let value = json!(0.0);
let rust_type = TypeRef::new("f64");
assert_conversion(&value, &rust_type, "0f64");
}
#[test]
fn test_unsigned_from_number() {
let value = json!(42);
let rust_type = TypeRef::new("u32");
assert_conversion(&value, &rust_type, "42u64");
let value = json!(255);
let rust_type = TypeRef::new("u8");
assert_conversion(&value, &rust_type, "255u64");
}
#[test]
fn test_unsigned_from_string() {
let value = json!("100");
let rust_type = TypeRef::new("u64");
assert_conversion(&value, &rust_type, "100u64");
}
#[test]
fn test_unsigned_invalid_negative() {
let value = json!(-1);
let rust_type = TypeRef::new("u32");
assert_conversion(&value, &rust_type, "Default :: default ()");
}
#[test]
fn test_format_based_type_coercion() {
let value = json!(100);
let rust_type = TypeRef::new("i32");
assert_conversion(&value, &rust_type, "100i64");
let value = json!(1.5);
let rust_type = TypeRef::new("f32");
assert_conversion(&value, &rust_type, "1.5f64");
}
}