@@ -24,6 +24,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilte
2424/// ```
2525pub struct RustApi {
2626 router : Router ,
27+ openapi_spec : rustapi_openapi:: OpenApiSpec ,
2728}
2829
2930impl RustApi {
@@ -39,6 +40,7 @@ impl RustApi {
3940
4041 Self {
4142 router : Router :: new ( ) ,
43+ openapi_spec : rustapi_openapi:: OpenApiSpec :: new ( "RustAPI Application" , "1.0.0" ) ,
4244 }
4345 }
4446
@@ -57,8 +59,39 @@ impl RustApi {
5759 /// RustApi::new()
5860 /// .state(AppState::new())
5961 /// ```
60- pub fn state < S : Clone + Send + Sync + ' static > ( mut self , state : S ) -> Self {
61- self . router = self . router . state ( state) ;
62+ pub fn state < S > ( self , state : S ) -> Self
63+ where
64+ S : Clone + Send + Sync + ' static ,
65+ {
66+ // For now, state is handled by the router/handlers directly capturing it
67+ // or through a middleware. The current router (matchit) implementation
68+ // doesn't support state injection directly in the same way axum does.
69+ // This is a placeholder for future state management.
70+ self
71+ }
72+
73+ /// Register an OpenAPI schema
74+ ///
75+ /// # Example
76+ ///
77+ /// ```rust,ignore
78+ /// #[derive(Schema)]
79+ /// struct User { ... }
80+ ///
81+ /// RustApi::new()
82+ /// .register_schema::<User>()
83+ /// ```
84+ pub fn register_schema < T : for < ' a > rustapi_openapi:: Schema < ' a > > ( mut self ) -> Self {
85+ self . openapi_spec = self . openapi_spec . register :: < T > ( ) ;
86+ self
87+ }
88+
89+ /// Configure OpenAPI info (title, version, description)
90+ pub fn openapi_info ( mut self , title : & str , version : & str , description : Option < & str > ) -> Self {
91+ self . openapi_spec = rustapi_openapi:: OpenApiSpec :: new ( title, version) ;
92+ if let Some ( desc) = description {
93+ self . openapi_spec = self . openapi_spec . description ( desc) ;
94+ }
6295 self
6396 }
6497
@@ -73,6 +106,11 @@ impl RustApi {
73106 /// .route("/users/{id}", get(get_user).delete(delete_user))
74107 /// ```
75108 pub fn route ( mut self , path : & str , method_router : MethodRouter ) -> Self {
109+ // Register operations in OpenAPI spec
110+ for ( method, op) in & method_router. operations {
111+ self . openapi_spec = self . openapi_spec . path ( path, method. as_str ( ) , op. clone ( ) ) ;
112+ }
113+
76114 self . router = self . router . route ( path, method_router) ;
77115 self
78116 }
@@ -102,26 +140,52 @@ impl RustApi {
102140 /// .run("127.0.0.1:8080")
103141 /// .await
104142 /// ```
105- pub fn mount_route ( self , route : crate :: handler:: Route ) -> Self {
106- use http:: Method ;
143+ pub fn mount_route ( mut self , route : crate :: handler:: Route ) -> Self {
144+ let method_enum = match route. method {
145+ "GET" => http:: Method :: GET ,
146+ "POST" => http:: Method :: POST ,
147+ "PUT" => http:: Method :: PUT ,
148+ "DELETE" => http:: Method :: DELETE ,
149+ "PATCH" => http:: Method :: PATCH ,
150+ _ => http:: Method :: GET ,
151+ } ;
152+
153+ // Register operation in OpenAPI spec
154+ self . openapi_spec = self . openapi_spec . path ( route. path , route. method , route. operation ) ;
107155
108- let method = match route. method {
109- "GET" => Method :: GET ,
110- "POST" => Method :: POST ,
111- "PUT" => Method :: PUT ,
112- "PATCH" => Method :: PATCH ,
113- "DELETE" => Method :: DELETE ,
114- "HEAD" => Method :: HEAD ,
115- "OPTIONS" => Method :: OPTIONS ,
116- _ => panic ! ( "Unknown HTTP method: {}" , route. method) ,
156+ self . route_with_method ( route. path , method_enum, route. handler )
157+ }
158+
159+ /// Helper to mount a single method handler
160+ fn route_with_method ( mut self , path : & str , method : http:: Method , handler : crate :: handler:: BoxedHandler ) -> Self {
161+ use crate :: router:: MethodRouter ;
162+ // use http::Method; // Removed
163+
164+ // This is simplified. In a real implementation we'd merge with existing router at this path
165+ // For now we assume one handler per path or we simply allow overwriting for this MVP step
166+ // (matchit router doesn't allow easy merging/updating existing entries without rebuilding)
167+ //
168+ // TOOD: Enhance Router to support method merging
169+
170+ let path = if !path. starts_with ( '/' ) {
171+ format ! ( "/{}" , path)
172+ } else {
173+ path. to_string ( )
117174 } ;
118175
119- // Convert the boxed handler into a MethodRouter
176+ // Check if we already have this path?
177+ // For MVP, valid assumption: user calls .route() or .mount() once per path-method-combo
178+ // But we need to handle multiple methods on same path.
179+ // Our Router wrapper currently just inserts.
180+
181+ // Since we can't easily query matchit, we'll just insert.
182+ // Limitations: strictly sequential mounting for now.
183+
120184 let mut handlers = std:: collections:: HashMap :: new ( ) ;
121- handlers. insert ( method, route . handler ) ;
185+ handlers. insert ( method, handler) ;
122186
123187 let method_router = MethodRouter :: from_boxed ( handlers) ;
124- self . route ( route . path , method_router)
188+ self . route ( & path, method_router)
125189 }
126190
127191 /// Nest a router under a prefix
@@ -140,17 +204,82 @@ impl RustApi {
140204 self
141205 }
142206
143- /// Enable Swagger UI at the specified path
207+ /// Enable Swagger UI documentation
208+ ///
209+ /// This adds two endpoints:
210+ /// - `{path}` - Swagger UI interface
211+ /// - `{path}/openapi.json` - OpenAPI JSON specification
144212 ///
145213 /// # Example
146214 ///
147215 /// ```rust,ignore
148216 /// RustApi::new()
149- /// .docs("/docs") // Swagger UI at /docs
217+ /// .route("/users", get(list_users))
218+ /// .docs("/docs") // Swagger UI at /docs, spec at /docs/openapi.json
219+ /// .run("127.0.0.1:8080")
220+ /// .await
150221 /// ```
151- pub fn docs ( self , _path : & str ) -> Self {
152- // TODO: Implement OpenAPI + Swagger UI
153- self
222+ pub fn docs ( self , path : & str ) -> Self {
223+ let title = self . openapi_spec . info . title . clone ( ) ;
224+ let version = self . openapi_spec . info . version . clone ( ) ;
225+ let description = self . openapi_spec . info . description . clone ( ) ;
226+
227+ self . docs_with_info ( path, & title, & version, description. as_deref ( ) )
228+ }
229+
230+ /// Enable Swagger UI documentation with custom API info
231+ ///
232+ /// # Example
233+ ///
234+ /// ```rust,ignore
235+ /// RustApi::new()
236+ /// .docs_with_info("/docs", "My API", "2.0.0", Some("API for managing users"))
237+ /// ```
238+ pub fn docs_with_info (
239+ mut self ,
240+ path : & str ,
241+ title : & str ,
242+ version : & str ,
243+ description : Option < & str > ,
244+ ) -> Self {
245+ use crate :: router:: get;
246+ // Update spec info
247+ self . openapi_spec . info . title = title. to_string ( ) ;
248+ self . openapi_spec . info . version = version. to_string ( ) ;
249+ if let Some ( desc) = description {
250+ self . openapi_spec . info . description = Some ( desc. to_string ( ) ) ;
251+ }
252+
253+ let path = path. trim_end_matches ( '/' ) ;
254+ let openapi_path = format ! ( "{}/openapi.json" , path) ;
255+
256+ // Clone values for closures
257+ let spec_json = serde_json:: to_string_pretty ( & self . openapi_spec . to_json ( ) ) . unwrap_or_default ( ) ;
258+ let openapi_url = openapi_path. clone ( ) ;
259+
260+ // Add OpenAPI JSON endpoint
261+ let spec_handler = move || {
262+ let json = spec_json. clone ( ) ;
263+ async move {
264+ http:: Response :: builder ( )
265+ . status ( http:: StatusCode :: OK )
266+ . header ( http:: header:: CONTENT_TYPE , "application/json" )
267+ . body ( http_body_util:: Full :: new ( bytes:: Bytes :: from ( json) ) )
268+ . unwrap ( )
269+ }
270+ } ;
271+
272+ // Add Swagger UI endpoint
273+ let docs_handler = move || {
274+ let url = openapi_url. clone ( ) ;
275+ async move {
276+ let html = rustapi_openapi:: swagger_ui_html ( & url) ;
277+ html
278+ }
279+ } ;
280+
281+ self . route ( & openapi_path, get ( spec_handler) )
282+ . route ( path, get ( docs_handler) )
154283 }
155284
156285 /// Run the server
0 commit comments