Skip to main content

rustapi_core/app/
openapi.rs

1#[cfg(feature = "swagger-ui")]
2use super::helpers::{check_basic_auth, unauthorized_response};
3use super::types::RustApi;
4
5impl RustApi {
6    pub fn register_schema<T: rustapi_openapi::schema::RustApiSchema>(mut self) -> Self {
7        self.openapi_spec = self.openapi_spec.register::<T>();
8        self
9    }
10
11    /// Configure OpenAPI info (title, version, description)
12    pub fn openapi_info(mut self, title: &str, version: &str, description: Option<&str>) -> Self {
13        // NOTE: Do not reset the spec here; doing so would drop collected paths/schemas.
14        // This is especially important for `RustApi::auto()` and `RustApi::config()`.
15        self.openapi_spec.info.title = title.to_string();
16        self.openapi_spec.info.version = version.to_string();
17        self.openapi_spec.info.description = description.map(|d| d.to_string());
18        self
19    }
20
21    /// Get the current OpenAPI spec (for advanced usage/testing).
22    pub fn openapi_spec(&self) -> &rustapi_openapi::OpenApiSpec {
23        &self.openapi_spec
24    }
25
26    /// If RUSTAPI_DUMP_OPENAPI=1 (or true), print the generated OpenAPI spec as JSON
27    /// to stdout and exit immediately. Used by `cargo rustapi mcp generate` to
28    /// extract the spec without needing a running HTTP server.
29    pub(super) fn maybe_dump_openapi(&self) {
30        if let Ok(val) = std::env::var("RUSTAPI_DUMP_OPENAPI") {
31            if matches!(val.as_str(), "1" | "true" | "yes") {
32                let json = self.openapi_spec.to_json();
33                // Print clean JSON only
34                if let Ok(pretty) = serde_json::to_string_pretty(&json) {
35                    println!("{}", pretty);
36                } else {
37                    println!("{}", json);
38                }
39                std::process::exit(0);
40            }
41        }
42    }
43    #[cfg(feature = "swagger-ui")]
44    pub fn docs(self, path: &str) -> Self {
45        let title = self.openapi_spec.info.title.clone();
46        let version = self.openapi_spec.info.version.clone();
47        let description = self.openapi_spec.info.description.clone();
48
49        self.docs_with_info(path, &title, &version, description.as_deref())
50    }
51
52    /// Enable Swagger UI documentation with custom API info
53    ///
54    /// # Example
55    ///
56    /// ```rust,ignore
57    /// RustApi::new()
58    ///     .docs_with_info("/docs", "My API", "2.0.0", Some("API for managing users"))
59    /// ```
60    #[cfg(feature = "swagger-ui")]
61    pub fn docs_with_info(
62        mut self,
63        path: &str,
64        title: &str,
65        version: &str,
66        description: Option<&str>,
67    ) -> Self {
68        use crate::router::get;
69        // Update spec info
70        self.openapi_spec.info.title = title.to_string();
71        self.openapi_spec.info.version = version.to_string();
72        if let Some(desc) = description {
73            self.openapi_spec.info.description = Some(desc.to_string());
74        }
75
76        let path = path.trim_end_matches('/');
77        let openapi_path = format!("{}/openapi.json", path);
78
79        // Clone values for closures
80        let spec_value = self.openapi_spec.to_json();
81        let spec_json = serde_json::to_string_pretty(&spec_value).unwrap_or_else(|e| {
82            // Safe fallback if JSON serialization fails (though unlikely for Value)
83            tracing::error!("Failed to serialize OpenAPI spec: {}", e);
84            "{}".to_string()
85        });
86        let openapi_url = openapi_path.clone();
87
88        // Add OpenAPI JSON endpoint
89        let spec_handler = move || {
90            let json = spec_json.clone();
91            async move {
92                http::Response::builder()
93                    .status(http::StatusCode::OK)
94                    .header(http::header::CONTENT_TYPE, "application/json")
95                    .body(crate::response::Body::from(json))
96                    .unwrap_or_else(|e| {
97                        tracing::error!("Failed to build response: {}", e);
98                        http::Response::builder()
99                            .status(http::StatusCode::INTERNAL_SERVER_ERROR)
100                            .body(crate::response::Body::from("Internal Server Error"))
101                            .unwrap()
102                    })
103            }
104        };
105
106        // Add Swagger UI endpoint
107        let docs_handler = move || {
108            let url = openapi_url.clone();
109            async move {
110                let response = rustapi_openapi::swagger_ui_html(&url);
111                response.map(crate::response::Body::Full)
112            }
113        };
114
115        self.route(&openapi_path, get(spec_handler))
116            .route(path, get(docs_handler))
117    }
118
119    /// Enable Swagger UI documentation with Basic Auth protection
120    ///
121    /// When username and password are provided, the docs endpoint will require
122    /// Basic Authentication. This is useful for protecting API documentation
123    /// in production environments.
124    ///
125    /// # Example
126    ///
127    /// ```rust,ignore
128    /// RustApi::new()
129    ///     .route("/users", get(list_users))
130    ///     .docs_with_auth("/docs", "admin", "secret123")
131    ///     .run("127.0.0.1:8080")
132    ///     .await
133    /// ```
134    #[cfg(feature = "swagger-ui")]
135    pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
136        let title = self.openapi_spec.info.title.clone();
137        let version = self.openapi_spec.info.version.clone();
138        let description = self.openapi_spec.info.description.clone();
139
140        self.docs_with_auth_and_info(
141            path,
142            username,
143            password,
144            &title,
145            &version,
146            description.as_deref(),
147        )
148    }
149
150    /// Enable Swagger UI documentation with Basic Auth and custom API info
151    ///
152    /// # Example
153    ///
154    /// ```rust,ignore
155    /// RustApi::new()
156    ///     .docs_with_auth_and_info(
157    ///         "/docs",
158    ///         "admin",
159    ///         "secret",
160    ///         "My API",
161    ///         "2.0.0",
162    ///         Some("Protected API documentation")
163    ///     )
164    /// ```
165    #[cfg(feature = "swagger-ui")]
166    pub fn docs_with_auth_and_info(
167        mut self,
168        path: &str,
169        username: &str,
170        password: &str,
171        title: &str,
172        version: &str,
173        description: Option<&str>,
174    ) -> Self {
175        use crate::router::MethodRouter;
176        use std::collections::HashMap;
177
178        #[inline]
179        fn base64_encode(input: &[u8]) -> String {
180            const ALPHA: &[u8; 64] =
181                b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
182            let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
183            for chunk in input.chunks(3) {
184                let b0 = chunk[0] as usize;
185                let b1 = if chunk.len() > 1 {
186                    chunk[1] as usize
187                } else {
188                    0
189                };
190                let b2 = if chunk.len() > 2 {
191                    chunk[2] as usize
192                } else {
193                    0
194                };
195                out.push(ALPHA[b0 >> 2] as char);
196                out.push(ALPHA[((b0 & 3) << 4) | (b1 >> 4)] as char);
197                out.push(if chunk.len() > 1 {
198                    ALPHA[((b1 & 0xf) << 2) | (b2 >> 6)] as char
199                } else {
200                    '='
201                });
202                out.push(if chunk.len() > 2 {
203                    ALPHA[b2 & 63] as char
204                } else {
205                    '='
206                });
207            }
208            out
209        }
210
211        // Update spec info
212        self.openapi_spec.info.title = title.to_string();
213        self.openapi_spec.info.version = version.to_string();
214        if let Some(desc) = description {
215            self.openapi_spec.info.description = Some(desc.to_string());
216        }
217
218        let path = path.trim_end_matches('/');
219        let openapi_path = format!("{}/openapi.json", path);
220
221        // Create expected auth header value
222        let credentials = format!("{}:{}", username, password);
223        let encoded = base64_encode(credentials.as_bytes());
224        let expected_auth = format!("Basic {}", encoded);
225
226        // Clone values for closures
227        let spec_value = self.openapi_spec.to_json();
228        let spec_json = serde_json::to_string_pretty(&spec_value).unwrap_or_else(|e| {
229            tracing::error!("Failed to serialize OpenAPI spec: {}", e);
230            "{}".to_string()
231        });
232        let openapi_url = openapi_path.clone();
233        let expected_auth_spec = expected_auth.clone();
234        let expected_auth_docs = expected_auth;
235
236        // Create spec handler with auth check
237        let spec_handler: crate::handler::BoxedHandler =
238            std::sync::Arc::new(move |req: crate::Request| {
239                let json = spec_json.clone();
240                let expected = expected_auth_spec.clone();
241                Box::pin(async move {
242                    if !check_basic_auth(&req, &expected) {
243                        return unauthorized_response();
244                    }
245                    http::Response::builder()
246                        .status(http::StatusCode::OK)
247                        .header(http::header::CONTENT_TYPE, "application/json")
248                        .body(crate::response::Body::from(json))
249                        .unwrap_or_else(|e| {
250                            tracing::error!("Failed to build response: {}", e);
251                            http::Response::builder()
252                                .status(http::StatusCode::INTERNAL_SERVER_ERROR)
253                                .body(crate::response::Body::from("Internal Server Error"))
254                                .unwrap()
255                        })
256                })
257                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
258            });
259
260        // Create docs handler with auth check
261        let docs_handler: crate::handler::BoxedHandler =
262            std::sync::Arc::new(move |req: crate::Request| {
263                let url = openapi_url.clone();
264                let expected = expected_auth_docs.clone();
265                Box::pin(async move {
266                    if !check_basic_auth(&req, &expected) {
267                        return unauthorized_response();
268                    }
269                    let response = rustapi_openapi::swagger_ui_html(&url);
270                    response.map(crate::response::Body::Full)
271                })
272                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
273            });
274
275        // Create method routers with boxed handlers
276        let mut spec_handlers = HashMap::new();
277        spec_handlers.insert(http::Method::GET, spec_handler);
278        let spec_router = MethodRouter::from_boxed(spec_handlers);
279
280        let mut docs_handlers = HashMap::new();
281        docs_handlers.insert(http::Method::GET, docs_handler);
282        let docs_router = MethodRouter::from_boxed(docs_handlers);
283
284        self.route(&openapi_path, spec_router)
285            .route(path, docs_router)
286    }
287}