1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
//! `#[derive(OpenApiSchema)]` — a standalone derive that gives a plain struct a
//! field-accurate `OpenApiSchema` impl (issue #1972).
//!
//! Before this derive, the only automatic `OpenApiSchema` impls came from
//! `#[model]` codegen and the primitive macro impls, so any other handler-arg
//! struct (a `Query<T>` param struct or a non-`#[model]` `Json<T>` body) had to
//! carry a hand-written impl plus an `OpenApiConfig::register_schema` call — or
//! its `OpenAPI` / MCP `inputSchema` degraded to a generic
//! `{"type":"object","title":"X"}` placeholder.
//!
//! This derive mirrors the schema `#[model]` already generates
//! (`crate::model::emit_schema_fn_body`): each field becomes a JSON-schema
//! property and every non-`Option` field is `required`. It additionally submits
//! the schema into the compile-time `DerivedSchemaDescriptor` inventory that the
//! spec/MCP back-fill loops consult, so a referenced struct resolves to its real
//! schema with no manual registration.
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields, parse_macro_input};
pub fn derive_openapi_schema(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// The emitted impl uses the bare type name for both `schema_name()` and the
// inventory descriptor, and the descriptor's `schema` field is a plain
// `fn() -> Value` — none of which can carry generic parameters. Reject
// generics with a clear message rather than emitting an impl that fails to
// compile downstream.
if !input.generics.params.is_empty() {
return syn::Error::new_spanned(
&input.generics,
"#[derive(OpenApiSchema)] does not support generic types",
)
.to_compile_error()
.into();
}
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(named) => &named.named,
_ => {
return syn::Error::new_spanned(
&input,
"#[derive(OpenApiSchema)] is only supported on structs with named fields",
)
.to_compile_error()
.into();
}
},
_ => {
return syn::Error::new_spanned(
&input,
"#[derive(OpenApiSchema)] is only supported on structs",
)
.to_compile_error()
.into();
}
};
// `emit_schema_fn_body` expects `&[&&Field]` (it was written against the
// `Vec<&&Field>` the model macro collects); build that shape here.
let field_refs: Vec<&syn::Field> = fields.iter().collect();
let field_ref_refs: Vec<&&syn::Field> = field_refs.iter().collect();
// Honor a container `#[serde(rename_all = "...")]` on the derived struct so
// the advertised property names + `required` entries match the serialized
// wire names — same helper/precedence `#[model]` and `FormModel` use.
let rename_all_rule = crate::model::serde_rename_all_serialize_rule(&input.attrs);
let schema_body =
crate::model::emit_schema_fn_body(&field_ref_refs, false, rename_all_rule.as_deref());
quote! {
impl ::autumn_web::openapi::OpenApiSchema for #name {
fn schema_name() -> &'static str {
::core::stringify!(#name)
}
fn schema() -> ::autumn_web::reexports::serde_json::Value {
#schema_body
}
}
// Advertise the derived schema by name so the OpenAPI/MCP schema
// back-fill resolves it instead of the generic object placeholder.
::autumn_web::reexports::inventory::submit! {
::autumn_web::openapi::DerivedSchemaDescriptor {
name: ::core::stringify!(#name),
identity: ::autumn_web::openapi::type_name_of::<#name>,
schema: <#name as ::autumn_web::openapi::OpenApiSchema>::schema,
}
}
}
.into()
}