use heck::{ToSnakeCase, ToUpperCamelCase}; use indexmap::{map::Entry, IndexMap}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; use syn::Ident; use crate::openapi::r#type::OpenApiType; use super::{r#enum::Enum, ResolvedSchema, WarningReporter}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PrimitiveType { Bool, I32, I64, String, Float, DateTime, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum PropertyType { Primitive(PrimitiveType), Ref(String), Enum(Enum), Nested(Box), Array(Box), Any, } impl PropertyType { pub fn codegen( &self, namespace: &mut ObjectNamespace, resolved: &ResolvedSchema, ) -> Option { match self { Self::Primitive(PrimitiveType::Bool) => Some(format_ident!("bool").into_token_stream()), Self::Primitive(PrimitiveType::I32) => Some(format_ident!("i32").into_token_stream()), Self::Primitive(PrimitiveType::I64) => Some(format_ident!("i64").into_token_stream()), Self::Primitive(PrimitiveType::String) => { Some(format_ident!("String").into_token_stream()) } Self::Primitive(PrimitiveType::DateTime) => { Some(quote! { chrono::DateTime }) } Self::Primitive(PrimitiveType::Float) => Some(format_ident!("f64").into_token_stream()), Self::Ref(path) => { let name = path.strip_prefix("#/components/schemas/")?; let name = format_ident!("{name}"); Some(quote! { crate::models::#name }) } Self::Enum(r#enum) => { let code = r#enum.codegen(resolved)?; namespace.push_element(code); let ns = namespace.get_ident(); let name = format_ident!("{}", r#enum.name); Some(quote! { #ns::#name }) } Self::Array(array) => { let inner_ty = array.codegen(namespace, resolved)?; Some(quote! { Vec<#inner_ty> }) } Self::Nested(nested) => { let code = nested.codegen(resolved)?; namespace.push_element(code); let ns = namespace.get_ident(); let name = format_ident!("{}", nested.name); Some(quote! { #ns::#name }) } Self::Any => Some(quote! { serde_json::Value }), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Property { pub field_name: String, pub name: String, pub description: Option, pub required: bool, pub nullable: bool, pub r#type: PropertyType, pub deprecated: bool, } impl Property { pub fn from_schema( name: &str, required: bool, schema: &OpenApiType, schemas: &IndexMap<&str, OpenApiType>, warnings: WarningReporter, ) -> Option { let name = name.to_owned(); let field_name = name.to_snake_case(); let description = schema.description.as_deref().map(ToOwned::to_owned); match schema { OpenApiType { r#enum: Some(_), .. } => { let Some(r#enum) = Enum::from_schema( &name.clone().to_upper_camel_case(), schema, warnings.clone(), ) else { warnings.push("Failed to create enum"); return None; }; Some(Self { r#type: PropertyType::Enum(r#enum), name, field_name, description, required, deprecated: schema.deprecated, nullable: false, }) } OpenApiType { one_of: Some(types), .. } => match types.as_slice() { [left, OpenApiType { r#type: Some("null"), .. }] => { let mut inner = Self::from_schema(&name, required, left, schemas, warnings)?; inner.nullable = true; Some(inner) } [left @ .., OpenApiType { r#type: Some("null"), .. }] => { let rest = OpenApiType { one_of: Some(left.to_owned()), ..schema.clone() }; let mut inner = Self::from_schema(&name, required, &rest, schemas, warnings)?; inner.nullable = true; Some(inner) } cases => { let Some(r#enum) = Enum::from_one_of(&name.to_upper_camel_case(), cases, warnings.clone()) else { warnings.push("Failed to create oneOf enum"); return None; }; Some(Self { name, field_name, description, required, nullable: false, deprecated: schema.deprecated, r#type: PropertyType::Enum(r#enum), }) } }, OpenApiType { all_of: Some(types), .. } => { let obj_name = name.to_upper_camel_case(); let composite = Object::from_all_of(&obj_name, types, schemas, warnings.child(&obj_name)); Some(Self { name, field_name, description, required, nullable: false, deprecated: schema.deprecated, r#type: PropertyType::Nested(Box::new(composite)), }) } OpenApiType { r#type: Some("object"), properties: None, .. } => Some(Self { field_name, name, description, required, nullable: false, r#type: PropertyType::Any, deprecated: schema.deprecated, }), OpenApiType { r#type: Some("object"), .. } => { let obj_name = name.to_upper_camel_case(); Some(Self { r#type: PropertyType::Nested(Box::new(Object::from_schema_object( &obj_name, schema, schemas, warnings.child(&obj_name), ))), name, field_name, description, required, deprecated: schema.deprecated, nullable: false, }) } OpenApiType { ref_path: Some(path), .. } => Some(Self { name, field_name, description, r#type: PropertyType::Ref((*path).to_owned()), required, deprecated: schema.deprecated, nullable: false, }), OpenApiType { r#type: Some("array"), items: Some(items), .. } => { let inner = Self::from_schema(&name, required, items, schemas, warnings)?; Some(Self { name, field_name, description, required, nullable: false, deprecated: schema.deprecated, r#type: PropertyType::Array(Box::new(inner.r#type)), }) } OpenApiType { r#type: Some(_), .. } => { let prim = match (schema.r#type, schema.format) { (Some("integer"), Some("int32")) => PrimitiveType::I32, (Some("integer"), Some("int64")) => PrimitiveType::I64, (Some("number"), /* Some("float") */ _) | (_, Some("float")) => { PrimitiveType::Float } (Some("string"), None) => PrimitiveType::String, (Some("string"), Some("date")) => PrimitiveType::DateTime, (Some("boolean"), None) => PrimitiveType::Bool, _ => return None, }; Some(Self { name, field_name, description, required, nullable: false, deprecated: schema.deprecated, r#type: PropertyType::Primitive(prim), }) } _ => { warnings.push("Could not resolve property type"); None } } } pub fn codegen( &self, namespace: &mut ObjectNamespace, resolved: &ResolvedSchema, ) -> Option { let desc = self.description.as_ref().map(|d| quote! { #[doc = #d]}); let name = &self.name; let (name, serde_attr) = match name.as_str() { // https://doc.rust-lang.org/reference/keywords.html#r-lex.keywords "as" => (format_ident!("r#as"), None), "break" => (format_ident!("r#break"), None), "const" => (format_ident!("r#const"), None), "continue" => (format_ident!("r#continue"), None), "crate" => (format_ident!("r#crate"), None), "else" => (format_ident!("r#else"), None), "enum" => (format_ident!("r#enum"), None), "extern" => (format_ident!("r#extern"), None), "false" => (format_ident!("r#false"), None), "fn" => (format_ident!("r#fn"), None), "for" => (format_ident!("r#for"), None), "if" => (format_ident!("r#if"), None), "impl" => (format_ident!("r#impl"), None), "in" => (format_ident!("r#in"), None), "let" => (format_ident!("r#let"), None), "loop" => (format_ident!("r#loop"), None), "match" => (format_ident!("r#match"), None), "mod" => (format_ident!("r#mod"), None), "move" => (format_ident!("r#move"), None), "mut" => (format_ident!("r#mut"), None), "pub" => (format_ident!("r#pub"), None), "ref" => (format_ident!("r#ref"), None), "return" => (format_ident!("r#return"), None), "self" => (format_ident!("r#self"), None), "Self" => (format_ident!("r#Self"), None), "static" => (format_ident!("r#static"), None), "struct" => (format_ident!("r#struct"), None), "super" => (format_ident!("r#super"), None), "trait" => (format_ident!("r#trait"), None), "true" => (format_ident!("r#true"), None), "type" => (format_ident!("r#type"), None), "unsafe" => (format_ident!("r#unsafe"), None), "use" => (format_ident!("r#use"), None), "where" => (format_ident!("r#where"), None), "while" => (format_ident!("r#while"), None), "async" => (format_ident!("r#async"), None), "await" => (format_ident!("r#await"), None), "dyn" => (format_ident!("r#dyn"), None), "abstract" => (format_ident!("r#abstract"), None), "become" => (format_ident!("r#become"), None), "box" => (format_ident!("r#box"), None), "do" => (format_ident!("r#do"), None), "final" => (format_ident!("r#final"), None), "macro" => (format_ident!("r#macro"), None), "override" => (format_ident!("r#override"), None), "priv" => (format_ident!("r#priv"), None), "typeof" => (format_ident!("r#typeof"), None), "unsized" => (format_ident!("r#unsized"), None), "virtual" => (format_ident!("r#virtual"), None), "yield" => (format_ident!("r#yield"), None), "try" => (format_ident!("r#try"), None), "gen" => (format_ident!("r#gen"), None), name if name != self.field_name => ( format_ident!("{}", self.field_name), Some(quote! { #[serde(rename = #name)]}), ), _ => (format_ident!("{}", self.field_name), None), }; let ty_inner = self.r#type.codegen(namespace, resolved)?; let ty = if !self.required || self.nullable { quote! { Option<#ty_inner> } } else { ty_inner }; let deprecated = self.deprecated.then(|| { let note = self.description.as_ref().map(|d| quote! { note = #d }); quote! { #[deprecated(#note)] } }); Some(quote! { #desc #deprecated #serde_attr pub #name: #ty }) } } #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Object { pub name: String, pub description: Option, pub properties: IndexMap, } impl Object { pub fn from_schema_object( name: &str, schema: &OpenApiType, schemas: &IndexMap<&str, OpenApiType>, warnings: WarningReporter, ) -> Self { let mut result = Object { name: name.to_owned(), description: schema.description.as_deref().map(ToOwned::to_owned), ..Default::default() }; let Some(props) = &schema.properties else { warnings.push("Missing properties"); return result; }; let required = schema.required.clone().unwrap_or_default(); for (prop_name, prop) in props { let Some(prop) = Property::from_schema( prop_name, required.contains(prop_name), prop, schemas, warnings.child(prop_name), ) else { continue; }; let field_name = prop.field_name.clone(); let entry = result.properties.entry(field_name.clone()); if let Entry::Occupied(mut entry) = entry { let other_name = entry.get().name.clone(); warnings.push(format!( "Property name collision: {other_name} and {field_name}" )); // deprioritise kebab and camelcase if other_name.contains('-') || other_name .chars() .filter(|c| c.is_alphabetic()) .any(|c| c.is_ascii_uppercase()) { entry.insert(prop); } } else { entry.insert_entry(prop); } } result } pub fn from_all_of( name: &str, types: &[OpenApiType], schemas: &IndexMap<&str, OpenApiType>, warnings: WarningReporter, ) -> Self { let mut result = Self { name: name.to_owned(), ..Default::default() }; for r#type in types { let r#type = if let OpenApiType { ref_path: Some(ref_path), .. } = r#type { let Some(name) = ref_path.strip_prefix("#/components/schemas/") else { warnings.push(format!("Malformed ref {ref_path}")); continue; }; let Some(schema) = schemas.get(name) else { warnings.push(format!("Missing schema for ref {name}")); continue; }; schema } else { r#type }; let obj = Self::from_schema_object(name, r#type, schemas, warnings.child("variant")); result.description = result.description.or(obj.description); result.properties.extend(obj.properties); } result } pub fn codegen(&self, resolved: &ResolvedSchema) -> Option { let doc = self.description.as_ref().map(|d| { quote! { #[doc = #d] } }); let mut namespace = ObjectNamespace { object: self, ident: None, elements: Vec::default(), }; let mut props = Vec::with_capacity(self.properties.len()); for (_, prop) in &self.properties { props.push(prop.codegen(&mut namespace, resolved)?); } let name = format_ident!("{}", self.name); let ns = namespace.codegen(); Some(quote! { #ns #doc #[derive(Debug, Clone, PartialEq, serde::Deserialize)] pub struct #name { #(#props),* } }) } } pub struct ObjectNamespace<'o> { object: &'o Object, ident: Option, elements: Vec, } impl ObjectNamespace<'_> { pub fn get_ident(&mut self) -> Ident { self.ident .get_or_insert_with(|| { let name = self.object.name.to_snake_case(); format_ident!("{name}") }) .clone() } pub fn push_element(&mut self, el: TokenStream) { self.elements.push(el); } pub fn codegen(mut self) -> Option { if self.elements.is_empty() { None } else { let ident = self.get_ident(); let elements = self.elements; Some(quote! { pub mod #ident { #(#elements)* } }) } } } #[cfg(test)] mod test { use super::*; use crate::openapi::schema::test::get_schema; #[test] fn resolve_objects() { let schema = get_schema(); let mut objects = 0; let mut unresolved = vec![]; for (name, desc) in &schema.components.schemas { if desc.r#type == Some("object") { objects += 1; let reporter = WarningReporter::new(); Object::from_schema_object( name, desc, &schema.components.schemas, reporter.clone(), ); if !reporter.is_empty() { unresolved.push(name); } } } if !unresolved.is_empty() { panic!( "Failed to resolve {}/{} objects. Could not resolve [{}]", unresolved.len(), objects, unresolved .into_iter() .map(|u| format!("`{u}`")) .collect::>() .join(", ") ) } } }