use proc_macro2::TokenStream;
use quote::quote;
use crate::entity::parse::EntityDef;
pub fn generate_all_schema_types(entity: &EntityDef) -> TokenStream {
let entity_name_str = entity.name_str();
let mut types: Vec<TokenStream> = Vec::new();
let handlers = entity.api_config().handlers();
if handlers.any() {
let response = entity.ident_with("", "Response");
types.push(quote! { #response });
if handlers.create {
let create = entity.ident_with("Create", "Request");
types.push(quote! { #create });
}
if handlers.update {
let update = entity.ident_with("Update", "Request");
types.push(quote! { #update });
}
}
for cmd in entity.command_defs() {
let cmd_struct = cmd.struct_name(&entity_name_str);
types.push(quote! { #cmd_struct });
}
quote! { #(#types),* }
}
pub fn generate_common_schemas_code() -> TokenStream {
quote! {
if let Some(components) = openapi.components.as_mut() {
let error_schema = schema::ObjectBuilder::new()
.schema_type(schema::Type::Object)
.title(Some("ErrorResponse"))
.description(Some("Error response following RFC 7807 Problem Details"))
.property("type", schema::ObjectBuilder::new()
.schema_type(schema::Type::String)
.description(Some("A URI reference that identifies the problem type"))
.example(Some(serde_json::json!("https://errors.example.com/not-found")))
.build())
.required("type")
.property("title", schema::ObjectBuilder::new()
.schema_type(schema::Type::String)
.description(Some("A short, human-readable summary of the problem"))
.example(Some(serde_json::json!("Resource not found")))
.build())
.required("title")
.property("status", schema::ObjectBuilder::new()
.schema_type(schema::Type::Integer)
.description(Some("HTTP status code"))
.example(Some(serde_json::json!(404)))
.build())
.required("status")
.property("detail", schema::ObjectBuilder::new()
.schema_type(schema::Type::String)
.description(Some("A human-readable explanation specific to this occurrence"))
.example(Some(serde_json::json!("User with ID '123' was not found")))
.build())
.property("code", schema::ObjectBuilder::new()
.schema_type(schema::Type::String)
.description(Some("Application-specific error code"))
.example(Some(serde_json::json!("NOT_FOUND")))
.build())
.build();
components.schemas.insert("ErrorResponse".to_string(), error_schema.into());
let pagination_schema = schema::ObjectBuilder::new()
.schema_type(schema::Type::Object)
.title(Some("PaginationQuery"))
.description(Some("Query parameters for paginated list endpoints"))
.property("limit", schema::ObjectBuilder::new()
.schema_type(schema::Type::Integer)
.description(Some("Maximum number of items to return"))
.default(Some(serde_json::json!(100)))
.minimum(Some(1.0))
.maximum(Some(1000.0))
.build())
.property("offset", schema::ObjectBuilder::new()
.schema_type(schema::Type::Integer)
.description(Some("Number of items to skip for pagination"))
.default(Some(serde_json::json!(0)))
.minimum(Some(0.0))
.build())
.build();
components.schemas.insert("PaginationQuery".to_string(), pagination_schema.into());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entity::parse::EntityDef;
#[test]
fn schema_types_no_handlers() {
let input: syn::DeriveInput = syn::parse_quote! {
#[entity(table = "users", api(tag = "Users"))]
pub struct User {
#[id]
pub id: uuid::Uuid,
}
};
let entity = EntityDef::from_derive_input(&input).unwrap();
let types = generate_all_schema_types(&entity);
assert!(types.is_empty());
}
#[test]
fn schema_types_with_all_handlers() {
let input: syn::DeriveInput = syn::parse_quote! {
#[entity(table = "users", api(tag = "Users", handlers))]
pub struct User {
#[id]
pub id: uuid::Uuid,
#[field(create, update, response)]
pub name: String,
}
};
let entity = EntityDef::from_derive_input(&input).unwrap();
let types = generate_all_schema_types(&entity);
let types_str = types.to_string();
assert!(types_str.contains("UserResponse"));
assert!(types_str.contains("CreateUserRequest"));
assert!(types_str.contains("UpdateUserRequest"));
}
#[test]
fn schema_types_create_only() {
let input: syn::DeriveInput = syn::parse_quote! {
#[entity(table = "users", api(tag = "Users", handlers(create)))]
pub struct User {
#[id]
pub id: uuid::Uuid,
#[field(create, response)]
pub name: String,
}
};
let entity = EntityDef::from_derive_input(&input).unwrap();
let types = generate_all_schema_types(&entity);
let types_str = types.to_string();
assert!(types_str.contains("UserResponse"));
assert!(types_str.contains("CreateUserRequest"));
assert!(!types_str.contains("UpdateUserRequest"));
}
#[test]
fn schema_types_with_commands() {
let input: syn::DeriveInput = syn::parse_quote! {
#[entity(table = "users", commands, api(tag = "Users"))]
#[command(Ban)]
#[command(Activate)]
pub struct User {
#[id]
pub id: uuid::Uuid,
#[field(create, response)]
pub name: String,
}
};
let entity = EntityDef::from_derive_input(&input).unwrap();
let types = generate_all_schema_types(&entity);
let types_str = types.to_string();
assert!(types_str.contains("BanUser"));
assert!(types_str.contains("ActivateUser"));
}
#[test]
fn common_schemas_code_generated() {
let code = generate_common_schemas_code();
let code_str = code.to_string();
assert!(code_str.contains("ErrorResponse"));
assert!(code_str.contains("PaginationQuery"));
assert!(code_str.contains("RFC 7807"));
assert!(code_str.contains("limit"));
assert!(code_str.contains("offset"));
}
}