torch_web/
api.rs

1//! # API Development and Documentation
2//!
3//! This module provides comprehensive tools for building and documenting REST APIs
4//! with Torch. It includes API versioning, automatic OpenAPI/Swagger documentation
5//! generation, endpoint documentation, and API testing utilities.
6//!
7//! ## Features
8//!
9//! - **API Versioning**: Support for multiple API versions with deprecation handling
10//! - **OpenAPI Generation**: Automatic OpenAPI 3.0 specification generation
11//! - **Interactive Documentation**: Built-in Swagger UI for API exploration
12//! - **Endpoint Documentation**: Rich documentation for API endpoints
13//! - **Schema Validation**: Request/response schema validation
14//! - **API Testing**: Built-in testing utilities for API endpoints
15//! - **Rate Limiting**: Per-endpoint rate limiting configuration
16//! - **Authentication**: API key and JWT authentication support
17//!
18//! **Note**: This module requires the `api` feature to be enabled.
19//!
20//! ## Quick Start
21//!
22//! ### Basic API Setup
23//!
24//! ```rust
25//! use torch_web::{App, api::*};
26//! use serde::{Deserialize, Serialize};
27//!
28//! #[derive(Deserialize, Serialize)]
29//! struct User {
30//!     id: u32,
31//!     name: String,
32//!     email: String,
33//! }
34//!
35//! let app = App::new()
36//!     // Enable API documentation
37//!     .with_api_docs(ApiDocBuilder::new()
38//!         .title("My API")
39//!         .version("1.0.0")
40//!         .description("A sample API built with Torch"))
41//!
42//!     // API endpoints with documentation
43//!     .get("/api/users", |_req| async {
44//!         let users = vec![
45//!             User { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string() },
46//!             User { id: 2, name: "Bob".to_string(), email: "bob@example.com".to_string() },
47//!         ];
48//!         Response::ok().json(&users)
49//!     })
50//!     .document_endpoint("/api/users", EndpointDoc::new()
51//!         .method("GET")
52//!         .summary("List all users")
53//!         .description("Returns a list of all users in the system")
54//!         .response(200, ResponseDoc::new()
55//!             .description("List of users")
56//!             .json_schema::<Vec<User>>()))
57//!
58//!     // Swagger UI endpoint
59//!     .get("/docs", |_req| async {
60//!         swagger_ui("/api/openapi.json")
61//!     })
62//!
63//!     // OpenAPI spec endpoint
64//!     .get("/api/openapi.json", |_req| async {
65//!         generate_openapi_spec()
66//!     });
67//! ```
68//!
69//! ### API Versioning
70//!
71//! ```rust
72//! use torch_web::{App, api::*};
73//!
74//! let app = App::new()
75//!     // Version 1 (deprecated)
76//!     .api_version(ApiVersion::new("v1", "Legacy API")
77//!         .deprecated(Some("2024-12-31")))
78//!     .get("/api/v1/users", |_req| async {
79//!         Response::ok()
80//!             .header("Deprecation", "true")
81//!             .header("Sunset", "2024-12-31")
82//!             .json(&legacy_users())
83//!     })
84//!
85//!     // Version 2 (current)
86//!     .api_version(ApiVersion::new("v2", "Current API"))
87//!     .get("/api/v2/users", |_req| async {
88//!         Response::ok().json(&current_users())
89//!     });
90//! ```
91//!
92//! ### Advanced Documentation
93//!
94//! ```rust
95//! use torch_web::{App, api::*, extractors::*};
96//! use serde::{Deserialize, Serialize};
97//!
98//! #[derive(Deserialize)]
99//! struct CreateUserRequest {
100//!     name: String,
101//!     email: String,
102//! }
103//!
104//! #[derive(Serialize)]
105//! struct CreateUserResponse {
106//!     id: u32,
107//!     name: String,
108//!     email: String,
109//!     created_at: String,
110//! }
111//!
112//! let app = App::new()
113//!     .post("/api/users", |Json(req): Json<CreateUserRequest>| async move {
114//!         // Create user logic
115//!         let user = CreateUserResponse {
116//!             id: 123,
117//!             name: req.name,
118//!             email: req.email,
119//!             created_at: "2024-01-01T00:00:00Z".to_string(),
120//!         };
121//!         Response::created().json(&user)
122//!     })
123//!     .document_endpoint("/api/users", EndpointDoc::new()
124//!         .method("POST")
125//!         .summary("Create a new user")
126//!         .description("Creates a new user account with the provided information")
127//!         .tag("Users")
128//!         .request_body(RequestBodyDoc::new()
129//!             .description("User creation data")
130//!             .json_schema::<CreateUserRequest>()
131//!             .required(true))
132//!         .response(201, ResponseDoc::new()
133//!             .description("User created successfully")
134//!             .json_schema::<CreateUserResponse>())
135//!         .response(400, ResponseDoc::new()
136//!             .description("Invalid request data"))
137//!         .response(409, ResponseDoc::new()
138//!             .description("User already exists")));
139//! ```
140
141use std::collections::HashMap;
142use crate::{Request, Response, App, Handler};
143
144#[cfg(feature = "json")]
145use serde_json::{json, Value};
146
147/// API version information
148#[derive(Debug, Clone)]
149pub struct ApiVersion {
150    pub version: String,
151    pub description: String,
152    pub deprecated: bool,
153    pub sunset_date: Option<String>,
154}
155
156impl ApiVersion {
157    pub fn new(version: &str, description: &str) -> Self {
158        Self {
159            version: version.to_string(),
160            description: description.to_string(),
161            deprecated: false,
162            sunset_date: None,
163        }
164    }
165
166    pub fn deprecated(mut self, sunset_date: Option<&str>) -> Self {
167        self.deprecated = true;
168        self.sunset_date = sunset_date.map(|s| s.to_string());
169        self
170    }
171}
172
173/// API endpoint documentation
174#[derive(Debug, Clone)]
175pub struct EndpointDoc {
176    pub method: String,
177    pub path: String,
178    pub summary: String,
179    pub description: String,
180    pub parameters: Vec<ParameterDoc>,
181    pub responses: HashMap<u16, ResponseDoc>,
182    pub tags: Vec<String>,
183}
184
185/// Internal API endpoint representation
186#[derive(Debug, Clone)]
187pub struct ApiEndpoint {
188    pub method: String,
189    pub path: String,
190    pub summary: String,
191    pub description: String,
192    pub parameters: Vec<ParameterDoc>,
193    pub responses: HashMap<u16, ResponseDoc>,
194    pub tags: Vec<String>,
195}
196
197/// Complete API documentation
198#[derive(Debug, Clone)]
199pub struct ApiDocumentation {
200    pub title: String,
201    pub version: String,
202    pub description: String,
203    pub endpoints: Vec<ApiEndpoint>,
204}
205
206#[derive(Debug, Clone)]
207pub struct ParameterDoc {
208    pub name: String,
209    pub location: ParameterLocation,
210    pub description: String,
211    pub required: bool,
212    pub schema_type: String,
213    pub example: Option<String>,
214}
215
216#[derive(Debug, Clone)]
217pub enum ParameterLocation {
218    Path,
219    Query,
220    Header,
221    Body,
222}
223
224#[derive(Debug, Clone)]
225pub struct ResponseDoc {
226    pub description: String,
227    pub content_type: String,
228    pub example: Option<String>,
229}
230
231/// API documentation builder
232#[derive(Clone)]
233pub struct ApiDocBuilder {
234    title: String,
235    description: String,
236    version: String,
237    base_url: String,
238    endpoints: Vec<EndpointDoc>,
239    versions: HashMap<String, ApiVersion>,
240}
241
242impl ApiDocBuilder {
243    pub fn new(title: &str, version: &str) -> Self {
244        Self {
245            title: title.to_string(),
246            description: String::new(),
247            version: version.to_string(),
248            base_url: "/".to_string(),
249            endpoints: Vec::new(),
250            versions: HashMap::new(),
251        }
252    }
253
254    pub fn description(mut self, description: &str) -> Self {
255        self.description = description.to_string();
256        self
257    }
258
259    pub fn base_url(mut self, base_url: &str) -> Self {
260        self.base_url = base_url.to_string();
261        self
262    }
263
264    pub fn add_version(mut self, version: ApiVersion) -> Self {
265        self.versions.insert(version.version.clone(), version);
266        self
267    }
268
269    pub fn add_endpoint(mut self, endpoint: EndpointDoc) -> Self {
270        self.endpoints.push(endpoint);
271        self
272    }
273
274    /// Generate OpenAPI 3.0 specification
275    #[cfg(feature = "json")]
276    pub fn generate_openapi(&self) -> Value {
277        let mut paths = serde_json::Map::new();
278        
279        for endpoint in &self.endpoints {
280            let path_item = paths.entry(&endpoint.path).or_insert_with(|| json!({}));
281            
282            let mut operation = serde_json::Map::new();
283            operation.insert("summary".to_string(), json!(endpoint.summary));
284            operation.insert("description".to_string(), json!(endpoint.description));
285            operation.insert("tags".to_string(), json!(endpoint.tags));
286            
287            // Note: deprecated field removed from ApiEndpoint for simplicity
288            
289            // Parameters
290            if !endpoint.parameters.is_empty() {
291                let params: Vec<Value> = endpoint.parameters.iter().map(|p| {
292                    json!({
293                        "name": p.name,
294                        "in": match p.location {
295                            ParameterLocation::Path => "path",
296                            ParameterLocation::Query => "query",
297                            ParameterLocation::Header => "header",
298                            ParameterLocation::Body => "body",
299                        },
300                        "description": p.description,
301                        "required": p.required,
302                        "schema": {
303                            "type": p.schema_type
304                        }
305                    })
306                }).collect();
307                operation.insert("parameters".to_string(), json!(params));
308            }
309            
310            // Responses
311            let mut responses = serde_json::Map::new();
312            for (status, response) in &endpoint.responses {
313                responses.insert(status.to_string(), json!({
314                    "description": response.description,
315                    "content": {
316                        response.content_type.clone(): {
317                            "example": response.example
318                        }
319                    }
320                }));
321            }
322            operation.insert("responses".to_string(), json!(responses));
323            
324            path_item[endpoint.method.to_lowercase()] = json!(operation);
325        }
326        
327        json!({
328            "openapi": "3.0.0",
329            "info": {
330                "title": self.title,
331                "description": self.description,
332                "version": self.version
333            },
334            "servers": [{
335                "url": self.base_url
336            }],
337            "paths": paths
338        })
339    }
340
341    #[cfg(not(feature = "json"))]
342    pub fn generate_openapi(&self) -> String {
343        "OpenAPI generation requires 'json' feature".to_string()
344    }
345
346    /// Generate simple HTML documentation
347    pub fn generate_html_docs(&self) -> String {
348        let mut html = format!(
349            r#"<!DOCTYPE html>
350<html>
351<head>
352    <title>{} API Documentation</title>
353    <style>
354        body {{ font-family: Arial, sans-serif; margin: 40px; }}
355        .endpoint {{ margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }}
356        .method {{ display: inline-block; padding: 4px 8px; border-radius: 3px; color: white; font-weight: bold; }}
357        .get {{ background-color: #61affe; }}
358        .post {{ background-color: #49cc90; }}
359        .put {{ background-color: #fca130; }}
360        .delete {{ background-color: #f93e3e; }}
361        .deprecated {{ opacity: 0.6; }}
362        .parameter {{ margin: 10px 0; padding: 10px; background-color: #f8f9fa; border-radius: 3px; }}
363    </style>
364</head>
365<body>
366    <h1>{} API Documentation</h1>
367    <p>{}</p>
368    <p><strong>Version:</strong> {}</p>
369"#,
370            self.title, self.title, self.description, self.version
371        );
372
373        if !self.versions.is_empty() {
374            html.push_str("<h2>Available Versions</h2>");
375            for version in self.versions.values() {
376                let deprecated_class = if version.deprecated { " class=\"deprecated\"" } else { "" };
377                html.push_str(&format!(
378                    "<div{}><strong>v{}</strong> - {}</div>",
379                    deprecated_class, version.version, version.description
380                ));
381            }
382        }
383
384        html.push_str("<h2>Endpoints</h2>");
385        
386        for endpoint in &self.endpoints {
387            let deprecated_class = ""; // Deprecated field removed for simplicity
388            let method_class = endpoint.method.to_lowercase();
389            
390            html.push_str(&format!(
391                r#"<div class="endpoint{}">
392                    <h3><span class="method {}">{}</span> {}</h3>
393                    <p><strong>Summary:</strong> {}</p>
394                    <p>{}</p>
395"#,
396                deprecated_class, method_class, endpoint.method, endpoint.path,
397                endpoint.summary, endpoint.description
398            ));
399
400            if !endpoint.parameters.is_empty() {
401                html.push_str("<h4>Parameters</h4>");
402                for param in &endpoint.parameters {
403                    html.push_str(&format!(
404                        r#"<div class="parameter">
405                            <strong>{}</strong> ({:?}) - {}
406                            {}</div>"#,
407                        param.name,
408                        param.location,
409                        param.description,
410                        if param.required { " <em>(required)</em>" } else { "" }
411                    ));
412                }
413            }
414
415            if !endpoint.responses.is_empty() {
416                html.push_str("<h4>Responses</h4>");
417                for (status, response) in &endpoint.responses {
418                    html.push_str(&format!(
419                        "<div><strong>{}</strong> - {}</div>",
420                        status, response.description
421                    ));
422                }
423            }
424
425            html.push_str("</div>");
426        }
427
428        html.push_str("</body></html>");
429        html
430    }
431}
432
433/// API versioning middleware
434pub struct ApiVersioning {
435    default_version: String,
436    supported_versions: Vec<String>,
437    version_header: String,
438}
439
440impl ApiVersioning {
441    pub fn new(default_version: &str) -> Self {
442        Self {
443            default_version: default_version.to_string(),
444            supported_versions: vec![default_version.to_string()],
445            version_header: "API-Version".to_string(),
446        }
447    }
448
449    pub fn add_version(mut self, version: &str) -> Self {
450        self.supported_versions.push(version.to_string());
451        self
452    }
453
454    pub fn version_header(mut self, header: &str) -> Self {
455        self.version_header = header.to_string();
456        self
457    }
458
459    fn extract_version(&self, req: &Request) -> String {
460        // Try header first
461        if let Some(version) = req.header(&self.version_header) {
462            return version.to_string();
463        }
464
465        // Try query parameter
466        if let Some(version) = req.query("version") {
467            return version.to_string();
468        }
469
470        // Try path prefix (e.g., /v1/users)
471        let path = req.path();
472        if path.starts_with("/v") {
473            if let Some(version_part) = path.split('/').nth(1) {
474                if version_part.starts_with('v') {
475                    return version_part[1..].to_string();
476                }
477            }
478        }
479
480        self.default_version.clone()
481    }
482}
483
484impl crate::middleware::Middleware for ApiVersioning {
485    fn call(
486        &self,
487        req: Request,
488        next: Box<dyn Fn(Request) -> std::pin::Pin<Box<dyn std::future::Future<Output = Response> + Send + 'static>> + Send + Sync>,
489    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Response> + Send + 'static>> {
490        let version = self.extract_version(&req);
491        let supported_versions = self.supported_versions.clone();
492        let version_header = self.version_header.clone();
493
494        Box::pin(async move {
495            // Check if version is supported
496            if !supported_versions.contains(&version) {
497                return Response::bad_request()
498                    .json(&json!({
499                        "error": "Unsupported API version",
500                        "requested_version": version,
501                        "supported_versions": supported_versions
502                    }))
503                    .unwrap_or_else(|_| Response::bad_request().body("Unsupported API version"));
504            }
505
506            // Add version info to request context (would need to extend Request struct)
507            let mut response = next(req).await;
508            response = response.header(&version_header, &version);
509            response
510        })
511    }
512}
513
514/// Convenience methods for App to add documented endpoints
515impl App {
516    /// Add a documented GET endpoint
517    pub fn documented_get<H, T>(
518        self,
519        path: &str,
520        handler: H,
521        doc: EndpointDoc,
522    ) -> Self
523    where
524        H: Handler<T>,
525    {
526        // Store the documentation for later use in API doc generation
527        #[cfg(feature = "api")]
528        {
529            let mut app = self;
530            if let Some(ref mut api_docs) = app.api_docs {
531                let mut endpoint_doc = doc;
532                endpoint_doc.method = "GET".to_string();
533                endpoint_doc.path = path.to_string();
534                *api_docs = api_docs.clone().add_endpoint(endpoint_doc);
535            }
536            app.get(path, handler)
537        }
538
539        #[cfg(not(feature = "api"))]
540        {
541            let _ = doc; // Suppress unused warning
542            self.get(path, handler)
543        }
544    }
545
546    /// Add a documented POST endpoint
547    pub fn documented_post<H, T>(
548        self,
549        path: &str,
550        handler: H,
551        doc: EndpointDoc,
552    ) -> Self
553    where
554        H: Handler<T>,
555    {
556        // Store the documentation for later use in API doc generation
557        #[cfg(feature = "api")]
558        {
559            let mut app = self;
560            if let Some(ref mut api_docs) = app.api_docs {
561                let mut endpoint_doc = doc;
562                endpoint_doc.method = "POST".to_string();
563                endpoint_doc.path = path.to_string();
564                *api_docs = api_docs.clone().add_endpoint(endpoint_doc);
565            }
566            app.post(path, handler)
567        }
568
569        #[cfg(not(feature = "api"))]
570        {
571            let _ = doc; // Suppress unused warning
572            self.post(path, handler)
573        }
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580
581    #[test]
582    fn test_api_version() {
583        let version = ApiVersion::new("1.0", "Initial version");
584        assert_eq!(version.version, "1.0");
585        assert!(!version.deprecated);
586    }
587
588    #[test]
589    fn test_api_doc_builder() {
590        let builder = ApiDocBuilder::new("Test API", "1.0")
591            .description("A test API")
592            .base_url("https://api.example.com");
593        
594        assert_eq!(builder.title, "Test API");
595        assert_eq!(builder.version, "1.0");
596    }
597
598    #[cfg(feature = "json")]
599    #[test]
600    fn test_openapi_generation() {
601        let mut builder = ApiDocBuilder::new("Test API", "1.0");
602        
603        let endpoint = EndpointDoc {
604            method: "GET".to_string(),
605            path: "/users".to_string(),
606            summary: "Get users".to_string(),
607            description: "Retrieve all users".to_string(),
608            parameters: vec![],
609            responses: HashMap::new(),
610            tags: vec!["users".to_string()],
611            // deprecated field removed
612        };
613        
614        builder = builder.add_endpoint(endpoint);
615        let openapi = builder.generate_openapi();
616        
617        assert!(openapi["openapi"].as_str().unwrap().starts_with("3.0"));
618        assert_eq!(openapi["info"]["title"], "Test API");
619    }
620}