rust_microservice_macros/lib.rs
1//! # π Rust Microservice Macros
2//!
3//! A procedural macro crate designed to power the
4//! [`rust_microservice`](https://crates.io/crates/rust_microservice)
5//! ecosystem with compile-time server generation, automatic controller
6//! discovery, OpenAPI integration, authentication enforcement, and
7//! database injection.
8//!
9//! This crate eliminates runtime registration patterns by generating
10//! deterministic, compile-time server bootstrap logic.
11//!
12//! ---
13//!
14//! # π― Design Goals
15//!
16//! - β
Zero runtime reflection
17//! - β
Compile-time controller discovery
18//! - β
Deterministic OpenAPI generation
19//! - β
Integrated JWT security middleware
20//! - β
Declarative database injection
21//! - β
Strict compile-time validation
22//!
23//! All routing, OpenAPI metadata, middleware wrapping, and database
24//! bindings are generated at compile time using Rustβs procedural macro
25//! system.
26//!
27//! ---
28//!
29//! # ποΈ Architecture Overview
30//!
31//! This crate is implemented using:
32//!
33//! - `proc_macro`
34//! - `proc_macro2`
35//! - `syn` (AST parsing)
36//! - `quote` (token generation)
37//! - `walkdir` (controller discovery)
38//!
39//! ## Macro Expansion Pipeline
40//!
41//! 1. Parse attribute arguments (`key = value` pairs)
42//! 2. Parse annotated Rust items (`ItemFn`, modules, etc.)
43//! 3. Load and inspect controller files
44//! 4. Extract Actix-Web handlers
45//! 5. Generate:
46//! - Server bootstrap
47//! - Route registration
48//! - Swagger/OpenAPI specification
49//! - JWT middleware wrappers
50//! - Database injection logic
51//!
52//! No runtime route aggregation occurs β all handlers are resolved
53//! during compilation.
54//!
55//! ---
56//!
57//! # π§© Provided Macros
58//!
59//! This crate exposes three primary procedural attribute macros:
60//!
61//! - `#[api_server]`
62//! - `#[secured]`
63//! - `#[database]`
64//!
65//! ---
66//!
67//! # π `#[api_server]`
68//!
69//! Generates the full HTTP server bootstrap and controller registration
70//! logic for an `actix-web` application.
71//!
72//! ## Responsibilities
73//!
74//! - Recursively scans controller directories
75//! - Registers all HTTP handlers
76//! - Generates Swagger UI configuration
77//! - Generates OpenAPI documentation using `utoipa`
78//! - Optionally initializes database connections
79//! - Wraps the main function with `#[tokio::main]`
80//! - Initializes and runs the global `Server`
81//!
82//! ## Supported Attributes
83//!
84//! | Attribute | Type | Description |
85//! |------------|------|------------|
86//! | `controllers_path` | `&str` | Comma-separated directories containing controllers |
87//! | `openapi_title` | `&str` | OpenAPI title |
88//! | `openapi_api_name` | `&str` | OpenAPI tag name |
89//! | `openapi_api_description` | `&str` | OpenAPI tag description |
90//! | `openapi_auth_server` | `&str` | OAuth2 token URL fallback |
91//! | `database` | `"true" / "false"` | Enables SeaORM database initialization |
92//! | `banner` | `&str` | Startup banner printed during server initialization |
93//!
94//! ## Example
95//!
96//! ```rust,ignore
97//! use rust_microservice::ServerApi; // api_server was renamed to ServerApi for better ergonomics
98//!
99//! #[ServerApi(
100//! controllers_path = "src/controllers",
101//! openapi_title = "π My API",
102//! openapi_api_description = "Example API",
103//! database = "true"
104//! )]
105//! async fn start() -> rust_microservice::Result<(), String> {}
106//! ```
107//!
108//! ---
109//!
110//! β οΈ `IMPORTANT`: The `server_api` (*ServerApi in rust_microservice*), `database` and `secured`
111//! macros has been renamed or re-exported to improve ergonomics in the rust_microservice crate.
112//! This crate provides only the macro implementation. The public API is re-exported by the
113//! rust_microservice crate. Therefore, when using the macro in your project, prefer ServerApi
114//! instead of api_server.
115//!
116//! ---
117//!
118//! ## Generated Behavior
119//!
120//! - Wraps your function with `#[tokio::main]`
121//! - Discovers all Actix-Web handlers
122//! - Generates:
123//! - `register_endpoints`
124//! - `ApiDoc` (`utoipa::OpenApi`)
125//! - Swagger UI endpoint `/swagger-ui/*`
126//!
127//! ---
128//!
129//! # π `#[secured]`
130//!
131//! Protects an Actix-Web endpoint with JWT authentication and
132//! role-based authorization.
133//!
134//! Internally generates:
135//!
136//! - A middleware module
137//! - A wrapper using `actix_web::middleware::from_fn`
138//! - Automatic role validation via `Server::validate_jwt`
139//!
140//! ## Supported Attributes
141//!
142//! | Attribute | Description |
143//! |------------|-------------|
144//! | `method` | HTTP method (`get`, `post`, etc.) |
145//! | `path` | Route path |
146//! | `authorize` | Role expression |
147//!
148//! ## Authorization Formats
149//!
150//! ### Single Role
151//!
152//! ```text
153//! authorize = "ROLE_ADMIN"
154//! ```
155//!
156//! ### Any Role
157//!
158//! ```text
159//! authorize = "hasAnyRole(ROLE_ADMIN, ROLE_USER)"
160//! ```
161//!
162//! ### All Roles
163//!
164//! ```text
165//! authorize = "hasAllRoles(ROLE_ADMIN, ROLE_AUDITOR)"
166//! ```
167//!
168//! ## Example
169//!
170//! ```rust,ignore
171//! use rust_microservice::secured;
172//! use actix_web::HttpResponse;
173//!
174//! #[secured(
175//! method = "get",
176//! path = "/v1/users",
177//! authorize = "hasAnyRole(ROLE_ADMIN, ROLE_USER)"
178//! )]
179//! pub async fn list_users() -> HttpResponse {
180//! HttpResponse::Ok().finish()
181//! }
182//! ```
183//!
184//! ## Security Validation
185//!
186//! The middleware validates:
187//!
188//! - JWT presence
189//! - Signature
190//! - Expiration (`exp`)
191//! - Issuer (`iss`)
192//! - Required roles
193//!
194//! If validation fails β `401 Unauthorized`.
195//!
196//! ---
197//!
198//! # π’οΈ `#[database]`
199//!
200//! Injects a SeaORM `DatabaseConnection` into a repository function.
201//!
202//! ## Required Attributes
203//!
204//! | Attribute | Description |
205//! |------------|-------------|
206//! | `name` | Database configuration name |
207//! | `error` | Error variant returned if connection is unavailable |
208//!
209//! The macro injects:
210//!
211//! ```rust,ignore
212//! let db = Server::global()
213//! .database_with_name("name")?;
214//! ```
215//!
216//! ## Example
217//!
218//! ```rust,ignore
219//! use rust_microservice::database;
220//!
221//! #[database(name = "api", error = "UserError::DatabaseNotConfigured")]
222//! pub async fn find_user(id: i32) -> Result<()> {
223//! // `db` is available here
224//! Ok(())
225//! }
226//! ```
227//!
228//! ---
229//!
230//! # π Controller Discovery
231//!
232//! The `api_server` macro:
233//!
234//! - Traverses `controllers_path`
235//! - Parses each `.rs` file using `syn`
236//! - Extracts functions annotated with:
237//!
238//! ```text
239//! #[get]
240//! #[post]
241//! #[put]
242//! #[delete]
243//! #[patch]
244//! #[head]
245//! #[options]
246//! #[trace]
247//! #[connect]
248//! #[secured]
249//! ```
250//!
251//! These handlers are automatically registered into
252//! `actix_web::web::ServiceConfig`.
253//!
254//! ---
255//!
256//! # π OpenAPI Generation
257//!
258//! Uses `utoipa` to generate:
259//!
260//! - `#[derive(OpenApi)]`
261//! - Swagger UI configuration
262//! - OAuth2 security scheme
263//! - Global security requirements
264//!
265//! The security scheme is dynamically configured from:
266//!
267//! ```rust,ignore
268//! Server::global()?.settings().get_auth2_token_url()
269//! ```
270//!
271//! ---
272//!
273//! # βοΈ Internal Utility Structures
274//!
275//! ### `KeyValue`
276//!
277//! Parses:
278//!
279//! ```text
280//! key = value
281//! ```
282//!
283//! ### `ArgList`
284//!
285//! Parses:
286//!
287//! ```text
288//! key1 = value1, key2 = value2
289//! ```
290//!
291//! These power all attribute parsing in this crate.
292//!
293//! ---
294//!
295//! # π§ Compile-Time Guarantees
296//!
297//! - Controllers must be valid Rust modules
298//! - Handlers must use supported HTTP attributes
299//! - Database names must exist at runtime
300//! - Invalid macro parameters cause compile errors
301//!
302//! ---
303//!
304//! # π§ͺ Runtime Integration
305//!
306//! Although this crate generates compile-time code,
307//! runtime behavior depends on:
308//!
309//! - `actix-web`
310//! - `tokio`
311//! - `utoipa`
312//! - `sea-orm`
313//! - `rust_microservice::Server`
314//!
315//! ---
316//!
317//! # π Summary
318//!
319//! This macro crate transforms a modular Rust project into a fully
320//! initialized HTTP API server with:
321//!
322//! - Automatic route wiring
323//! - JWT security enforcement
324//! - OpenAPI documentation
325//! - Swagger UI
326//! - Database injection
327//!
328//! All achieved with minimal boilerplate and strict compile-time guarantees.
329//!
330//! ---
331//!
332//! π¦ Built for high-performance Rust microservices.
333//! Deterministic. Secure. Compile-time powered.
334//!
335#![allow(clippy::expect_fun_call)]
336#![allow(clippy::bind_instead_of_map)]
337#![allow(clippy::cmp_owned)]
338
339use proc_macro::TokenStream;
340use proc_macro2::Span;
341use quote::{ToTokens, format_ident, quote};
342use std::{fs::File, io::Read, path::Path};
343use syn::{
344 Attribute, Expr, Ident, ItemFn, Result, Token,
345 parse::{Parse, ParseStream},
346 parse_file, parse_macro_input, parse_str,
347 punctuated::Punctuated,
348};
349use walkdir::DirEntry;
350
351/// Represents a single `key = value` pair parsed from a macro input.
352///
353/// This structure is used when implementing procedural macros
354/// that accept configuration-style arguments. The `KeyValue` struct
355/// captures:
356///
357/// - A `key` as a [`syn::Ident`], representing the identifier on the left side.
358/// - An equals sign token (`=`), represented by [`Token![=]`].
359/// - A `value` expression, stored as a [`syn::Expr`].
360///
361/// # Parsing Behavior
362///
363/// The `Parse` implementation consumes three consecutive elements from
364/// the input stream:
365///
366/// ```text
367/// <ident> = <expr>
368/// ```
369///
370/// If any of these elements are missing or malformed, a parsing error
371/// is returned.
372///
373/// # Example of how a single key-value pair might appear in a macro:
374///
375/// ```text
376/// timeout = 30
377/// ```
378struct KeyValue {
379 pub key: Ident,
380 pub _eq: Token![=],
381 pub value: Expr,
382}
383
384impl Parse for KeyValue {
385 fn parse(input: ParseStream) -> Result<Self> {
386 Ok(Self {
387 key: input.parse()?,
388 _eq: input.parse()?,
389 value: input.parse()?,
390 })
391 }
392}
393
394/// The ArgList struct represents a comma-separated list of `key = value`
395/// pairs.
396///
397/// `ArgList` is a generic container used in procedural macros to accept
398/// lists of configuration arguments in the form:
399///
400/// ```text
401/// key1 = value1, key2 = value2, key3 = value3
402/// ```
403///
404/// Internally it stores the items in a [`syn::punctuated::Punctuated`]
405/// structure, which keeps track of both the elements and their punctuation.
406///
407/// # Parsing Behavior
408///
409/// The `Parse` implementation uses [`Punctuated::parse_terminated`] to
410/// parse zero or more `KeyValue` entries separated by commas. Trailing
411/// commas are allowed.
412///
413/// # Example use inside a macro attribute:
414///
415/// ```rust
416/// #[my_macro(a = 1, b = "text", c = true)]
417/// ```
418///
419/// This structure facilitates ergonomic parsing of argument lists in
420/// procedural macros.
421struct ArgList {
422 items: Punctuated<KeyValue, Token![,]>,
423}
424
425impl Parse for ArgList {
426 fn parse(input: ParseStream) -> Result<Self> {
427 Ok(Self {
428 items: Punctuated::parse_terminated(input)?,
429 })
430 }
431}
432
433/// # π API Server Macro
434///
435/// The `api_server` macro is a procedural macro that generates the code necessary to
436/// start an `actix-web` HTTP server with support for OpenAPI documentation and
437/// a health check endpoint.
438///
439/// The `api_server` macro takes the following attributes:
440///
441/// - `controllers_path`: A comma-separated list of paths to modules containing
442/// controllers. The macro will recursively traverse the directories and generate
443/// code to register the controllers with the HTTP server.
444///
445/// - `openapi_title`: A string used as the title of the OpenAPI documentation.
446///
447/// - `openapi_api_description`: A string used as the description of the OpenAPI
448/// documentation.
449///
450/// - `database`: A boolean indicating whether the microservice should enable database
451/// integration. If set to `true`, the macro will generate code to initialize the
452/// database connection pool using the `sea_orm` crate.
453///
454/// - `banner`: A string used as the banner of the microservice. The banner is displayed
455/// in the server logs during startup.
456///
457/// Example of a minimal server bootstrap using this crate:
458///
459/// ```rust,ignore
460/// use rust_microservice::ServerApi;
461///
462/// #[ServerApi(
463/// controllers_path = "src/module, src/controllers",
464/// openapi_title = "π Rest API Server",
465/// openapi_api_description = "Rest API OpenApi Documentation built with Rust π¦.",
466/// database = "true",
467/// banner = r#"
468/// _~^~^~_ ___ ___ ____ ____
469/// \) / o o \ (/ / _ | / _ \ / _/ / __/___ ____ _ __ ___ ____
470/// '_ - _' / __ | / ___/_/ / _\ \ / -_)/ __/| |/ //! -_)/ __/
471/// / '-----' \ /_/ |_|/_/ /___/ /___/ \__//!_/ |___/ \__//!_/
472/// "#
473/// )]
474/// async fn start_server() -> rust_microservice::Result<(), String> {}
475/// ```
476#[proc_macro_attribute]
477pub fn api_server(attrs: TokenStream, item: TokenStream) -> TokenStream {
478 let main_fn = parse_macro_input!(item as ItemFn);
479 let arg_list = parse_macro_input!(attrs as ArgList);
480
481 impl_main_fn(main_fn, arg_list)
482}
483
484/// Generates the expanded `main` function for the procedural macro.
485///
486/// This function extracts the body of the user-provided `main` function,
487/// processes the project's controllers to build endpoint registration code,
488/// and produces the final token stream that initializes and runs the server.
489///
490/// # Parameters
491/// - `main_fn`: The original `main` function captured by the macro.
492/// - `arg_list`: Parsed arguments used to locate and register controllers.
493///
494/// # Returns
495/// A [`TokenStream`] containing the generated `main` function and the
496/// controller registration code.
497fn impl_main_fn(main_fn: ItemFn, arg_list: ArgList) -> TokenStream {
498 let main_body = &main_fn.block.stmts;
499
500 let new_server_command = impl_generate_new_server(&arg_list);
501
502 // Search and process project controllers
503 let (register_token, openapi_handlers, import_modules) =
504 search_and_process_controllers(&arg_list);
505 let openapi_token = generate_openapi_token(openapi_handlers, &arg_list);
506 let database = impl_generate_database_intialization(&arg_list);
507 let fn_name = main_fn.sig.ident;
508 let fn_visibility = &main_fn.vis;
509
510 quote! {
511 #import_modules
512
513 use rust_microservice::Server;
514
515 #[tokio::main]
516 #fn_visibility async fn #fn_name() -> rust_microservice::Result<(), String> {
517 #( #main_body )*
518
519 let server = #new_server_command
520 .init().await.map_err(|e| e.to_string())?
521 #database
522 .configure(Some(register_endpoints));
523
524 Server::set_global(server);
525 let result = Server::global_server();
526 match result {
527 Some(server) => {
528 server.run().await;
529 }
530 None => {
531 return Err("Global server is not initialized".to_string());
532 }
533 }
534
535 Ok(())
536 }
537
538 #register_token
539
540 #openapi_token
541 }
542 .into()
543}
544
545/// Generates code to initialize the database connection based on the provided
546/// configuration parameters.
547///
548/// # Arguments
549///
550/// - `arg_list`: The list of arguments provided to the macro.
551///
552/// # Returns
553///
554/// A `TokenStream` containing the generated code to initialize the database
555/// connection. If the `database` parameter is set to `true`, the generated code
556/// will initialize the database connection. Otherwise, it will return an empty
557/// `TokenStream`.
558fn impl_generate_database_intialization(arg_list: &ArgList) -> proc_macro2::TokenStream {
559 let initialize_database: bool =
560 get_arg_string_value(arg_list, "database".to_string(), "false".to_string())
561 .parse()
562 .expect("Failed to parse Database value");
563
564 match initialize_database {
565 true => quote! {
566 .intialize_database().await.map_err(|e| e.to_string())?
567 },
568 false => quote! {},
569 }
570}
571
572fn impl_generate_new_server(arg_list: &ArgList) -> proc_macro2::TokenStream {
573 let banner = get_arg_string_value(arg_list, "banner".to_string(), "".to_string());
574 if !banner.is_empty() {
575 return quote! {
576 Server::new(env!("CARGO_PKG_VERSION").to_string(), Some(#banner.into()))
577 };
578 }
579 quote! {
580 Server::new(env!("CARGO_PKG_VERSION").to_string(), None)
581 }
582}
583
584/// Searches for controller files and generates endpoint registration code.
585///
586/// This function reads the `controllers_path` argument, recursively scans the
587/// specified directories for Rust (`.rs`) files, and processes each controller
588/// to extract route handlers and required module imports.
589///
590/// It returns:
591/// - A `TokenStream` containing the generated `register_endpoints` function,
592/// which registers all discovered handlers and configures Swagger UI.
593/// - A list of `TokenStream`s representing the collected handlers.
594/// - A `TokenStream` with the unique `use` statements (module imports)
595/// required by the discovered controllers.
596///
597/// # Parameters
598/// * `arg_list` - Macro arguments containing the `controllers_path` configuration.
599///
600/// # Returns
601/// A tuple with:
602/// 1. Generated endpoint registration code.
603/// 2. A vector of handler token streams.
604/// 3. Generated module import token streams.
605fn search_and_process_controllers(
606 arg_list: &ArgList,
607) -> (
608 proc_macro2::TokenStream,
609 Vec<proc_macro2::TokenStream>,
610 proc_macro2::TokenStream,
611) {
612 let controllers_path =
613 get_arg_string_value(arg_list, "controllers_path".to_string(), "".to_string());
614
615 let span = proc_macro::Span::call_site();
616 let main_file_syntax_tree = load_syntax_tree_from_file(span.file());
617
618 let mut handlers: Vec<proc_macro2::TokenStream> = Vec::new();
619 let mut openapi_handlers: Vec<proc_macro2::TokenStream> = Vec::new();
620 let mut import_modules: Vec<proc_macro2::TokenStream> = Vec::new();
621 if !controllers_path.is_empty() {
622 let paths = controllers_path.split(',').collect::<Vec<&str>>();
623 paths.iter().for_each(|root| {
624 //println!("Processing controller path: {root}");
625
626 let path = Path::new(root.trim_matches(|c| c == ' ' || c == '"'));
627 if path.exists() && path.is_dir() {
628 //println!("Processing controller DIR: {path:?}");
629 for entry in walkdir::WalkDir::new(path) {
630 match entry {
631 Ok(entry) => {
632 let file_path = entry.path();
633 if file_path.is_file()
634 && file_path.extension().and_then(|s| s.to_str()) == Some("rs")
635 {
636 let module_path = convert_path_to_module(file_path);
637 let module_token: proc_macro2::TokenStream =
638 parse_str(module_path.as_str()).unwrap();
639
640 let (handler, openapi) = process_controller(&entry, &module_token);
641 if !handler.is_empty() {
642 handlers.push(handler);
643 openapi_handlers.push(openapi);
644 }
645
646 let import_module =
647 find_imported_module(&main_file_syntax_tree, &module_token);
648 if !import_module.is_empty() {
649 let match_found = import_modules
650 .iter()
651 .find(|m| *m.to_string() == import_module.to_string());
652 if match_found.is_none() {
653 import_modules.push(import_module);
654 }
655 }
656 }
657 }
658 Err(error) => println!("Error processing controller. Detail: {error}"),
659 }
660 }
661 }
662 // else {
663 // println!("Controller path does not exist or is not a directory: {path:?}. isDir: {}, exists: {}",
664 // path.is_dir(),
665 // path.exists()
666 // );
667 // }
668 });
669 }
670
671 let quote = quote! {
672 use actix_web::web::ServiceConfig;
673
674 fn register_endpoints(cfg: &mut ServiceConfig) {
675 #(#handlers)*
676
677 // Register the swagger-ui handler
678 let openapi = ApiDoc::openapi();
679 cfg.service(
680 SwaggerUi::new("/swagger-ui/{_:.*}")
681 .url("/api-docs/openapi.json", openapi.clone())
682 .config(Config::default().validator_url("none"))
683 );
684 }
685 };
686
687 let import_mod = quote! {
688 #( #import_modules )*
689 };
690
691 (quote, openapi_handlers, import_mod)
692}
693
694/// Converts a file system path into a Rust module path.
695///
696/// This function normalizes a controller file path by stripping the `src`
697/// prefix, removing the extension, and converting directory separators
698/// into `::`, producing a valid Rust module path.
699///
700/// # Parameters
701/// - `path`: The file path to convert.
702///
703/// # Returns
704/// A `String` containing the normalized module path.
705fn convert_path_to_module(path: &Path) -> String {
706 let s = path
707 .with_extension("")
708 .strip_prefix("src")
709 .unwrap()
710 .to_string_lossy()
711 .to_string();
712 let normalized = s.replace('\\', "/").replace('/', "::");
713 //println!("NORMALIZED: {normalized}");
714 normalized
715 .replace("::mod::", "::")
716 .strip_suffix("::mod")
717 .unwrap_or(&normalized)
718 .to_string()
719}
720
721/// Generates the token stream needed to register all handler functions
722/// from a controller module.
723///
724/// This function reads the controller source file, parses its syntax tree,
725/// extracts handler functions, and produces the Actix-Web service
726/// registration code.
727///
728/// # Parameters
729/// - `file`: Directory entry of the controller file.
730/// - `module`: Parsed module path token.
731///
732/// # Returns
733/// A [`TokenStream`] containing the registration statements.
734fn process_controller(
735 file: &DirEntry,
736 module: &proc_macro2::TokenStream,
737) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
738 let filename = file.file_name().to_str().unwrap();
739 let mut f = File::open(file.path()).expect(format!("Unable to open file: {filename}").as_str());
740 let mut contents = String::new();
741 f.read_to_string(&mut contents)
742 .expect(format!("Unable to read file: {filename}").as_str());
743 let syntax_tree = parse_file(&contents).unwrap();
744 let handles = find_file_handles(&syntax_tree);
745
746 let services = quote! {
747 #( cfg.service(#module::#handles); )*
748 };
749
750 let openapi = quote! {
751 #(#module::#handles),*
752 };
753
754 (services, openapi)
755}
756
757/// Checks whether a module is already imported in the given syntax tree.
758///
759/// This function inspects the root items of a parsed Rust source file
760/// (`syn::File`) to determine if the root module of the provided module path
761/// is declared. If the module is not found, it generates a `mod <name>;`
762/// declaration.
763///
764/// # Parameters
765/// - `syntax_tree`: The parsed Rust file AST to search for module declarations.
766/// - `module`: A token stream representing a module path (e.g. `foo::bar`).
767///
768/// # Returns
769/// A [`TokenStream`] containing:
770/// - an empty token stream if the root module is already declared, or
771/// - a `mod <root_module>;` declaration if it is missing.
772fn find_imported_module(
773 file: &Option<syn::File>,
774 module: &proc_macro2::TokenStream,
775) -> proc_macro2::TokenStream {
776 let root_module: proc_macro2::TokenStream =
777 parse_str(module.to_string().split("::").next().unwrap()).unwrap();
778
779 if let Some(syntax_tree) = file {
780 for item in &syntax_tree.items {
781 if let syn::Item::Mod(item_mod) = item
782 && item_mod.ident.to_string() == root_module.to_string()
783 {
784 // println!("Module already imported: {}", root_module.to_string(),);
785 return quote! {};
786 }
787 }
788 }
789 quote! {
790 pub mod #root_module;
791 }
792}
793
794/// Loads and parses a Rust source file into a `syn::File` syntax tree.
795///
796/// This function opens the given file path, reads its entire contents,
797/// and parses it using `syn::parse_file`, returning the resulting
798/// abstract syntax tree (AST).
799///
800/// # Panics
801///
802/// Panics if the file cannot be opened, read, or if the source code
803/// is not valid Rust syntax.
804///
805/// # Arguments
806///
807/// * `file` - Path to the Rust source file to be parsed.
808///
809/// # Returns
810///
811/// A `syn::File` representing the parsed syntax tree of the source file.
812fn load_syntax_tree_from_file(file: String) -> Option<syn::File> {
813 let f = Path::new(&file);
814 if f.is_dir() || !f.exists() {
815 return None;
816 }
817 let mut f = File::open(file).expect("Unable to open file.");
818 let mut contents = String::new();
819 f.read_to_string(&mut contents)
820 .expect("Unable to read file: {file}");
821 let syntax_tree = parse_file(&contents).unwrap();
822 Some(syntax_tree)
823}
824
825/// Extracts all handler function identifiers from a parsed Rust file.
826///
827/// A handler function is any function annotated with an Actix-Web HTTP
828/// method macro (e.g., `#[get]`, `#[post]`). This function collects those
829/// identifiers for later endpoint registration.
830///
831/// # Parameters
832/// - `syntax_tree`: Parsed Rust file.
833///
834/// # Returns
835/// A vector of function identifiers representing handler endpoints.
836fn find_file_handles(syntax_tree: &syn::File) -> Vec<Ident> {
837 let mut handles: Vec<Ident> = Vec::new();
838 for item in &syntax_tree.items {
839 if let syn::Item::Fn(item_fn) = item
840 && is_handle_function(item_fn)
841 {
842 handles.push(item_fn.sig.ident.clone());
843 }
844 }
845 handles
846}
847
848/// Determines whether a function is an Actix-Web handler.
849///
850/// A function qualifies as a handler if it contains one of the supported
851/// HTTP method attributes (e.g., `#[get]`, `#[post]`, `#[put]`).
852///
853/// # Parameters
854/// - `item_fn`: The function to inspect.
855///
856/// # Returns
857/// `true` if the function is a handler, otherwise `false`.
858fn is_handle_function(item_fn: &ItemFn) -> bool {
859 const HTTP_METHODS: &[&str] = &[
860 "get", "post", "put", "delete", "head", "connect", "options", "trace", "patch", "secured",
861 ];
862
863 item_fn
864 .attrs
865 .iter()
866 .filter_map(|attr| attr.meta.path().get_ident())
867 .any(|ident| HTTP_METHODS.iter().any(|m| ident == m))
868}
869
870/// Generates an OpenAPI specification token stream.
871///
872/// This function builds a `TokenStream` containing the `#[derive(OpenApi)]`
873/// configuration, using values extracted from `arg_list` to define the
874/// OpenAPI title, API name, and description. It also injects the provided
875/// handler paths into the OpenAPI `paths` section.
876///
877/// # Parameters
878/// - `handlers`: A list of tokenized API handler paths.
879/// - `arg_list`: The arguments used to customize OpenAPI metadata.
880///
881/// # Returns
882/// A `TokenStream` representing the OpenAPI configuration for code generation.
883fn generate_openapi_token(
884 handlers: Vec<proc_macro2::TokenStream>,
885 arg_list: &ArgList,
886) -> proc_macro2::TokenStream {
887 let openapi_title = get_arg_string_value(
888 arg_list,
889 "openapi_title".to_string(),
890 "π API Server".to_string(),
891 );
892 let api_name = get_arg_string_value(
893 arg_list,
894 "openapi_api_name".to_string(),
895 "βοΈ Rest API".to_string(),
896 );
897 let api_description = get_arg_string_value(
898 arg_list,
899 "openapi_api_description".to_string(),
900 "Rest API OpenApi Documentation.".to_string(),
901 );
902 let auth_server =
903 get_arg_string_value(arg_list, "openapi_auth_server".to_string(), "".to_string());
904
905 quote! {
906 use utoipa_swagger_ui::{SwaggerUi, Config};
907 use utoipa::{
908 Modify, OpenApi,
909 openapi::SecurityRequirement,
910 openapi::security::{
911 Flow,
912 Password,
913 OAuth2,
914 Scopes,
915 SecurityScheme,
916 Http,
917 HttpAuthScheme
918 },
919 };
920
921 #[derive(OpenApi)]
922 #[openapi(
923 info(
924 title = #openapi_title,
925 ),
926 paths(
927 #( #handlers, )*
928 ),
929 components(
930
931 ),
932 modifiers(&SecurityAddon),
933 tags(
934 (name = #api_name, description = #api_description)
935 ),
936 )]
937 struct ApiDoc;
938
939 struct SecurityAddon;
940
941 impl Modify for SecurityAddon {
942 fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
943 let token_url = match Server::global()
944 .ok()
945 .and_then(|s| s.settings().get_auth2_token_url())
946 {
947 Some(url) => url,
948 None => #auth_server.to_string(),
949 };
950
951 openapi.security = Some(vec![SecurityRequirement::new(
952 "OAuth2 Authentication",
953 ["openid", "profile", "email"],
954 )]);
955
956 if let Some(components) = openapi.components.as_mut() {
957 components.add_security_scheme(
958 "OAuth2 Authentication",
959 SecurityScheme::OAuth2(OAuth2::with_description(
960 [Flow::Password(Password::new(
961 token_url, // authorization url
962 Scopes::from_iter([
963 ("openid", "Standard OIDC scope"),
964 ("profile", "Access to user profile info"),
965 ("email", "Access to user email"),
966 ]),
967 ))],
968 "OAuth2 Authentication",
969 )),
970 );
971 // components.add_security_scheme(
972 // "bearerAuth",
973 // SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
974 // );
975 }
976 }
977 }
978 }
979}
980
981/// Returns the string value of a given key from an `ArgList`.
982///
983/// This function searches the list for an item whose `key` matches the
984/// provided `key`. If found and the value is a string literal, that
985/// literal is returned. Otherwise, the provided `default` value is
986/// returned.
987///
988/// # Parameters
989/// - `arg_list`: The list of parsed arguments.
990/// - `key`: The key to search for.
991/// - `default`: The fallback value if the key is missing or not a string literal.
992///
993/// # Returns
994/// A `String` containing either the matched literal value or the default.
995fn get_arg_string_value(arg_list: &ArgList, key: String, default: String) -> String {
996 let value = arg_list
997 .items
998 .iter()
999 .find(|kv| kv.key == key)
1000 .and_then(|kv| match &kv.value {
1001 Expr::Lit(expr_lit) => match &expr_lit.lit {
1002 syn::Lit::Str(lit_str) => Some(lit_str.value()),
1003 _ => Some(default.clone()),
1004 },
1005 _ => Some(default.clone()),
1006 });
1007 value.unwrap_or(default)
1008}
1009
1010/// # π Secured Macro
1011///
1012/// The `Secured` macro protects `actix-web` endpoints by attaching an authentication middleware.
1013///
1014/// When applied to an endpoint, it validates:
1015///
1016/// - JWT presence in the request.
1017/// - JWT signature.
1018/// - JWT expiration time (`exp` claim).
1019/// - JWT issuer (`iss` claim).
1020/// - Required roles from the `authorize` expression.
1021///
1022/// ## Attribute Reference
1023//
1024/// Macro usage format:
1025//
1026/// ```no_run
1027/// #[secured(method = "...", path = "...", authorize = "...")]
1028/// ```
1029///
1030/// ### **`method`**
1031///
1032/// Defines the HTTP method used to map the endpoint in Actix-Web.
1033///
1034/// Supported values:
1035///
1036/// - `get`
1037/// - `post`
1038/// - `put`
1039/// - `delete`
1040/// - `head`
1041/// - `connect`
1042/// - `options`
1043/// - `trace`
1044/// - `patch`
1045///
1046/// ### **`path`**
1047///
1048/// Defines the endpoint path to be registered by Actix-Web.
1049///
1050/// Example:
1051///
1052/// `path = "/v1/user/{id}"`
1053///
1054/// ### **`authorize`**
1055///
1056/// Defines the required role rule that must be satisfied by roles present in the JWT.
1057///
1058/// Supported formats:
1059///
1060/// 1. `Single role`: validates one role in the token.
1061///
1062/// `authorize = "ROLE_ADMIN"`
1063///
1064/// 2. `hasAnyRole`: validates that at least one role in the list exists in the token.
1065///
1066/// `authorize = "hasAnyRole(ROLE_ADMIN, ROLE_USER)"`
1067///
1068/// 3. `hasAllRoles`: validates that all roles in the list exist in the token.
1069///
1070/// `authorize = "hasAllRoles(ROLE_ADMIN, ROLE_USER)"`
1071///
1072/// ## Examples
1073///
1074/// ### **`Single role`**:
1075///
1076/// ```rust,ignore
1077/// use rust_microservice::secured;
1078/// use actix_web::{HttpResponse, delete, get, http::StatusCode, post, put, web};
1079///
1080/// #[secured(method = "post", path = "/v1/user", authorize = "ROLE_ADMIN")]
1081/// pub async fn create_user_endpoint() -> HttpResponse {
1082/// // handler body
1083/// HttpResponse::Ok().finish()
1084/// }
1085/// ```
1086///
1087/// ### **`Any role`**:
1088///
1089/// ```rust,ignore
1090/// use rust_microservice::secured;
1091/// use actix_web::{HttpResponse, delete, get, http::StatusCode, post, put, web};
1092///
1093/// #[secured(
1094/// method = "get",
1095/// path = "/v1/user/{id}",
1096/// authorize = "hasAnyRole(ROLE_ADMIN, ROLE_USER)"
1097/// )]
1098/// pub async fn get_user_endpoint() -> HttpResponse {
1099/// // handler body
1100/// HttpResponse::Ok().finish()
1101/// }
1102/// ```
1103///
1104/// ### **`All roles`**:
1105///
1106/// ```rust,ignore
1107/// use rust_microservice::secured;
1108/// use actix_web::{HttpResponse, delete, get, http::StatusCode, post, put, web};
1109///
1110/// #[secured(
1111/// method = "delete",
1112/// path = "/v1/user/{id}",
1113/// authorize = "hasAllRoles(ROLE_ADMIN, ROLE_AUDITOR)"
1114/// )]
1115/// pub async fn delete_user_endpoint() -> HttpResponse {
1116/// // handler body
1117/// HttpResponse::Ok().finish()
1118/// }
1119/// ```
1120#[proc_macro_attribute]
1121pub fn secured(attrs: TokenStream, item: TokenStream) -> TokenStream {
1122 let secure_fn = parse_macro_input!(item as ItemFn);
1123 let arg_list = parse_macro_input!(attrs as ArgList);
1124
1125 impl_secured_fn(secure_fn, arg_list)
1126}
1127
1128/// Generates a secured version of a function.
1129///
1130/// This function takes an original function (ItemFn) and an argument list (ArgList)
1131/// and returns a new function definition with the same signature and body,
1132/// but with the authentication and authorization checks applied.
1133///
1134/// The returned function checks if the authenticated user has at least one of the roles
1135/// specified in the `roles` parameter. If the user is authorized, the function body is executed.
1136///
1137/// # Parameters
1138/// - `secured_fn`: The original function to secure.
1139/// - `arg_list`: The list of arguments containing the `roles` configuration parameter.
1140///
1141/// # Returns
1142/// A `TokenStream` containing the secured function definition.
1143fn impl_secured_fn(secured_fn: ItemFn, arg_list: ArgList) -> TokenStream {
1144 let fn_body = &secured_fn.block.stmts;
1145 let sig = &secured_fn.sig.to_token_stream();
1146 let fn_name = &secured_fn.sig.ident;
1147 let _fn_output = &secured_fn.sig.output;
1148 let _fn_params = &secured_fn.sig.inputs;
1149 let _fn_modifiers = &secured_fn.sig.asyncness;
1150 let fn_visibility = &secured_fn.vis;
1151
1152 let method = Ident::new(
1153 get_arg_string_value(&arg_list, "method".to_string(), "".to_string()).as_str(),
1154 Span::call_site(),
1155 );
1156 let path = get_arg_string_value(&arg_list, "path".to_string(), "".to_string()).to_lowercase();
1157 let authorize = get_arg_string_value(&arg_list, "authorize".to_string(), "".to_string());
1158 let _actix_web_attr = update_actix_web_attr(&secured_fn.attrs);
1159 let auth_module_name = format_ident!("auth_{}", fn_name);
1160 let wrap_fn = format!(
1161 "::actix_web::middleware::from_fn({}::auth_middleware)",
1162 auth_module_name
1163 )
1164 .to_string();
1165
1166 quote! {
1167 mod #auth_module_name {
1168 use ::actix_web::{
1169 HttpMessage,
1170 http::header::{self, HeaderValue}
1171 };
1172 use rust_microservice::Server;
1173 use tracing::warn;
1174
1175 pub async fn auth_middleware(
1176 req: ::actix_web::dev::ServiceRequest,
1177 next: ::actix_web::middleware::Next<impl ::actix_web::body::MessageBody>,
1178 ) -> Result<
1179 ::actix_web::dev::ServiceResponse<impl ::actix_web::body::MessageBody>,
1180 ::actix_web::Error,
1181 > {
1182 Server::global()
1183 .map_err(|e| ::actix_web::error::ErrorInternalServerError(e.to_string()))?
1184 .validate_jwt(&req, #authorize.to_string())
1185 .map_err(|e| {
1186 //warn!("Unauthorized: {}", e);
1187 ::actix_web::error::ErrorUnauthorized("Unauthorized user.")
1188 })?;
1189 next.call(req).await
1190 }
1191 }
1192
1193 #[#method(#path, wrap = #wrap_fn)]
1194 #fn_visibility #sig {
1195 #( #fn_body )*
1196 }
1197 }
1198 .into()
1199}
1200
1201/// Updates an Actix-Web attribute by extracting the HTTP method and path.
1202///
1203/// It takes a vector of `syn::Attribute`s, finds the first attribute that matches
1204/// one of the supported HTTP methods, and returns a `proc_macro2::TokenStream`
1205/// containing the updated attribute.
1206///
1207/// Supported HTTP methods are:
1208/// - `get`
1209/// - `post`
1210/// - `put`
1211/// - `delete`
1212/// - `head`
1213/// - `connect`
1214/// - `options`
1215/// - `trace`
1216/// - `patch`
1217///
1218/// If no matching attribute is found, an empty `TokenStream` is returned.
1219fn update_actix_web_attr(attrs: &[Attribute]) -> proc_macro2::TokenStream {
1220 use syn::Expr;
1221
1222 const HTTP_METHODS: &[&str] = &[
1223 "get", "post", "put", "delete", "head", "connect", "options", "trace", "patch",
1224 ];
1225
1226 let attr = match attrs
1227 .iter()
1228 .find(|attr| HTTP_METHODS.iter().any(|m| attr.path().is_ident(m)))
1229 {
1230 Some(attr) => attr,
1231 None => return quote! {},
1232 };
1233
1234 let method = match attr.path().get_ident() {
1235 Some(ident) => ident.to_string(),
1236 None => return quote! {},
1237 };
1238
1239 let value: Expr = match attr.parse_args() {
1240 Ok(v) => v,
1241 Err(_) => return quote! {},
1242 };
1243
1244 let wrapper = quote! {
1245 wrap = "::actix_web::middleware::from_fn(auth::auth_middleware)"
1246 };
1247
1248 match method.as_str() {
1249 "post" => {
1250 quote! { #[post(#value, #wrapper)] }
1251 }
1252 "get" => quote! { #[get(#value)] },
1253 "put" => quote! { #[put(#value)] },
1254 "delete" => quote! { #[delete(#value)] },
1255 "head" => quote! { #[head(#value)] },
1256 "connect" => quote! { #[connect(#value)] },
1257 "options" => quote! { #[options(#value)] },
1258 "trace" => quote! { #[trace(#value)] },
1259 "patch" => quote! { #[patch(#value)] },
1260 _ => quote! {},
1261 }
1262}
1263
1264/// Extracts the list of roles from the `secured` macro attribute.
1265///
1266/// If the `roles` parameter is specified, it is split by commas into a vector of strings.
1267/// If no roles are specified, an empty vector is returned.
1268///
1269/// # Parameters
1270/// - `arg_list`: The list of arguments containing the `roles` parameter.
1271///
1272/// # Returns
1273/// A vector of strings representing the roles specified in the `secured` macro attribute.
1274fn _get_security_roles(arg_list: &ArgList) -> proc_macro2::TokenStream {
1275 let roles_arg = get_arg_string_value(arg_list, "roles".to_string(), "".to_string());
1276 if !roles_arg.is_empty() {
1277 let roles = roles_arg.split(',').collect::<Vec<&str>>();
1278 quote! {
1279 const ROLES: &[&'static str] = &[#(#roles),*];
1280 }
1281 } else {
1282 quote! {
1283 let roles = vec![];
1284 }
1285 }
1286}
1287
1288/// # π’οΈ Database Macro
1289///
1290/// The `database` macro is a procedural macro that injects a database connection
1291/// into repository methods.
1292///
1293/// It expects two mandatory attributes:
1294/// - `name`: selects which configured database connection will be used.
1295/// - `error`: defines the error variant returned when the database is not configured or
1296/// database connection cannot be found.
1297///
1298/// The macro injects a variable named `db` with type `&DatabaseConnection` (seaorm),
1299/// so the function body can execute queries directly.
1300///
1301/// Example:
1302///
1303/// ```rust,ignore
1304/// use rust_microservice::{Server, database};
1305/// use thiserror::Error;
1306///
1307/// #[derive(Debug, Error)]
1308/// pub enum UserError {
1309/// #[error("Database is not configured")]
1310/// DatabaseNotConfigured,
1311///
1312/// #[error("User not found")]
1313/// NotFound,
1314/// }
1315///
1316/// pub type Result<T, E = UserError> = std::result::Result<T, E>;
1317///
1318/// #[database(name = "api", error = "UserError::DatabaseNotConfigured")]
1319/// pub async fn get_user_by_id(user_id: i32) -> Result<()> {
1320///
1321/// // Database will be injected here as `db`
1322///
1323/// //user::Entity::find_by_id(user_id)
1324/// // .one(&db)
1325/// // .await
1326/// // .map_err(|_| UserError::NotFound)?
1327/// // .ok_or(UserError::NotFound)
1328/// // .map(Into::into)
1329///
1330/// Ok(())
1331/// }
1332/// ```
1333#[proc_macro_attribute]
1334pub fn database(attrs: TokenStream, item: TokenStream) -> TokenStream {
1335 let item_fn = parse_macro_input!(item as ItemFn);
1336 let arg_list = parse_macro_input!(attrs as ArgList);
1337
1338 impl_database_fn(item_fn, arg_list)
1339}
1340
1341/// Wraps a function with a database connection retrieval call.
1342///
1343/// This function takes an `ItemFn` and an `ArgList` as parameters. It extracts the
1344/// function body, signature, and visibility from the `ItemFn`, and extracts the database
1345/// name and error message from the `ArgList`. It then generates a token stream that wraps the
1346/// function body with a call to retrieve a database connection from the server's global
1347/// state, using the extracted database name and error message.
1348///
1349/// The generated token stream will contain a function with the same signature and visibility
1350/// as the original function, but with a wrapped body that first retrieves a database connection
1351/// and then calls the original function body with the connection as an argument.
1352///
1353/// # Parameters
1354/// - `item_fn`: The function to wrap with a database connection retrieval call.
1355/// - `arg_list`: The arguments containing the database name and error message.
1356///
1357/// # Returns
1358/// A token stream representing the wrapped function.
1359fn impl_database_fn(item_fn: ItemFn, arg_list: ArgList) -> TokenStream {
1360 let fn_body = &item_fn.block.stmts;
1361 let sig = &item_fn.sig.to_token_stream();
1362 let fn_visibility = &item_fn.vis;
1363 let db_name = get_arg_string_value(&arg_list, "name".to_string(), "".to_string());
1364 let error_str = get_arg_string_value(&arg_list, "error".to_string(), "".to_string());
1365 let error: proc_macro2::TokenStream = parse_str(error_str.as_str()).unwrap();
1366
1367 quote! {
1368 #fn_visibility #sig {
1369 let db = Server::global()
1370 .map_err(|_| #error)?
1371 .database_with_name(#db_name)
1372 .map_err(|_| #error)?;
1373
1374 #( #fn_body )*
1375 }
1376
1377 }
1378 .into()
1379}