@@ -4,6 +4,7 @@ use crate::error::Result;
44use crate :: middleware:: { BodyLimitLayer , LayerStack , MiddlewareLayer , DEFAULT_BODY_LIMIT } ;
55use crate :: router:: { MethodRouter , Router } ;
66use crate :: server:: Server ;
7+ use std:: collections:: HashMap ;
78use tracing_subscriber:: { layer:: SubscriberExt , util:: SubscriberInitExt , EnvFilter } ;
89
910/// Main application builder for RustAPI
@@ -53,6 +54,65 @@ impl RustApi {
5354 }
5455 }
5556
57+ /// Create a zero-config RustAPI application.
58+ ///
59+ /// All routes decorated with `#[rustapi::get]`, `#[rustapi::post]`, etc.
60+ /// are automatically registered. Swagger UI is enabled at `/docs` by default.
61+ ///
62+ /// # Example
63+ ///
64+ /// ```rust,ignore
65+ /// use rustapi_rs::prelude::*;
66+ ///
67+ /// #[rustapi::get("/users")]
68+ /// async fn list_users() -> Json<Vec<User>> {
69+ /// Json(vec![])
70+ /// }
71+ ///
72+ /// #[rustapi::main]
73+ /// async fn main() -> Result<()> {
74+ /// // Zero config - routes are auto-registered!
75+ /// RustApi::auto()
76+ /// .run("0.0.0.0:8080")
77+ /// .await
78+ /// }
79+ /// ```
80+ #[ cfg( feature = "swagger-ui" ) ]
81+ pub fn auto ( ) -> Self {
82+ // Build app with grouped auto-routes and auto-schemas, then enable docs.
83+ Self :: new ( ) . mount_auto_routes_grouped ( ) . docs ( "/docs" )
84+ }
85+
86+ /// Create a zero-config RustAPI application (without swagger-ui feature).
87+ ///
88+ /// All routes decorated with `#[rustapi::get]`, `#[rustapi::post]`, etc.
89+ /// are automatically registered.
90+ #[ cfg( not( feature = "swagger-ui" ) ) ]
91+ pub fn auto ( ) -> Self {
92+ Self :: new ( ) . mount_auto_routes_grouped ( )
93+ }
94+
95+ /// Create a configurable RustAPI application with auto-routes.
96+ ///
97+ /// Provides builder methods for customization while still
98+ /// auto-registering all decorated routes.
99+ ///
100+ /// # Example
101+ ///
102+ /// ```rust,ignore
103+ /// use rustapi_rs::prelude::*;
104+ ///
105+ /// RustApi::config()
106+ /// .docs_path("/api-docs")
107+ /// .body_limit(5 * 1024 * 1024) // 5MB
108+ /// .openapi_info("My API", "2.0.0", Some("API Description"))
109+ /// .run("0.0.0.0:8080")
110+ /// .await?;
111+ /// ```
112+ pub fn config ( ) -> RustApiConfig {
113+ RustApiConfig :: new ( )
114+ }
115+
56116 /// Set the global body size limit for request bodies
57117 ///
58118 /// This protects against denial-of-service attacks via large payloads.
@@ -166,10 +226,58 @@ impl RustApi {
166226
167227 /// Configure OpenAPI info (title, version, description)
168228 pub fn openapi_info ( mut self , title : & str , version : & str , description : Option < & str > ) -> Self {
169- self . openapi_spec = rustapi_openapi:: OpenApiSpec :: new ( title, version) ;
170- if let Some ( desc) = description {
171- self . openapi_spec = self . openapi_spec . description ( desc) ;
229+ // NOTE: Do not reset the spec here; doing so would drop collected paths/schemas.
230+ // This is especially important for `RustApi::auto()` and `RustApi::config()`.
231+ self . openapi_spec . info . title = title. to_string ( ) ;
232+ self . openapi_spec . info . version = version. to_string ( ) ;
233+ self . openapi_spec . info . description = description. map ( |d| d. to_string ( ) ) ;
234+ self
235+ }
236+
237+ /// Get the current OpenAPI spec (for advanced usage/testing).
238+ pub fn openapi_spec ( & self ) -> & rustapi_openapi:: OpenApiSpec {
239+ & self . openapi_spec
240+ }
241+
242+ fn mount_auto_routes_grouped ( mut self ) -> Self {
243+ let routes = crate :: auto_route:: collect_auto_routes ( ) ;
244+ let mut by_path: HashMap < String , MethodRouter > = HashMap :: new ( ) ;
245+
246+ for route in routes {
247+ let method_enum = match route. method {
248+ "GET" => http:: Method :: GET ,
249+ "POST" => http:: Method :: POST ,
250+ "PUT" => http:: Method :: PUT ,
251+ "DELETE" => http:: Method :: DELETE ,
252+ "PATCH" => http:: Method :: PATCH ,
253+ _ => http:: Method :: GET ,
254+ } ;
255+
256+ let path = if route. path . starts_with ( '/' ) {
257+ route. path . to_string ( )
258+ } else {
259+ format ! ( "/{}" , route. path)
260+ } ;
261+
262+ let entry = by_path. entry ( path) . or_insert_with ( MethodRouter :: new) ;
263+ entry. insert_boxed_with_operation ( method_enum, route. handler , route. operation ) ;
172264 }
265+
266+ let route_count = by_path
267+ . values ( )
268+ . map ( |mr| mr. allowed_methods ( ) . len ( ) )
269+ . sum :: < usize > ( ) ;
270+ let path_count = by_path. len ( ) ;
271+
272+ for ( path, method_router) in by_path {
273+ self = self . route ( & path, method_router) ;
274+ }
275+
276+ tracing:: info!( paths = path_count, routes = route_count, "Auto-registered routes" ) ;
277+
278+ // Apply any auto-registered schemas.
279+ crate :: auto_schema:: apply_auto_schemas ( & mut self . openapi_spec ) ;
280+
173281 self
174282 }
175283
@@ -186,7 +294,9 @@ impl RustApi {
186294 pub fn route ( mut self , path : & str , method_router : MethodRouter ) -> Self {
187295 // Register operations in OpenAPI spec
188296 for ( method, op) in & method_router. operations {
189- self . openapi_spec = self . openapi_spec . path ( path, method. as_str ( ) , op. clone ( ) ) ;
297+ let mut op = op. clone ( ) ;
298+ add_path_params_to_operation ( path, & mut op) ;
299+ self . openapi_spec = self . openapi_spec . path ( path, method. as_str ( ) , op) ;
190300 }
191301
192302 self . router = self . router . route ( path, method_router) ;
@@ -229,9 +339,9 @@ impl RustApi {
229339 } ;
230340
231341 // Register operation in OpenAPI spec
232- self . openapi_spec = self
233- . openapi_spec
234- . path ( route. path , route. method , route . operation ) ;
342+ let mut op = route . operation ;
343+ add_path_params_to_operation ( route . path , & mut op ) ;
344+ self . openapi_spec = self . openapi_spec . path ( route. path , route. method , op ) ;
235345
236346 self . route_with_method ( route. path , method_enum, route. handler )
237347 }
@@ -526,6 +636,57 @@ impl RustApi {
526636 }
527637}
528638
639+ fn add_path_params_to_operation ( path : & str , op : & mut rustapi_openapi:: Operation ) {
640+ let mut params: Vec < String > = Vec :: new ( ) ;
641+ let mut in_brace = false ;
642+ let mut current = String :: new ( ) ;
643+
644+ for ch in path. chars ( ) {
645+ match ch {
646+ '{' => {
647+ in_brace = true ;
648+ current. clear ( ) ;
649+ }
650+ '}' => {
651+ if in_brace {
652+ in_brace = false ;
653+ if !current. is_empty ( ) {
654+ params. push ( current. clone ( ) ) ;
655+ }
656+ }
657+ }
658+ _ => {
659+ if in_brace {
660+ current. push ( ch) ;
661+ }
662+ }
663+ }
664+ }
665+
666+ if params. is_empty ( ) {
667+ return ;
668+ }
669+
670+ let op_params = op. parameters . get_or_insert_with ( Vec :: new) ;
671+
672+ for name in params {
673+ let already = op_params
674+ . iter ( )
675+ . any ( |p| p. location == "path" && p. name == name) ;
676+ if already {
677+ continue ;
678+ }
679+
680+ op_params. push ( rustapi_openapi:: Parameter {
681+ name,
682+ location : "path" . to_string ( ) ,
683+ required : true ,
684+ description : None ,
685+ schema : rustapi_openapi:: SchemaRef :: Inline ( serde_json:: json!( { "type" : "string" } ) ) ,
686+ } ) ;
687+ }
688+ }
689+
529690impl Default for RustApi {
530691 fn default ( ) -> Self {
531692 Self :: new ( )
@@ -557,3 +718,105 @@ fn unauthorized_response() -> crate::Response {
557718 ) ) )
558719 . unwrap ( )
559720}
721+
722+ /// Configuration builder for RustAPI with auto-routes
723+ pub struct RustApiConfig {
724+ docs_path : Option < String > ,
725+ docs_enabled : bool ,
726+ api_title : String ,
727+ api_version : String ,
728+ api_description : Option < String > ,
729+ body_limit : Option < usize > ,
730+ layers : LayerStack ,
731+ }
732+
733+ impl RustApiConfig {
734+ pub fn new ( ) -> Self {
735+ Self {
736+ docs_path : Some ( "/docs" . to_string ( ) ) ,
737+ docs_enabled : true ,
738+ api_title : "RustAPI" . to_string ( ) ,
739+ api_version : "1.0.0" . to_string ( ) ,
740+ api_description : None ,
741+ body_limit : None ,
742+ layers : LayerStack :: new ( ) ,
743+ }
744+ }
745+
746+ /// Set the docs path (default: "/docs")
747+ pub fn docs_path ( mut self , path : impl Into < String > ) -> Self {
748+ self . docs_path = Some ( path. into ( ) ) ;
749+ self
750+ }
751+
752+ /// Enable or disable docs (default: true)
753+ pub fn docs_enabled ( mut self , enabled : bool ) -> Self {
754+ self . docs_enabled = enabled;
755+ self
756+ }
757+
758+ /// Set OpenAPI info
759+ pub fn openapi_info (
760+ mut self ,
761+ title : impl Into < String > ,
762+ version : impl Into < String > ,
763+ description : Option < impl Into < String > > ,
764+ ) -> Self {
765+ self . api_title = title. into ( ) ;
766+ self . api_version = version. into ( ) ;
767+ self . api_description = description. map ( |d| d. into ( ) ) ;
768+ self
769+ }
770+
771+ /// Set body size limit
772+ pub fn body_limit ( mut self , limit : usize ) -> Self {
773+ self . body_limit = Some ( limit) ;
774+ self
775+ }
776+
777+ /// Add a middleware layer
778+ pub fn layer < L > ( mut self , layer : L ) -> Self
779+ where
780+ L : MiddlewareLayer ,
781+ {
782+ self . layers . push ( Box :: new ( layer) ) ;
783+ self
784+ }
785+
786+ /// Build the RustApi instance
787+ pub fn build ( self ) -> RustApi {
788+ let mut app = RustApi :: new ( ) . mount_auto_routes_grouped ( ) ;
789+
790+ // Apply configuration
791+ if let Some ( limit) = self . body_limit {
792+ app = app. body_limit ( limit) ;
793+ }
794+
795+ app = app. openapi_info (
796+ & self . api_title ,
797+ & self . api_version ,
798+ self . api_description . as_deref ( ) ,
799+ ) ;
800+
801+ #[ cfg( feature = "swagger-ui" ) ]
802+ if self . docs_enabled {
803+ if let Some ( path) = self . docs_path {
804+ app = app. docs ( & path) ;
805+ }
806+ }
807+
808+ // Apply layers
809+ // Note: layers are applied in reverse order in RustApi::layer logic (pushing to vec)
810+ app. layers . extend ( self . layers . into_iter ( ) ) ;
811+
812+ app
813+ }
814+
815+ /// Build and run the server
816+ pub async fn run (
817+ self ,
818+ addr : impl AsRef < str > ,
819+ ) -> Result < ( ) , Box < dyn std:: error:: Error + Send + Sync > > {
820+ self . build ( ) . run ( addr. as_ref ( ) ) . await
821+ }
822+ }
0 commit comments