Skip to main content

openapi_trait_axum/
lib.rs

1//! Axum backend proc-macro for `openapi-trait`.
2//!
3//! This crate is not intended for direct use. Use the
4//! [`openapi-trait`](https://docs.rs/openapi-trait) crate instead, which
5//! re-exports the [`openapi_trait`] attribute macro from here as
6//! `openapi_trait::axum`.
7
8/// Code-generation modules for the axum backend.
9mod codegen;
10
11use proc_macro::TokenStream;
12use proc_macro2::Span;
13use quote::quote;
14use syn::{parse_macro_input, ItemMod, LitStr};
15
16/// Generates typed Rust code from an `OpenAPI` specification file.
17///
18/// Apply this attribute to a `mod` block. The macro reads the `OpenAPI`
19/// document at the given path (resolved relative to `CARGO_MANIFEST_DIR`) at
20/// compile time and replaces the module's contents with:
21///
22/// - Schema structs derived from `components/schemas`
23/// - A `{OperationId}Request` struct per operation (bundles path, query,
24///   header params and the request body)
25/// - Per-operation `{OperationId}Response` enums implementing
26///   [`axum::response::IntoResponse`](https://docs.rs/axum/latest/axum/response/trait.IntoResponse.html)
27/// - A `{Title}Api` trait with one `async fn` per operation (keyed by
28///   `operationId`). Trait methods have a default implementation that returns
29///   `500 Internal Server Error`, so you only need to override the operations
30///   your server handles.
31/// - A `router` method on the trait that wires all operations to an
32///   [`axum::Router`](https://docs.rs/axum/latest/axum/struct.Router.html)
33///
34/// The crate recompiles automatically whenever the spec file changes.
35///
36/// # Arguments
37///
38/// First positional argument: path to the `OpenAPI` YAML or JSON file,
39/// relative to the crate root (`CARGO_MANIFEST_DIR`).
40///
41/// # Examples
42///
43/// ```rust,ignore
44/// #[openapi_trait::axum("openapi/petstore.yaml")]
45/// pub mod petstore {}
46///
47/// #[derive(Clone)]
48/// struct MyServer;
49///
50/// impl petstore::PetstoreApi for MyServer {
51///     type Error = std::convert::Infallible;
52///
53///     async fn get_pet_by_id(
54///         &self,
55///         req: petstore::GetPetByIdRequest,
56///         _state: axum::extract::State<()>,
57///         _headers: axum::http::HeaderMap,
58///     ) -> Result<petstore::GetPetByIdResponse, Self::Error> {
59///         Ok(petstore::GetPetByIdResponse::Status200(petstore::Pet {
60///             id: Some(req.pet_id),
61///             name: "doggie".into(),
62///             photo_urls: vec![],
63///             category: None,
64///             tags: None,
65///             status: None,
66///         }))
67///     }
68/// }
69///
70/// let app = MyServer.router().with_state(());
71/// ```
72///
73/// # Errors
74///
75/// The macro emits a compile error if:
76///
77/// - The file cannot be found or read.
78/// - The `OpenAPI` document is malformed or cannot be parsed.
79/// - An operation is missing an `operationId`.
80#[proc_macro_attribute]
81pub fn openapi_trait(attr: TokenStream, item: TokenStream) -> TokenStream {
82    let path_lit = parse_macro_input!(attr as LitStr);
83    run_macro(&path_lit, item)
84}
85
86/// Run the core macro logic with the resolved spec path literal.
87fn run_macro(path_lit: &LitStr, item: TokenStream) -> TokenStream {
88    let module = parse_macro_input!(item as ItemMod);
89    let mod_ident = &module.ident;
90    let mod_vis = &module.vis;
91
92    let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") else {
93        return syn::Error::new(
94            Span::call_site(),
95            "CARGO_MANIFEST_DIR is not set; cannot resolve spec path",
96        )
97        .to_compile_error()
98        .into();
99    };
100
101    let spec_path = std::path::PathBuf::from(&manifest_dir).join(path_lit.value());
102    let spec_path_str = spec_path.to_string_lossy().into_owned();
103
104    let content = match std::fs::read_to_string(&spec_path) {
105        Ok(c) => c,
106        Err(e) => {
107            let msg = format!("cannot read OpenAPI spec `{spec_path_str}`: {e}");
108            return syn::Error::new(path_lit.span(), msg)
109                .to_compile_error()
110                .into();
111        }
112    };
113
114    let openapi: openapiv3::OpenAPI = match serde_yaml::from_str(&content) {
115        Ok(o) => o,
116        Err(e) => {
117            let msg = format!("cannot parse OpenAPI spec `{spec_path_str}`: {e}");
118            return syn::Error::new(path_lit.span(), msg)
119                .to_compile_error()
120                .into();
121        }
122    };
123
124    let body = codegen::generate_axum(mod_ident, &openapi);
125
126    let expanded = quote! {
127        // Re-compile when the spec file changes.
128        const _: &str = ::core::include_str!(#spec_path_str);
129
130        #[allow(
131            missing_docs,
132            missing_debug_implementations,
133            dead_code,
134            unused_imports,
135            clippy::all,
136            clippy::nursery,
137            clippy::pedantic,
138        )]
139        #mod_vis mod #mod_ident {
140            #body
141        }
142    };
143
144    expanded.into()
145}