Skip to content

Commit 4db3a86

Browse files
committed
Add schema auto-registration and improve state handling
Introduces the #[rustapi_rs::schema] macro for automatic OpenAPI schema registration of structs and enums, and applies it to UserInfo. Enhances RustApi::state to store state in the router for extractor compatibility, and adds a test to verify state extraction. Also updates OpenApiSpec to register additional error body schemas.
1 parent 3559bab commit 4db3a86

5 files changed

Lines changed: 100 additions & 8 deletions

File tree

crates/rustapi-core/src/app.rs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ impl RustApi {
4747
router: Router::new(),
4848
openapi_spec: rustapi_openapi::OpenApiSpec::new("RustAPI Application", "1.0.0")
4949
.register::<rustapi_openapi::ErrorSchema>()
50+
.register::<rustapi_openapi::ErrorBodySchema>()
5051
.register::<rustapi_openapi::ValidationErrorSchema>()
52+
.register::<rustapi_openapi::ValidationErrorBodySchema>()
5153
.register::<rustapi_openapi::FieldErrorSchema>(),
5254
layers: LayerStack::new(),
5355
body_limit: Some(DEFAULT_BODY_LIMIT), // Default 1MB limit
@@ -201,11 +203,11 @@ impl RustApi {
201203
where
202204
S: Clone + Send + Sync + 'static,
203205
{
204-
// For now, state is handled by the router/handlers directly capturing it
205-
// or through a middleware. The current router (matchit) implementation
206-
// doesn't support state injection directly in the same way axum does.
207-
// This is a placeholder for future state management.
208-
self
206+
// Store state in the router's shared Extensions so `State<T>` extractor can retrieve it.
207+
let state = _state;
208+
let mut app = self;
209+
app.router = app.router.state(state);
210+
app
209211
}
210212

211213
/// Register an OpenAPI schema
@@ -693,6 +695,33 @@ impl Default for RustApi {
693695
}
694696
}
695697

698+
#[cfg(test)]
699+
mod tests {
700+
use super::RustApi;
701+
use crate::extract::{FromRequestParts, State};
702+
use crate::request::Request;
703+
use bytes::Bytes;
704+
use http::Method;
705+
use std::collections::HashMap;
706+
707+
#[test]
708+
fn state_is_available_via_extractor() {
709+
let app = RustApi::new().state(123u32);
710+
let router = app.into_router();
711+
712+
let req = http::Request::builder()
713+
.method(Method::GET)
714+
.uri("/test")
715+
.body(())
716+
.unwrap();
717+
let (parts, _) = req.into_parts();
718+
719+
let request = Request::new(parts, Bytes::new(), router.state_ref(), HashMap::new());
720+
let State(value) = State::<u32>::from_request_parts(&request).unwrap();
721+
assert_eq!(value, 123u32);
722+
}
723+
}
724+
696725
/// Check Basic Auth header against expected credentials
697726
#[cfg(feature = "swagger-ui")]
698727
fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {

crates/rustapi-macros/src/lib.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,66 @@ use syn::{
2121
parse_macro_input, FnArg, GenericArgument, ItemFn, LitStr, PathArguments, ReturnType, Type,
2222
};
2323

24+
/// Auto-register a schema type for zero-config OpenAPI.
25+
///
26+
/// Attach this to a `struct` or `enum` that also derives `Schema` (utoipa::ToSchema).
27+
/// This ensures the type is registered into RustAPI's OpenAPI components even if it is
28+
/// only referenced indirectly (e.g. as a nested field type).
29+
///
30+
/// ```rust,ignore
31+
/// use rustapi_rs::prelude::*;
32+
///
33+
/// #[rustapi_rs::schema]
34+
/// #[derive(Serialize, Schema)]
35+
/// struct UserInfo { /* ... */ }
36+
/// ```
37+
#[proc_macro_attribute]
38+
pub fn schema(_attr: TokenStream, item: TokenStream) -> TokenStream {
39+
let input = parse_macro_input!(item as syn::Item);
40+
41+
let (ident, generics) = match &input {
42+
syn::Item::Struct(s) => (&s.ident, &s.generics),
43+
syn::Item::Enum(e) => (&e.ident, &e.generics),
44+
_ => {
45+
return syn::Error::new_spanned(
46+
&input,
47+
"#[rustapi_rs::schema] can only be used on structs or enums",
48+
)
49+
.to_compile_error()
50+
.into();
51+
}
52+
};
53+
54+
if !generics.params.is_empty() {
55+
return syn::Error::new_spanned(
56+
generics,
57+
"#[rustapi_rs::schema] does not support generic types",
58+
)
59+
.to_compile_error()
60+
.into();
61+
}
62+
63+
let registrar_ident = syn::Ident::new(
64+
&format!("__RUSTAPI_AUTO_SCHEMA_{}", ident),
65+
proc_macro2::Span::call_site(),
66+
);
67+
68+
let expanded = quote! {
69+
#input
70+
71+
#[allow(non_upper_case_globals)]
72+
#[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
73+
#[linkme(crate = ::rustapi_rs::__private::linkme)]
74+
static #registrar_ident: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) =
75+
|spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec| {
76+
spec.register_in_place::<#ident>();
77+
};
78+
};
79+
80+
debug_output("schema", &expanded);
81+
expanded.into()
82+
}
83+
2484
fn extract_schema_types(ty: &Type, out: &mut Vec<Type>, allow_leaf: bool) {
2585
match ty {
2686
Type::Reference(r) => extract_schema_types(&r.elem, out, allow_leaf),

crates/rustapi-openapi/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ mod spec;
3636
mod swagger;
3737

3838
pub use config::OpenApiConfig;
39-
pub use schemas::{ErrorSchema, FieldErrorSchema, ValidationErrorSchema};
39+
pub use schemas::{
40+
ErrorBodySchema, ErrorSchema, FieldErrorSchema, ValidationErrorBodySchema,
41+
ValidationErrorSchema,
42+
};
4043
pub use spec::{
4144
ApiInfo, MediaType, OpenApiSpec, Operation, OperationModifier, Parameter, PathItem,
4245
RequestBody, ResponseModifier, ResponseSpec, SchemaRef,

crates/rustapi-openapi/src/spec.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,7 @@ impl OpenApiSpec {
138138

139139
/// Register a type that implements Schema (utoipa::ToSchema)
140140
pub fn register<T: for<'a> utoipa::ToSchema<'a>>(mut self) -> Self {
141-
let (name, schema) = T::schema(); // returns (Cow<str>, RefOr<Schema>)
142-
// Convert to JSON value
141+
let (name, schema) = T::schema();
143142
if let Ok(json_schema) = serde_json::to_value(schema) {
144143
self.schemas.insert(name.to_string(), json_schema);
145144
}

examples/proof-of-concept/src/models.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ pub struct AuthResponse {
9595
}
9696

9797
/// User information returned in auth responses
98+
#[rustapi_rs::schema]
9899
#[derive(Debug, Serialize, Schema)]
99100
pub struct UserInfo {
100101
pub id: u64,

0 commit comments

Comments
 (0)