rovo_macros/
lib.rs

1#![warn(clippy::all)]
2#![warn(clippy::nursery)]
3#![warn(clippy::pedantic)]
4#![warn(missing_docs)]
5#![warn(rust_2018_idioms)]
6#![deny(unsafe_code)]
7// Allow some overly strict pedantic lints
8#![allow(clippy::too_many_lines)]
9#![allow(clippy::similar_names)]
10
11//! Procedural macros for the Rovo `OpenAPI` documentation framework.
12//!
13//! This crate provides the `#[rovo]` attribute macro that processes doc comments
14//! with special annotations to generate `OpenAPI` documentation automatically.
15
16use proc_macro::TokenStream;
17use quote::{quote, quote_spanned};
18
19mod parser;
20mod utils;
21
22use parser::parse_rovo_function;
23
24/// Macro that generates `OpenAPI` documentation from doc comments.
25///
26/// This macro automatically generates `OpenAPI` documentation for your handlers
27/// using doc comments with special annotations.
28///
29/// # Documentation Format
30///
31/// Use Rust-style doc comment sections and metadata annotations:
32///
33/// ## Sections
34/// - `# Responses` - Document response status codes
35/// - `# Examples` - Provide example responses
36/// - `# Metadata` - Add tags, security, and other metadata
37///
38/// ## Metadata Annotations
39/// - `@tag <tag_name>` - Add a tag for grouping operations (can be used multiple times)
40/// - `@security <scheme_name>` - Add security requirements (can be used multiple times)
41/// - `@id <operation_id>` - Set a custom operation ID (defaults to function name)
42/// - `@hidden` - Hide this operation from documentation
43/// - `@rovo-ignore` - Stop processing annotations after this point
44///
45/// Additionally, the Rust `#[deprecated]` attribute is automatically detected
46/// and will mark the operation as deprecated in the `OpenAPI` spec.
47///
48/// # Example
49///
50/// ```rust,ignore
51/// /// Get a single Todo item.
52/// ///
53/// /// Retrieve a Todo item by its ID from the database.
54/// ///
55/// /// # Responses
56/// ///
57/// /// 200: Json<TodoItem> - Successfully retrieved the todo item
58/// /// 404: () - Todo item was not found
59/// ///
60/// /// # Examples
61/// ///
62/// /// 200: TodoItem::default()
63/// ///
64/// /// # Metadata
65/// ///
66/// /// @tag todos
67/// #[rovo]
68/// async fn get_todo(
69///     State(app): State<AppState>,
70///     Path(todo): Path<SelectTodo>
71/// ) -> impl IntoApiResponse {
72///     // ...
73/// }
74///
75/// /// This is a deprecated endpoint.
76/// ///
77/// /// # Metadata
78/// ///
79/// /// @tag admin
80/// /// @security bearer_auth
81/// #[deprecated]
82/// #[rovo]
83/// async fn old_handler() -> impl IntoApiResponse {
84///     // ...
85/// }
86/// ```
87#[proc_macro_attribute]
88pub fn rovo(_attr: TokenStream, item: TokenStream) -> TokenStream {
89    let input = item;
90
91    match parse_rovo_function(input.into()) {
92        Ok((func_item, doc_info)) => {
93            let func_name = &func_item.name;
94
95            let title = doc_info.title.as_deref().unwrap_or("");
96            let description = doc_info.description.as_deref().unwrap_or("");
97
98            // Generate response setters if we have doc comments
99            let response_code_setters = if doc_info.responses.is_empty() {
100                // No responses specified - generate a minimal docs function
101                vec![]
102            } else {
103                doc_info
104                    .responses
105                    .iter()
106                    .map(|resp| {
107                        let code = resp.status_code;
108                        let response_type = &resp.response_type;
109                        let desc = &resp.description;
110
111                        // Check if there's an explicit example for this status code
112                        doc_info
113                            .examples
114                            .iter()
115                            .find(|e| e.status_code == code)
116                            .map_or_else(
117                                || {
118                                    // No explicit example, just add the description
119                                    quote! {
120                                        .response_with::<#code, #response_type, _>(|res| {
121                                            res.description(#desc)
122                                        })
123                                    }
124                                },
125                                |example| {
126                                    let example_code = &example.example_code;
127                                    quote! {
128                                        .response_with::<#code, #response_type, _>(|res| {
129                                            res.description(#desc)
130                                                .example(#example_code)
131                                        })
132                                    }
133                                },
134                            )
135                    })
136                    .collect()
137            };
138
139            // Generate tag setters
140            let tag_setters: Vec<_> = doc_info
141                .tags
142                .iter()
143                .map(|tag| {
144                    quote! { .tag(#tag) }
145                })
146                .collect();
147
148            // Generate security requirement setters
149            let security_setters: Vec<_> = doc_info
150                .security_requirements
151                .iter()
152                .map(|scheme| {
153                    quote! { .security_requirement(#scheme) }
154                })
155                .collect();
156
157            // Generate operation ID setter
158            let operation_id_setter = doc_info.operation_id.as_ref().map_or_else(
159                || {
160                    // Default to function name if no custom ID provided
161                    let default_id = func_name.to_string();
162                    quote! { .id(#default_id) }
163                },
164                |id| quote! { .id(#id) },
165            );
166
167            // Generate deprecated setter
168            let deprecated_setter = if doc_info.deprecated {
169                quote! { .with(|mut op| { op.inner_mut().deprecated = true; op }) }
170            } else {
171                quote! {}
172            };
173
174            // Generate hidden setter
175            let hidden_setter = if doc_info.hidden {
176                quote! { .hidden(true) }
177            } else {
178                quote! {}
179            };
180
181            // Generate an internal implementation name
182            let impl_name = quote::format_ident!("__{}_impl", func_name);
183
184            // Get the renamed function tokens
185            let impl_func = func_item.with_renamed(&impl_name);
186
187            // Create a const with an uppercase version of the handler name
188            let const_name = quote::format_ident!("{}", func_name.to_string().to_uppercase());
189
190            // Determine the state type for the trait implementation
191            let state_type = func_item
192                .state_type
193                .as_ref()
194                .map_or_else(|| quote! { () }, |st| quote! { #st });
195
196            let output = quote! {
197                // Internal implementation with renamed function
198                #[allow(non_snake_case, private_interfaces)]
199                #impl_func
200
201                // Create a zero-sized type that can be passed to routing functions
202                #[allow(non_camel_case_types)]
203                #[derive(Clone, Copy)]
204                pub struct #func_name;
205
206                impl #func_name {
207                    #[doc(hidden)]
208                    pub fn __docs(op: aide::transform::TransformOperation) -> aide::transform::TransformOperation {
209                        op
210                            #operation_id_setter
211                            .summary(#title)
212                            .description(#description)
213                            #(#tag_setters)*
214                            #deprecated_setter
215                            #hidden_setter
216                            #(#security_setters)*
217                            #(#response_code_setters)*
218                    }
219                }
220
221                // Implement the IntoApiMethodRouter trait
222                impl ::rovo::IntoApiMethodRouter<#state_type> for #func_name {
223                    fn into_get_route(self) -> aide::axum::routing::ApiMethodRouter<#state_type> {
224                        aide::axum::routing::get_with(#impl_name, Self::__docs)
225                    }
226
227                    fn into_post_route(self) -> aide::axum::routing::ApiMethodRouter<#state_type> {
228                        aide::axum::routing::post_with(#impl_name, Self::__docs)
229                    }
230
231                    fn into_patch_route(self) -> aide::axum::routing::ApiMethodRouter<#state_type> {
232                        aide::axum::routing::patch_with(#impl_name, Self::__docs)
233                    }
234
235                    fn into_delete_route(self) -> aide::axum::routing::ApiMethodRouter<#state_type> {
236                        aide::axum::routing::delete_with(#impl_name, Self::__docs)
237                    }
238
239                    fn into_put_route(self) -> aide::axum::routing::ApiMethodRouter<#state_type> {
240                        aide::axum::routing::put_with(#impl_name, Self::__docs)
241                    }
242                }
243
244                // Also create a CONST for explicit use
245                #[allow(non_upper_case_globals)]
246                pub const #const_name: #func_name = #func_name;
247            };
248
249            output.into()
250        }
251        Err(err) => {
252            let err_msg = err.to_string();
253            // Use the span from the error if available, otherwise use call_site
254            let error_tokens = err.span().map_or_else(
255                || {
256                    quote! {
257                        compile_error!(#err_msg);
258                    }
259                },
260                |span| {
261                    quote_spanned! {span=>
262                        compile_error!(#err_msg);
263                    }
264                },
265            );
266            error_tokens.into()
267        }
268    }
269}