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 `{ModName}Api<S = ()>` 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 generated trait name is derived from the annotated module name, so
35/// `mod petstore {}` produces `petstore::PetstoreApi`.
36///
37/// The crate recompiles automatically whenever the spec file changes.
38///
39/// # Arguments
40///
41/// First positional argument: path to the `OpenAPI` YAML or JSON file,
42/// relative to the crate root (`CARGO_MANIFEST_DIR`).
43///
44/// # Errors
45///
46/// The macro emits a compile error if:
47///
48/// - The file cannot be found or read.
49/// - The `OpenAPI` document is malformed or cannot be parsed.
50/// - An operation is missing an `operationId`.
51#[proc_macro_attribute]
52pub fn openapi_trait(attr: TokenStream, item: TokenStream) -> TokenStream {
53 let path_lit = parse_macro_input!(attr as LitStr);
54 run_macro(&path_lit, item)
55}
56
57/// Run the core macro logic with the resolved spec path literal.
58fn run_macro(path_lit: &LitStr, item: TokenStream) -> TokenStream {
59 let module = parse_macro_input!(item as ItemMod);
60 let mod_ident = &module.ident;
61 let mod_vis = &module.vis;
62
63 let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") else {
64 return syn::Error::new(
65 Span::call_site(),
66 "CARGO_MANIFEST_DIR is not set; cannot resolve spec path",
67 )
68 .to_compile_error()
69 .into();
70 };
71
72 let spec_path = std::path::PathBuf::from(&manifest_dir).join(path_lit.value());
73 let spec_path_str = spec_path.to_string_lossy().into_owned();
74
75 let content = match std::fs::read_to_string(&spec_path) {
76 Ok(c) => c,
77 Err(e) => {
78 let msg = format!("cannot read OpenAPI spec `{spec_path_str}`: {e}");
79 return syn::Error::new(path_lit.span(), msg)
80 .to_compile_error()
81 .into();
82 }
83 };
84
85 let openapi: openapiv3::OpenAPI = match serde_yaml::from_str(&content) {
86 Ok(o) => o,
87 Err(e) => {
88 let msg = format!("cannot parse OpenAPI spec `{spec_path_str}`: {e}");
89 return syn::Error::new(path_lit.span(), msg)
90 .to_compile_error()
91 .into();
92 }
93 };
94
95 let body = codegen::generate_axum(mod_ident, &openapi);
96
97 let expanded = quote! {
98 // Re-compile when the spec file changes.
99 const _: &str = ::core::include_str!(#spec_path_str);
100
101 #[allow(
102 missing_docs,
103 missing_debug_implementations,
104 dead_code,
105 unused_imports,
106 clippy::all,
107 clippy::nursery,
108 clippy::pedantic,
109 )]
110 #mod_vis mod #mod_ident {
111 #body
112 }
113 };
114
115 expanded.into()
116}