use std::collections::BTreeSet;
use oas3::spec::{ObjectOrReference, ObjectSchema, Schema, SchemaType, SchemaTypeSet};
use regex::Regex;
use serde_json::Number;
use super::{
SchemaGraph,
ast::{EnumDef, FieldDef, RustType, StructDef, TypeRef, VariantContent, VariantDef},
utils::doc_comment_lines,
};
use crate::reserved::{to_rust_field_name, to_rust_type_name};
pub struct SchemaConverter<'a> {
graph: &'a SchemaGraph,
}
impl<'a> SchemaConverter<'a> {
pub fn new(graph: &'a SchemaGraph) -> Self {
Self { graph }
}
pub fn convert_schema(&self, name: &str, schema: &ObjectSchema) -> anyhow::Result<Vec<RustType>> {
if !schema.all_of.is_empty() {
return self.convert_all_of_schema(name, schema);
}
if !schema.one_of.is_empty() {
return Ok(vec![self.convert_one_of_enum(name, schema)?]);
}
if !schema.any_of.is_empty() {
return self.convert_any_of_enum(name, schema);
}
if !schema.enum_values.is_empty() {
return Ok(vec![self.convert_simple_enum(name, schema, &schema.enum_values)?]);
}
if !schema.properties.is_empty() {
let (main_type, inline_types) = self.convert_struct(name, schema)?;
let mut all_types = vec![main_type];
all_types.extend(inline_types);
return Ok(all_types);
}
Ok(vec![])
}
fn convert_all_of_schema(&self, name: &str, schema: &ObjectSchema) -> anyhow::Result<Vec<RustType>> {
let mut merged_schema = schema.clone();
for all_of_ref in &schema.all_of {
if let Ok(all_of_schema) = all_of_ref.resolve(self.graph.spec()) {
for (prop_name, prop_ref) in &all_of_schema.properties {
merged_schema.properties.insert(prop_name.clone(), prop_ref.clone());
}
merged_schema.required.extend(all_of_schema.required.iter().cloned());
}
}
let (main_type, inline_types) = self.convert_struct(name, &merged_schema)?;
let mut all_types = vec![main_type];
all_types.extend(inline_types);
Ok(all_types)
}
fn convert_one_of_enum(&self, name: &str, schema: &ObjectSchema) -> anyhow::Result<RustType> {
let mut variants = Vec::new();
let mut seen_names = BTreeSet::new();
let discriminator_property = schema.discriminator.as_ref().map(|d| d.property_name.as_str());
for (i, variant_schema_ref) in schema.one_of.iter().enumerate() {
if let Ok(variant_schema) = variant_schema_ref.resolve(self.graph.spec()) {
if variant_schema.schema_type == Some(SchemaTypeSet::Single(SchemaType::Null)) {
continue;
}
let mut variant_name = if let Some(ref title) = variant_schema.title {
to_rust_type_name(title)
} else {
self.infer_variant_name(&variant_schema, i)
};
if seen_names.contains(&variant_name) {
variant_name = format!("{}{}", variant_name, i);
}
seen_names.insert(variant_name.clone());
let docs = variant_schema
.description
.as_ref()
.map(|d| doc_comment_lines(d))
.unwrap_or_default();
let deprecated = variant_schema.deprecated.unwrap_or(false);
let content = if discriminator_property.is_some() {
if !variant_schema.properties.is_empty() {
let fields = self.convert_fields_with_exclusions(&variant_schema, discriminator_property)?;
VariantContent::Struct(fields)
} else {
let type_ref = self.schema_to_type_ref(&variant_schema)?;
let field = FieldDef {
name: "value".to_string(),
docs: vec![],
rust_type: type_ref,
optional: false,
serde_attrs: vec![],
validation_attrs: vec![],
regex_validation: None,
default_value: None,
read_only: false,
write_only: false,
deprecated: false,
multiple_of: None,
unique_items: false,
};
VariantContent::Struct(vec![field])
}
} else {
if let Some(ref title) = variant_schema.title {
if self.graph.get_schema(title).is_some() {
let type_ref = TypeRef::new(to_rust_type_name(title));
VariantContent::Tuple(vec![type_ref])
} else if !variant_schema.properties.is_empty() {
let fields = self.convert_fields(&variant_schema)?;
VariantContent::Struct(fields)
} else {
let type_ref = self.schema_to_type_ref(&variant_schema)?;
VariantContent::Tuple(vec![type_ref])
}
} else if !variant_schema.properties.is_empty() {
let fields = self.convert_fields(&variant_schema)?;
VariantContent::Struct(fields)
} else {
let type_ref = self.schema_to_type_ref(&variant_schema)?;
VariantContent::Tuple(vec![type_ref])
}
};
variants.push(VariantDef {
name: to_rust_type_name(&variant_name),
docs,
content,
serde_attrs: vec![],
deprecated,
});
}
}
let discriminator = schema.discriminator.as_ref().map(|d| d.property_name.clone());
Ok(RustType::Enum(EnumDef {
name: to_rust_type_name(name),
docs: schema
.description
.as_ref()
.map(|d| doc_comment_lines(d))
.unwrap_or_default(),
variants,
discriminator,
derives: vec!["Debug".into(), "Clone".into(), "Serialize".into(), "Deserialize".into()],
serde_attrs: vec![],
}))
}
fn convert_any_of_enum(&self, name: &str, schema: &ObjectSchema) -> anyhow::Result<Vec<RustType>> {
let has_freeform_string = schema.any_of.iter().any(|s| {
if let Ok(resolved) = s.resolve(self.graph.spec()) {
resolved.const_value.is_none() && resolved.schema_type == Some(SchemaTypeSet::Single(SchemaType::String))
} else {
false
}
});
let const_values: Vec<_> = schema
.any_of
.iter()
.filter_map(|s| {
if let Ok(resolved) = s.resolve(self.graph.spec()) {
resolved.const_value.as_ref().map(|v| {
(
v.clone(),
resolved.description.clone(),
resolved.deprecated.unwrap_or(false),
)
})
} else {
None
}
})
.collect();
if has_freeform_string && !const_values.is_empty() {
return self.convert_string_enum_with_catchall(name, schema, &const_values);
}
let mut variants = Vec::new();
let mut seen_names = BTreeSet::new();
for (i, variant_schema_ref) in schema.any_of.iter().enumerate() {
if let Ok(variant_schema) = variant_schema_ref.resolve(self.graph.spec()) {
if variant_schema.schema_type == Some(SchemaTypeSet::Single(SchemaType::Null)) {
continue;
}
let mut variant_name = if let Some(ref title) = variant_schema.title {
to_rust_type_name(title)
} else {
self.infer_variant_name(&variant_schema, i)
};
if seen_names.contains(&variant_name) {
variant_name = format!("{}{}", variant_name, i);
}
seen_names.insert(variant_name.clone());
let docs = variant_schema
.description
.as_ref()
.map(|d| doc_comment_lines(d))
.unwrap_or_default();
let deprecated = variant_schema.deprecated.unwrap_or(false);
let content = if let Some(ref title) = variant_schema.title {
if self.graph.get_schema(title).is_some() {
let type_ref = TypeRef::new(to_rust_type_name(title));
VariantContent::Tuple(vec![type_ref])
} else if !variant_schema.properties.is_empty() {
let fields = self.convert_fields(&variant_schema)?;
VariantContent::Struct(fields)
} else {
let type_ref = self.schema_to_type_ref(&variant_schema)?;
VariantContent::Tuple(vec![type_ref])
}
} else if !variant_schema.properties.is_empty() {
let fields = self.convert_fields(&variant_schema)?;
VariantContent::Struct(fields)
} else {
let type_ref = self.schema_to_type_ref(&variant_schema)?;
VariantContent::Tuple(vec![type_ref])
};
variants.push(VariantDef {
name: to_rust_type_name(&variant_name),
docs,
content,
serde_attrs: vec![],
deprecated,
});
}
}
let enum_name = to_rust_type_name(name);
for variant in &mut variants {
if let VariantContent::Struct(ref mut fields) = variant.content {
for field in fields {
if field.rust_type.base_type == enum_name && !field.rust_type.boxed {
field.rust_type = field.rust_type.clone().with_boxed();
}
}
}
}
Ok(vec![RustType::Enum(EnumDef {
name: enum_name,
docs: schema
.description
.as_ref()
.map(|d| doc_comment_lines(d))
.unwrap_or_default(),
variants,
discriminator: None,
derives: vec!["Debug".into(), "Clone".into(), "Serialize".into(), "Deserialize".into()],
serde_attrs: vec!["untagged".into()],
})])
}
fn convert_string_enum_with_catchall(
&self,
name: &str,
schema: &ObjectSchema,
const_values: &[(serde_json::Value, Option<String>, bool)],
) -> anyhow::Result<Vec<RustType>> {
let base_name = to_rust_type_name(name);
let known_name = format!("{}Known", base_name);
let mut known_variants = Vec::new();
let mut seen_names = BTreeSet::new();
for (i, (value, description, deprecated)) in const_values.iter().enumerate() {
if let Some(str_val) = value.as_str() {
let mut variant_name = to_rust_type_name(str_val);
if seen_names.contains(&variant_name) {
variant_name = format!("{}{}", variant_name, i);
}
seen_names.insert(variant_name.clone());
let docs = description.as_ref().map(|d| doc_comment_lines(d)).unwrap_or_default();
known_variants.push(VariantDef {
name: variant_name,
docs,
content: VariantContent::Unit,
serde_attrs: vec![format!("rename = \"{}\"", str_val)],
deprecated: *deprecated,
});
}
}
let inner_enum = RustType::Enum(EnumDef {
name: known_name.clone(),
docs: vec!["/// Known string values".to_string()],
variants: known_variants,
discriminator: None,
derives: vec![
"Debug".into(),
"Clone".into(),
"PartialEq".into(),
"Eq".into(),
"Serialize".into(),
"Deserialize".into(),
],
serde_attrs: vec![],
});
let outer_variants = vec![
VariantDef {
name: "Known".to_string(),
docs: vec!["/// A known string value".to_string()],
content: VariantContent::Tuple(vec![TypeRef::new(&known_name)]),
serde_attrs: vec![],
deprecated: false,
},
VariantDef {
name: "Other".to_string(),
docs: vec!["/// An unknown string value not in the known set".to_string()],
content: VariantContent::Tuple(vec![TypeRef::new("String")]),
serde_attrs: vec![],
deprecated: false,
},
];
let outer_enum = RustType::Enum(EnumDef {
name: base_name,
docs: schema
.description
.as_ref()
.map(|d| doc_comment_lines(d))
.unwrap_or_default(),
variants: outer_variants,
discriminator: None,
derives: vec![
"Debug".into(),
"Clone".into(),
"PartialEq".into(),
"Eq".into(),
"Serialize".into(),
"Deserialize".into(),
],
serde_attrs: vec!["untagged".into()],
});
Ok(vec![inner_enum, outer_enum])
}
fn infer_variant_name(&self, schema: &ObjectSchema, index: usize) -> String {
if !schema.enum_values.is_empty() {
return "Enum".to_string();
}
if let Some(ref schema_type) = schema.schema_type {
match schema_type {
SchemaTypeSet::Single(typ) => match typ {
SchemaType::String => "String".to_string(),
SchemaType::Number => "Number".to_string(),
SchemaType::Integer => "Integer".to_string(),
SchemaType::Boolean => "Boolean".to_string(),
SchemaType::Array => "Array".to_string(),
SchemaType::Object => "Object".to_string(),
SchemaType::Null => "Null".to_string(),
},
SchemaTypeSet::Multiple(_) => "Mixed".to_string(),
}
} else {
format!("Variant{}", index)
}
}
fn convert_simple_enum(
&self,
name: &str,
schema: &ObjectSchema,
enum_values: &[serde_json::Value],
) -> anyhow::Result<RustType> {
let mut variants = Vec::new();
let mut seen_names = BTreeSet::new();
for (i, value) in enum_values.iter().enumerate() {
if let Some(str_val) = value.as_str() {
let mut variant_name = to_rust_type_name(str_val);
if seen_names.contains(&variant_name) {
variant_name = format!("{}{}", variant_name, i);
}
seen_names.insert(variant_name.clone());
variants.push(VariantDef {
name: variant_name,
docs: vec![],
content: VariantContent::Unit,
serde_attrs: vec![format!("rename = \"{}\"", str_val)],
deprecated: false,
});
}
}
Ok(RustType::Enum(EnumDef {
name: to_rust_type_name(name),
docs: schema
.description
.as_ref()
.map(|d| doc_comment_lines(d))
.unwrap_or_default(),
variants,
discriminator: None,
derives: vec!["Debug".into(), "Clone".into(), "Serialize".into(), "Deserialize".into()],
serde_attrs: vec![],
}))
}
pub fn convert_struct(&self, name: &str, schema: &ObjectSchema) -> anyhow::Result<(RustType, Vec<RustType>)> {
let (mut fields, inline_types) = self.convert_fields_with_inline_types(name, schema)?;
let mut serde_attrs = vec![];
if let Some(ref additional) = schema.additional_properties {
match additional {
Schema::Boolean(bool_schema) => {
if !bool_schema.0 {
serde_attrs.push("deny_unknown_fields".to_string());
}
}
Schema::Object(schema_ref) => {
if let Ok(additional_schema) = schema_ref.resolve(self.graph.spec()) {
let value_type = self.schema_to_type_ref(&additional_schema)?;
let map_type = TypeRef::new(format!(
"std::collections::HashMap<String, {}>",
value_type.to_rust_type()
));
fields.push(FieldDef {
name: "additional_properties".to_string(),
docs: vec!["/// Additional properties not defined in the schema".to_string()],
rust_type: map_type,
optional: false,
serde_attrs: vec!["flatten".to_string()],
validation_attrs: vec![],
regex_validation: None,
default_value: None,
read_only: false,
write_only: false,
deprecated: false,
multiple_of: None,
unique_items: false,
});
}
}
}
}
let all_fields_defaultable = fields.iter().all(|f| {
f.default_value.is_some()
|| f.rust_type.nullable
|| f.rust_type.is_array
|| matches!(
f.rust_type.base_type.as_str(),
"String"
| "bool"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "f32"
| "f64"
| "serde_json::Value"
)
});
if all_fields_defaultable && fields.iter().any(|f| f.default_value.is_some()) {
serde_attrs.push("default".to_string());
}
let all_read_only = !fields.is_empty() && fields.iter().all(|f| f.read_only);
let all_write_only = !fields.is_empty() && fields.iter().all(|f| f.write_only);
let mut derives = vec!["Debug".into(), "Clone".into()];
if !all_read_only {
derives.push("Serialize".into());
}
if !all_write_only {
derives.push("Deserialize".into());
}
derives.push("Validate".into());
let struct_type = RustType::Struct(StructDef {
name: to_rust_type_name(name),
docs: schema
.description
.as_ref()
.map(|d| doc_comment_lines(d))
.unwrap_or_default(),
fields,
derives,
serde_attrs,
});
Ok((struct_type, inline_types))
}
pub fn extract_validation_pattern<'s>(&self, prop_name: &str, schema: &'s ObjectSchema) -> Option<&'s String> {
match (schema.schema_type.as_ref(), schema.pattern.as_ref()) {
(Some(SchemaTypeSet::Single(SchemaType::String)), Some(pattern)) => {
if Regex::new(pattern).is_ok() {
Some(pattern)
} else {
eprintln!(
"Warning: Invalid regex pattern '{}' for property '{}'",
pattern, prop_name
);
None
}
}
_ => None,
}
}
fn render_number(is_float: bool, num: &Number) -> String {
if is_float {
if num.to_string().contains(".") {
num.to_string()
} else {
format!("{}.0", num)
}
} else {
format!("{}i64", num.as_i64().unwrap_or_default())
}
}
pub fn extract_validation_attrs(&self, _prop_name: &str, is_required: bool, schema: &ObjectSchema) -> Vec<String> {
let mut attrs = Vec::new();
if let Some(ref format) = schema.format {
match format.as_str() {
"email" => attrs.push("email".to_string()),
"uri" | "url" => attrs.push("url".to_string()),
_ => {}
}
}
if let Some(ref schema_type) = schema.schema_type {
if matches!(
schema_type,
SchemaTypeSet::Single(SchemaType::Number) | SchemaTypeSet::Single(SchemaType::Integer)
) {
let mut parts = Vec::<String>::new();
let is_float = matches!(schema_type, SchemaTypeSet::Single(SchemaType::Number));
if schema.multiple_of.is_some() {
}
if let Some(exclusive_min) = schema
.exclusive_minimum
.as_ref()
.map(|v| format!("exclusive_min = {}", Self::render_number(is_float, v)))
{
parts.push(exclusive_min);
}
if let Some(exclusive_max) = schema
.exclusive_maximum
.as_ref()
.map(|v| format!("exclusive_max = {}", Self::render_number(is_float, v)))
{
parts.push(exclusive_max);
}
if let Some(min) = schema
.minimum
.as_ref()
.map(|v| format!("min = {}", Self::render_number(is_float, v)))
{
parts.push(min);
}
if let Some(max) = schema
.maximum
.as_ref()
.map(|v| format!("max = {}", Self::render_number(is_float, v)))
{
parts.push(max);
}
if !parts.is_empty() {
attrs.push(format!("range({})", parts.join(", ")));
}
}
if matches!(schema_type, SchemaTypeSet::Single(SchemaType::String)) && schema.enum_values.is_empty() {
let is_non_string_format = schema
.format
.as_ref()
.map(|f| matches!(f.as_str(), "date" | "date-time" | "time" | "binary" | "byte" | "uuid"))
.unwrap_or(false);
if !is_non_string_format {
if let (Some(min), Some(max)) = (schema.min_length, schema.max_length) {
attrs.push(format!("length(min = {min}, max = {max})"));
} else if let Some(min) = schema.min_length {
attrs.push(format!("length(min = {min})"));
} else if let Some(max) = schema.max_length {
attrs.push(format!("length(max = {max})"));
} else if is_required {
attrs.push("length(min = 1)".to_string());
}
}
}
if matches!(schema_type, SchemaTypeSet::Single(SchemaType::Array)) {
if let (Some(min), Some(max)) = (schema.min_items, schema.max_items) {
attrs.push(format!("length(min = {min}, max = {max})"));
} else if let Some(min) = schema.min_items {
attrs.push(format!("length(min = {min})"));
} else if let Some(max) = schema.max_items {
attrs.push(format!("length(max = {max})"));
}
}
}
attrs
}
pub fn extract_default_value(&self, schema: &ObjectSchema) -> Option<serde_json::Value> {
schema.default.clone()
}
fn convert_fields_with_exclusions(
&self,
schema: &ObjectSchema,
exclude_field: Option<&str>,
) -> anyhow::Result<Vec<FieldDef>> {
let mut fields = Vec::new();
let mut properties: Vec<_> = schema.properties.iter().collect();
properties.sort_by(|(a, _), (b, _)| a.cmp(b));
for (prop_name, prop_schema_ref) in properties {
if let Some(exclude) = exclude_field
&& prop_name == exclude
{
continue;
}
let rust_type = if let ObjectOrReference::Ref { ref_path, .. } = prop_schema_ref {
if let Some(ref_name) = SchemaGraph::extract_ref_name(ref_path) {
TypeRef::new(to_rust_type_name(&ref_name))
} else {
if let Ok(prop_schema) = prop_schema_ref.resolve(self.graph.spec()) {
self.schema_to_type_ref(&prop_schema)?
} else {
TypeRef::new("serde_json::Value")
}
}
} else {
if let Ok(prop_schema) = prop_schema_ref.resolve(self.graph.spec()) {
self.schema_to_type_ref(&prop_schema)?
} else {
TypeRef::new("serde_json::Value")
}
};
let is_required = schema.required.contains(prop_name);
let optional = !is_required;
let mut serde_attrs = vec![];
let rust_field_name = to_rust_field_name(prop_name);
if rust_field_name != *prop_name {
serde_attrs.push(format!("rename = \"{}\"", prop_name));
}
if optional || rust_type.nullable {
serde_attrs.push("skip_serializing_if = \"Option::is_none\"".to_string());
}
let (
docs,
validation_attrs,
regex_validation,
default_value,
read_only,
write_only,
deprecated,
multiple_of,
unique_items,
) = if let Ok(prop_schema) = prop_schema_ref.resolve(self.graph.spec()) {
let docs = prop_schema
.description
.as_ref()
.map(|d| doc_comment_lines(d))
.unwrap_or_default();
let validation = self.extract_validation_attrs(prop_name, is_required, &prop_schema);
let regex_validation = self.extract_validation_pattern(prop_name, &prop_schema);
let default = self.extract_default_value(&prop_schema);
let read_only = prop_schema.read_only.unwrap_or(false);
let write_only = prop_schema.write_only.unwrap_or(false);
let deprecated = prop_schema.deprecated.unwrap_or(false);
let multiple_of = prop_schema.multiple_of.clone();
let unique_items = prop_schema.unique_items.unwrap_or(false);
(
docs,
validation,
regex_validation.cloned(),
default,
read_only,
write_only,
deprecated,
multiple_of,
unique_items,
)
} else {
(vec![], vec![], None, None, false, false, false, None, false)
};
let regex_validation = match rust_type.base_type.as_str() {
"chrono::DateTime<chrono::Utc>" | "chrono::NaiveDate" | "chrono::NaiveTime" | "uuid::Uuid" => None,
_ => regex_validation,
};
let is_nullable = rust_type.nullable;
let final_type = if optional && !is_nullable {
rust_type.with_option()
} else {
rust_type
};
fields.push(FieldDef {
name: to_rust_field_name(prop_name),
docs,
rust_type: final_type,
optional: optional || is_nullable,
serde_attrs,
validation_attrs,
regex_validation,
default_value,
read_only,
write_only,
deprecated,
multiple_of,
unique_items,
});
}
Ok(fields)
}
fn convert_fields(&self, schema: &ObjectSchema) -> anyhow::Result<Vec<FieldDef>> {
self.convert_fields_with_exclusions(schema, None)
}
fn convert_fields_with_inline_types(
&self,
parent_name: &str,
schema: &ObjectSchema,
) -> anyhow::Result<(Vec<FieldDef>, Vec<RustType>)> {
let mut fields = Vec::new();
let mut inline_types = Vec::new();
let mut properties: Vec<_> = schema.properties.iter().collect();
properties.sort_by(|(a, _), (b, _)| a.cmp(b));
for (prop_name, prop_schema_ref) in properties {
let rust_type = if let ObjectOrReference::Ref { ref_path, .. } = prop_schema_ref {
if let Some(ref_name) = SchemaGraph::extract_ref_name(ref_path) {
TypeRef::new(to_rust_type_name(&ref_name))
} else {
if let Ok(prop_schema) = prop_schema_ref.resolve(self.graph.spec()) {
self.schema_to_type_ref(&prop_schema)?
} else {
TypeRef::new("serde_json::Value")
}
}
} else {
if let Ok(prop_schema) = prop_schema_ref.resolve(self.graph.spec()) {
let has_null = prop_schema.any_of.iter().any(|v| {
if let Ok(resolved) = v.resolve(self.graph.spec()) {
resolved.schema_type == Some(SchemaTypeSet::Single(SchemaType::Null))
} else {
false
}
});
if !prop_schema.any_of.is_empty() && has_null && prop_schema.any_of.len() == 2 {
let mut found_type = None;
for variant_ref in &prop_schema.any_of {
if let ObjectOrReference::Ref { ref_path, .. } = variant_ref {
if let Some(ref_name) = SchemaGraph::extract_ref_name(ref_path) {
found_type = Some(TypeRef::new(to_rust_type_name(&ref_name)).with_option());
break;
}
} else if let Ok(resolved) = variant_ref.resolve(self.graph.spec())
&& resolved.schema_type != Some(SchemaTypeSet::Single(SchemaType::Null))
{
found_type = Some(self.schema_to_type_ref(&resolved)?.with_option());
break;
}
}
found_type.unwrap_or_else(|| self.schema_to_type_ref(&prop_schema).unwrap().with_option())
} else if !prop_schema.any_of.is_empty()
&& (prop_schema.title.is_none()
|| prop_schema
.title
.as_ref()
.map(|t| self.graph.get_schema(t).is_none())
.unwrap_or(true))
{
let enum_name = format!("{}.{}", parent_name, prop_name);
let enum_types = self.convert_any_of_enum(&enum_name, &prop_schema)?;
let type_name = if let Some(RustType::Enum(enum_def)) = enum_types.last() {
enum_def.name.clone()
} else {
to_rust_type_name(&enum_name)
};
inline_types.extend(enum_types);
TypeRef::new(&type_name)
} else {
self.schema_to_type_ref(&prop_schema)?
}
} else {
TypeRef::new("serde_json::Value")
}
};
let is_required = schema.required.contains(prop_name);
let optional = !is_required;
let mut serde_attrs = vec![];
let rust_field_name = to_rust_field_name(prop_name);
if rust_field_name != *prop_name {
serde_attrs.push(format!("rename = \"{}\"", prop_name));
}
if optional || rust_type.nullable {
serde_attrs.push("skip_serializing_if = \"Option::is_none\"".to_string());
}
let (
docs,
validation_attrs,
regex_validation,
default_value,
read_only,
write_only,
deprecated,
multiple_of,
unique_items,
) = if let Ok(prop_schema) = prop_schema_ref.resolve(self.graph.spec()) {
let docs = prop_schema
.description
.as_ref()
.map(|d| doc_comment_lines(d))
.unwrap_or_default();
let required = schema.required.contains(prop_name);
let validation = self.extract_validation_attrs(prop_name, required, &prop_schema);
let regex_validation = self.extract_validation_pattern(prop_name, &prop_schema);
let default = self.extract_default_value(&prop_schema);
let read_only = prop_schema.read_only.unwrap_or(false);
let write_only = prop_schema.write_only.unwrap_or(false);
let deprecated = prop_schema.deprecated.unwrap_or(false);
let multiple_of = prop_schema.multiple_of.clone();
let unique_items = prop_schema.unique_items.unwrap_or(false);
(
docs,
validation,
regex_validation.cloned(),
default,
read_only,
write_only,
deprecated,
multiple_of,
unique_items,
)
} else {
(vec![], vec![], None, None, false, false, false, None, false)
};
let regex_validation = match rust_type.base_type.as_str() {
"chrono::DateTime<chrono::Utc>" | "chrono::NaiveDate" | "chrono::NaiveTime" | "uuid::Uuid" => None,
_ => regex_validation,
};
let is_nullable = rust_type.nullable;
let final_type = if optional && !is_nullable {
rust_type.with_option()
} else {
rust_type
};
fields.push(FieldDef {
name: to_rust_field_name(prop_name),
docs,
rust_type: final_type,
optional: optional || is_nullable,
serde_attrs,
validation_attrs,
regex_validation,
default_value,
read_only,
write_only,
deprecated,
multiple_of,
unique_items,
});
}
Ok((fields, inline_types))
}
pub fn schema_to_type_ref(&self, schema: &ObjectSchema) -> anyhow::Result<TypeRef> {
if let Some(ref schema_type) = schema.schema_type {
if !matches!(schema_type, SchemaTypeSet::Single(SchemaType::Object)) {
} else if let Some(ref title) = schema.title
&& self.graph.get_schema(title).is_some()
&& !schema.properties.is_empty()
{
let is_cyclic = self.graph.is_cyclic(title);
let mut type_ref = TypeRef::new(to_rust_type_name(title));
if is_cyclic {
type_ref = type_ref.with_boxed();
}
return Ok(type_ref);
}
} else if let Some(ref title) = schema.title
&& self.graph.get_schema(title).is_some()
&& !schema.properties.is_empty()
{
let is_cyclic = self.graph.is_cyclic(title);
let mut type_ref = TypeRef::new(to_rust_type_name(title));
if is_cyclic {
type_ref = type_ref.with_boxed();
}
return Ok(type_ref);
}
if !schema.one_of.is_empty() || !schema.any_of.is_empty() {
let variants = if !schema.one_of.is_empty() {
&schema.one_of
} else {
&schema.any_of
};
let has_null = variants.iter().any(|v| {
if let Ok(resolved) = v.resolve(self.graph.spec()) {
resolved.schema_type == Some(SchemaTypeSet::Single(SchemaType::Null))
} else {
false
}
});
if has_null && variants.len() == 2 {
for variant_ref in variants {
if let ObjectOrReference::Ref { ref_path, .. } = variant_ref
&& let Some(ref_name) = SchemaGraph::extract_ref_name(ref_path)
{
return Ok(TypeRef::new(to_rust_type_name(&ref_name)).with_option());
}
if let Ok(resolved) = variant_ref.resolve(self.graph.spec()) {
if resolved.schema_type == Some(SchemaTypeSet::Single(SchemaType::Null)) {
continue;
}
let inner_type = self.schema_to_type_ref(&resolved)?;
return Ok(inner_type.with_option());
}
}
}
let mut fallback_type: Option<TypeRef> = None;
for variant_ref in variants {
if let ObjectOrReference::Ref { ref_path, .. } = variant_ref
&& let Some(ref_name) = SchemaGraph::extract_ref_name(ref_path)
{
return Ok(TypeRef::new(to_rust_type_name(&ref_name)));
}
if let Ok(resolved) = variant_ref.resolve(self.graph.spec()) {
if resolved.schema_type == Some(SchemaTypeSet::Single(SchemaType::Null)) {
continue;
}
if resolved.schema_type == Some(SchemaTypeSet::Single(SchemaType::Array)) {
let unique_items = resolved.unique_items.unwrap_or(false);
if let Some(ref items_box) = resolved.items
&& let Schema::Object(items_ref) = items_box.as_ref()
&& let Ok(items_schema) = items_ref.resolve(self.graph.spec())
{
if !items_schema.one_of.is_empty() {
for one_of_ref in &items_schema.one_of {
if let ObjectOrReference::Ref { ref_path, .. } = one_of_ref
&& let Some(ref_name) = SchemaGraph::extract_ref_name(ref_path)
{
return Ok(
TypeRef::new(to_rust_type_name(&ref_name))
.with_vec()
.with_unique_items(unique_items),
);
}
}
}
}
}
if resolved.schema_type == Some(SchemaTypeSet::Single(SchemaType::String)) && fallback_type.is_none() {
fallback_type = Some(TypeRef::new("String"));
continue;
}
if !resolved.one_of.is_empty() {
for nested_ref in &resolved.one_of {
if let ObjectOrReference::Ref { ref_path, .. } = nested_ref
&& let Some(ref_name) = SchemaGraph::extract_ref_name(ref_path)
{
return Ok(TypeRef::new(to_rust_type_name(&ref_name)));
}
}
}
if let Some(ref variant_title) = resolved.title
&& self.graph.get_schema(variant_title).is_some()
{
return Ok(TypeRef::new(to_rust_type_name(variant_title)));
}
}
}
if let Some(t) = fallback_type {
return Ok(t);
}
}
if let Some(ref schema_type) = schema.schema_type {
match schema_type {
SchemaTypeSet::Single(typ) => {
let base_type = match typ {
SchemaType::String => {
if let Some(ref format) = schema.format {
match format.as_str() {
"date" => "chrono::NaiveDate",
"date-time" => "chrono::DateTime<chrono::Utc>",
"time" => "chrono::NaiveTime",
"binary" => "Vec<u8>", "byte" => "String", "uuid" => "uuid::Uuid", _ => "String",
}
} else {
"String"
}
}
SchemaType::Number => "f64",
SchemaType::Integer => "i64",
SchemaType::Boolean => "bool",
SchemaType::Array => {
let unique_items = schema.unique_items.unwrap_or(false);
if let Some(ref items_box) = schema.items
&& let Schema::Object(items_ref) = items_box.as_ref()
{
if let ObjectOrReference::Ref { ref_path, .. } = items_ref.as_ref() {
if let Some(ref_name) = SchemaGraph::extract_ref_name(ref_path) {
return Ok(
TypeRef::new(to_rust_type_name(&ref_name))
.with_vec()
.with_unique_items(unique_items),
);
}
}
if let Ok(items_schema) = items_ref.resolve(self.graph.spec()) {
if !items_schema.one_of.is_empty() {
for one_of_ref in &items_schema.one_of {
if let ObjectOrReference::Ref { ref_path, .. } = one_of_ref
&& let Some(ref_name) = SchemaGraph::extract_ref_name(ref_path)
{
return Ok(
TypeRef::new(to_rust_type_name(&ref_name))
.with_vec()
.with_unique_items(unique_items),
);
}
}
}
let item_type = self.schema_to_type_ref(&items_schema)?;
return Ok(
TypeRef::new(item_type.to_rust_type())
.with_vec()
.with_unique_items(unique_items),
);
}
}
return Ok(
TypeRef::new("serde_json::Value")
.with_vec()
.with_unique_items(unique_items),
);
}
SchemaType::Object => {
return Ok(TypeRef::new("serde_json::Value"));
}
SchemaType::Null => {
return Ok(TypeRef::new("()").with_option());
}
};
return Ok(TypeRef::new(base_type));
}
SchemaTypeSet::Multiple(_) => {
return Ok(TypeRef::new("serde_json::Value"));
}
}
}
Ok(TypeRef::new("serde_json::Value"))
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use oas3::spec::{Discriminator, Spec};
use serde_json::json;
use super::*;
fn create_test_spec(schemas: BTreeMap<String, ObjectSchema>) -> Spec {
let mut spec_json = json!({
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"paths": {},
"components": {
"schemas": {}
}
});
if let Some(components) = spec_json.get_mut("components")
&& let Some(schemas_obj) = components.get_mut("schemas")
{
for (name, schema) in schemas {
let schema_json = serde_json::to_value(schema).unwrap();
schemas_obj[name] = schema_json;
}
}
serde_json::from_value(spec_json).unwrap()
}
#[test]
fn test_discriminated_union_uses_struct_variants() {
let mut one_of_schema = ObjectSchema::default();
let mut object_variant = ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
title: Some("ObjectVariant".to_string()),
..Default::default()
};
object_variant.properties.insert(
"type".to_string(),
ObjectOrReference::Object(ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
const_value: Some(json!("object")),
..Default::default()
}),
);
object_variant.properties.insert(
"name".to_string(),
ObjectOrReference::Object(ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
..Default::default()
}),
);
object_variant.required.push("type".to_string());
object_variant.required.push("name".to_string());
let string_variant = ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
title: Some("StringVariant".to_string()),
..Default::default()
};
let integer_variant = ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
title: Some("IntegerVariant".to_string()),
..Default::default()
};
one_of_schema.one_of.push(ObjectOrReference::Object(object_variant));
one_of_schema.one_of.push(ObjectOrReference::Object(string_variant));
one_of_schema.one_of.push(ObjectOrReference::Object(integer_variant));
one_of_schema.discriminator = Some(Discriminator {
property_name: "type".to_string(),
mapping: Some(BTreeMap::new()),
});
let mut schemas = BTreeMap::new();
schemas.insert("TestUnion".to_string(), one_of_schema);
let spec = create_test_spec(schemas);
let graph = SchemaGraph::new(spec).unwrap();
let converter = SchemaConverter::new(&graph);
let result = converter
.convert_schema("TestUnion", graph.get_schema("TestUnion").unwrap())
.unwrap();
assert_eq!(result.len(), 1, "Should generate exactly one type");
if let RustType::Enum(enum_def) = &result[0] {
assert_eq!(enum_def.name, "TestUnion");
assert!(enum_def.discriminator.is_some(), "Should have discriminator");
for variant in &enum_def.variants {
match &variant.content {
VariantContent::Struct(fields) => {
if variant.name == "StringVariant" || variant.name == "IntegerVariant" {
assert_eq!(
fields.len(),
1,
"Primitive variant {} should have exactly one field",
variant.name
);
assert_eq!(
fields[0].name, "value",
"Primitive variant field should be named 'value'"
);
}
}
VariantContent::Tuple(_) => {
panic!(
"Discriminated union variant {} must be a struct, not tuple",
variant.name
);
}
VariantContent::Unit => {
panic!(
"Discriminated union variant {} must be a struct, not unit",
variant.name
);
}
}
}
assert_eq!(
enum_def.serde_attrs.len(),
0,
"serde_attrs should be empty (discriminator is separate)"
);
} else {
panic!("Expected enum, got {:?}", result[0]);
}
}
#[test]
fn test_catch_all_enum_generates_two_level_structure() {
let mut any_of_schema = ObjectSchema::default();
any_of_schema.any_of.push(ObjectOrReference::Object(ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
const_value: Some(json!("known-value-1")),
description: Some("First known value".to_string()),
..Default::default()
}));
any_of_schema.any_of.push(ObjectOrReference::Object(ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
const_value: Some(json!("known-value-2")),
description: Some("Second known value".to_string()),
..Default::default()
}));
any_of_schema.any_of.push(ObjectOrReference::Object(ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
description: Some("Any other string".to_string()),
..Default::default()
}));
let mut schemas = BTreeMap::new();
schemas.insert("CatchAllEnum".to_string(), any_of_schema);
let spec = create_test_spec(schemas);
let graph = SchemaGraph::new(spec).unwrap();
let converter = SchemaConverter::new(&graph);
let result = converter
.convert_schema("CatchAllEnum", graph.get_schema("CatchAllEnum").unwrap())
.unwrap();
assert_eq!(
result.len(),
2,
"Should generate TWO types (inner Known enum + outer untagged wrapper)"
);
if let RustType::Enum(inner_enum) = &result[0] {
assert_eq!(inner_enum.name, "CatchAllEnumKnown");
assert_eq!(inner_enum.variants.len(), 2, "Should have 2 known variants");
assert!(inner_enum.serde_attrs.is_empty(), "Inner enum should not be untagged");
for variant in &inner_enum.variants {
assert!(
matches!(variant.content, VariantContent::Unit),
"Known enum variants should be unit variants"
);
assert!(
variant.serde_attrs.iter().any(|a| a.starts_with("rename")),
"Known enum variants should have rename attribute"
);
}
} else {
panic!("First type should be inner Known enum, got {:?}", result[0]);
}
if let RustType::Enum(outer_enum) = &result[1] {
assert_eq!(outer_enum.name, "CatchAllEnum");
assert_eq!(outer_enum.variants.len(), 2, "Should have 2 variants (Known + Other)");
assert!(
outer_enum.serde_attrs.contains(&"untagged".to_string()),
"Outer enum should be untagged"
);
let known_variant = outer_enum.variants.iter().find(|v| v.name == "Known").unwrap();
match &known_variant.content {
VariantContent::Tuple(types) => {
assert_eq!(types.len(), 1);
assert_eq!(types[0].base_type, "CatchAllEnumKnown");
}
_ => panic!("Known variant should be a tuple variant"),
}
let other_variant = outer_enum.variants.iter().find(|v| v.name == "Other").unwrap();
match &other_variant.content {
VariantContent::Tuple(types) => {
assert_eq!(types.len(), 1);
assert_eq!(types[0].base_type, "String");
}
_ => panic!("Other variant should be a tuple variant"),
}
} else {
panic!("Second type should be outer wrapper enum, got {:?}", result[1]);
}
}
#[test]
fn test_simple_string_enum() {
let enum_schema = ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
enum_values: vec![json!("value1"), json!("value2"), json!("value3")],
..Default::default()
};
let mut schemas = BTreeMap::new();
schemas.insert("SimpleEnum".to_string(), enum_schema);
let spec = create_test_spec(schemas);
let graph = SchemaGraph::new(spec).unwrap();
let converter = SchemaConverter::new(&graph);
let result = converter
.convert_schema("SimpleEnum", graph.get_schema("SimpleEnum").unwrap())
.unwrap();
assert_eq!(result.len(), 1, "Should generate exactly one enum");
if let RustType::Enum(enum_def) = &result[0] {
assert_eq!(enum_def.name, "SimpleEnum");
assert_eq!(enum_def.variants.len(), 3);
assert!(enum_def.discriminator.is_none());
assert!(enum_def.serde_attrs.is_empty(), "Simple enum should not be untagged");
for variant in &enum_def.variants {
assert!(matches!(variant.content, VariantContent::Unit));
assert!(variant.serde_attrs.iter().any(|a| a.starts_with("rename")));
}
} else {
panic!("Expected enum, got {:?}", result[0]);
}
}
#[test]
fn test_nullable_pattern_detection() {
let mut any_of_schema = ObjectSchema::default();
any_of_schema.any_of.push(ObjectOrReference::Object(ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
..Default::default()
}));
any_of_schema.any_of.push(ObjectOrReference::Object(ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::Null)),
..Default::default()
}));
let mut schemas = BTreeMap::new();
schemas.insert("NullableString".to_string(), any_of_schema);
let spec = create_test_spec(schemas);
let graph = SchemaGraph::new(spec).unwrap();
let converter = SchemaConverter::new(&graph);
let type_ref = converter
.schema_to_type_ref(graph.get_schema("NullableString").unwrap())
.unwrap();
assert_eq!(type_ref.base_type, "String");
assert!(
type_ref.nullable,
"Should detect nullable pattern and set nullable=true"
);
assert_eq!(type_ref.to_rust_type(), "Option<String>");
}
#[test]
fn test_untagged_any_of_enum() {
let mut any_of_schema = ObjectSchema::default();
any_of_schema.any_of.push(ObjectOrReference::Object(ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
title: Some("StringVariant".to_string()),
..Default::default()
}));
any_of_schema.any_of.push(ObjectOrReference::Object(ObjectSchema {
schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
title: Some("IntegerVariant".to_string()),
..Default::default()
}));
let mut schemas = BTreeMap::new();
schemas.insert("UntaggedUnion".to_string(), any_of_schema);
let spec = create_test_spec(schemas);
let graph = SchemaGraph::new(spec).unwrap();
let converter = SchemaConverter::new(&graph);
let result = converter
.convert_schema("UntaggedUnion", graph.get_schema("UntaggedUnion").unwrap())
.unwrap();
assert_eq!(result.len(), 1, "Should generate one enum");
if let RustType::Enum(enum_def) = &result[0] {
assert_eq!(enum_def.name, "UntaggedUnion");
assert!(enum_def.discriminator.is_none(), "Should not have discriminator");
assert!(
enum_def.serde_attrs.contains(&"untagged".to_string()),
"Should be untagged"
);
assert_eq!(enum_def.variants.len(), 2);
} else {
panic!("Expected enum, got {:?}", result[0]);
}
}
}