1use std::collections::HashMap;
142use crate::{Request, Response, App, Handler};
143
144#[cfg(feature = "json")]
145use serde_json::{json, Value};
146
147#[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#[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#[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#[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#[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 #[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 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 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 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 = ""; 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
433pub 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 if let Some(version) = req.header(&self.version_header) {
462 return version.to_string();
463 }
464
465 if let Some(version) = req.query("version") {
467 return version.to_string();
468 }
469
470 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 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 let mut response = next(req).await;
508 response = response.header(&version_header, &version);
509 response
510 })
511 }
512}
513
514impl App {
516 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 #[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; self.get(path, handler)
543 }
544 }
545
546 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 #[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; 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 };
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}