rustapi_core/app/
openapi.rs1#[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 pub fn openapi_info(mut self, title: &str, version: &str, description: Option<&str>) -> Self {
13 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 pub fn openapi_spec(&self) -> &rustapi_openapi::OpenApiSpec {
23 &self.openapi_spec
24 }
25
26 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 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 #[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 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 let spec_value = self.openapi_spec.to_json();
81 let spec_json = serde_json::to_string_pretty(&spec_value).unwrap_or_else(|e| {
82 tracing::error!("Failed to serialize OpenAPI spec: {}", e);
84 "{}".to_string()
85 });
86 let openapi_url = openapi_path.clone();
87
88 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 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 #[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 #[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 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 let credentials = format!("{}:{}", username, password);
223 let encoded = base64_encode(credentials.as_bytes());
224 let expected_auth = format!("Basic {}", encoded);
225
226 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 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 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 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}