use heck::{ToPascalCase, ToSnakeCase};
use openapiv3::{Components, OpenAPI, ReferenceOr, Schema, SchemaKind, Type};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use super::types::{is_string_enum, ref_to_ident, schema_to_rust_type, string_enum_values};
#[must_use]
pub fn generate_schemas(openapi: &OpenAPI) -> TokenStream {
let Some(components) = &openapi.components else {
return quote! {};
};
let items: Vec<TokenStream> = components
.schemas
.iter()
.map(|(name, ref_or)| generate_schema_item(name, ref_or, components))
.collect();
quote! { #(#items)* }
}
fn generate_schema_item(
name: &str,
ref_or: &ReferenceOr<Schema>,
components: &Components,
) -> TokenStream {
let schema = match ref_or {
ReferenceOr::Item(s) => s,
ReferenceOr::Reference { reference } => {
let ident = format_ident!("{}", name);
let target = ref_to_ident(reference);
return quote! { pub type #ident = #target; };
}
};
if is_string_enum(schema) {
generate_string_enum(name, schema)
} else if let SchemaKind::Type(Type::Object(obj)) = &schema.schema_kind {
generate_object_struct(name, schema, obj, components)
} else {
let ident = format_ident!("{}", name);
let inner = schema_to_rust_type(ref_or, true);
let doc = doc_attr(&schema.schema_data.description);
quote! {
#doc
pub type #ident = #inner;
}
}
}
fn generate_string_enum(name: &str, schema: &Schema) -> TokenStream {
let ident = format_ident!("{}", name);
let doc = doc_attr(&schema.schema_data.description);
let variants = string_enum_values(schema)
.into_iter()
.map(|v| {
let variant_ident = format_ident!("{}", v.to_pascal_case());
if variant_ident == v {
quote! { #variant_ident }
} else {
let rename = &v;
quote! {
#[serde(rename = #rename)]
#variant_ident
}
}
})
.collect::<Vec<_>>();
quote! {
#doc
#[derive(
::core::fmt::Debug,
::core::clone::Clone,
::serde::Serialize,
::serde::Deserialize,
)]
pub enum #ident {
#(#variants,)*
}
}
}
fn generate_object_struct(
name: &str,
schema: &Schema,
obj: &openapiv3::ObjectType,
_components: &Components,
) -> TokenStream {
let ident = format_ident!("{}", name);
let doc = doc_attr(&schema.schema_data.description);
let fields: Vec<TokenStream> = obj
.properties
.iter()
.map(|(prop_name, prop_ref_or)| {
let is_required = obj.required.iter().any(|r| r == prop_name);
let snake = prop_name.to_snake_case();
let field_ident = keyword_safe_ident(&snake);
let rename_attr = {
let n = prop_name.as_str();
quote! { #[serde(rename = #n)] }
};
let field_doc = match prop_ref_or {
ReferenceOr::Item(s) => doc_attr(&s.schema_data.description),
ReferenceOr::Reference { .. } => quote! {},
};
let field_type = schema_to_rust_type(&prop_ref_or.clone().unbox(), is_required);
quote! {
#field_doc
#rename_attr
pub #field_ident: #field_type,
}
})
.collect();
quote! {
#doc
#[derive(
::core::fmt::Debug,
::core::clone::Clone,
::serde::Serialize,
::serde::Deserialize,
)]
pub struct #ident {
#(#fields)*
}
}
}
fn keyword_safe_ident(name: &str) -> proc_macro2::Ident {
const KEYWORDS: &[&str] = &[
"as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
"extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
"mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
"true", "type", "union", "unsafe", "use", "where", "while", "yield",
];
if KEYWORDS.contains(&name) {
proc_macro2::Ident::new_raw(name, proc_macro2::Span::call_site())
} else {
format_ident!("{}", name)
}
}
#[must_use]
pub fn doc_attr(description: &Option<String>) -> TokenStream {
description
.as_ref()
.map_or_else(|| quote! {}, |d| quote! { #[doc = #d] })
}