1use rust_embed::RustEmbed;
2use serde_json::Value;
3
4#[derive(RustEmbed)]
6#[folder = "ui/"]
7struct Assets;
8
9pub fn get_asset(path: &str) -> Option<Vec<u8>> {
11 Assets::get(path).map(|d| d.data.into())
12}
13
14pub fn get_asset_with_mime(path: &str) -> Option<(String, Vec<u8>)> {
16 let asset = Assets::get(path)?;
17 let mime_type = get_mime_type(path);
18 Some((mime_type, asset.data.into()))
19}
20
21pub fn get_mime_type(path: &str) -> String {
23 match path.split('.').next_back() {
24 Some("html") => "text/html".to_string(),
25 Some("js") => "application/javascript".to_string(),
26 Some("css") => "text/css".to_string(),
27 Some("json") => "application/json".to_string(),
28 Some("png") => "image/png".to_string(),
29 Some("svg") => "image/svg+xml".to_string(),
30 Some("ico") => "image/x-icon".to_string(),
31 _ => "application/octet-stream".to_string(),
32 }
33}
34
35pub fn render_scalar(config_json: &str, js_bundle_url: Option<&str>) -> String {
37 let html_template = include_str!("../ui/index.html");
38 let js_url = js_bundle_url.unwrap_or("https://cdn.jsdelivr.net/npm/@scalar/api-reference");
39 html_template
40 .replace("__CONFIGURATION__", config_json)
41 .replace("__JS_BUNDLE_URL__", js_url)
42}
43
44pub fn scalar_html(config: &Value, js_bundle_url: Option<&str>) -> String {
46 render_scalar(&config.to_string(), js_bundle_url)
47}
48
49pub fn scalar_html_default(config: &Value) -> String {
51 scalar_html(config, None)
52}
53
54pub fn scalar_html_from_json(
56 config_json: &str,
57 js_bundle_url: Option<&str>,
58) -> Result<String, serde_json::Error> {
59 let config: Value = serde_json::from_str(config_json)?;
60 Ok(scalar_html(&config, js_bundle_url))
61}
62
63pub fn scalar_html_from_json_default(config_json: &str) -> Result<String, serde_json::Error> {
65 scalar_html_from_json(config_json, None)
66}
67
68#[cfg(feature = "axum")]
69pub mod axum {
70 use super::{get_asset_with_mime, scalar_html};
71 use axum::{
72 body::Body, http::StatusCode, response::Html, response::Response, routing::get, Router,
73 };
74 use serde_json::Value;
75
76 pub fn scalar_response(config: &Value, js_bundle_url: Option<&str>) -> Html<String> {
78 Html(scalar_html(config, js_bundle_url))
79 }
80
81 pub fn scalar_response_from_json(
83 config_json: &str,
84 js_bundle_url: Option<&str>,
85 ) -> Result<Html<String>, serde_json::Error> {
86 let config: Value = serde_json::from_str(config_json)?;
87 Ok(scalar_response(&config, js_bundle_url))
88 }
89
90 pub fn router(path: &str, config: &Value) -> Router {
92 let js_path = format!("{}/scalar.js", path);
93 let config_clone = config.clone();
94 let js_path_clone = js_path.clone();
95
96 Router::new()
97 .route(
98 path,
99 get(move || {
100 let config = config_clone.clone();
101 let js_path = js_path_clone.clone();
102 async move { scalar_response(&config, Some(&js_path)) }
103 }),
104 )
105 .route(
106 &js_path,
107 get(|| async {
108 match get_asset_with_mime("scalar.js") {
109 Some((mime_type, content)) => Response::builder()
110 .status(StatusCode::OK)
111 .header("content-type", mime_type)
112 .body(Body::from(content))
113 .unwrap(),
114 None => Response::builder()
115 .status(StatusCode::NOT_FOUND)
116 .body(Body::from("Not found"))
117 .unwrap(),
118 }
119 }),
120 )
121 }
122
123 pub fn routes(path: &str, config: &Value) -> (Router, Router) {
125 let js_path = format!("{}/scalar.js", path);
126 let config_clone = config.clone();
127 let js_path_clone = js_path.clone();
128
129 let scalar_route = Router::new().route(
130 path,
131 get(move || {
132 let config = config_clone.clone();
133 let js_path = js_path_clone.clone();
134 async move { scalar_response(&config, Some(&js_path)) }
135 }),
136 );
137
138 let asset_route = Router::new().route(
139 &js_path,
140 get(|| async {
141 match get_asset_with_mime("scalar.js") {
142 Some((mime_type, content)) => Response::builder()
143 .status(StatusCode::OK)
144 .header("content-type", mime_type)
145 .body(Body::from(content))
146 .unwrap(),
147 None => Response::builder()
148 .status(StatusCode::NOT_FOUND)
149 .body(Body::from("Not found"))
150 .unwrap(),
151 }
152 }),
153 );
154
155 (scalar_route, asset_route)
156 }
157}
158
159#[cfg(feature = "actix-web")]
160pub mod actix_web {
161 use super::{get_asset_with_mime, scalar_html};
162 use actix_web::web::ServiceConfig;
163 use actix_web::{http::header::ContentType, web, HttpResponse, Result};
164 use serde_json::Value;
165
166 pub fn scalar_response(config: &Value, js_bundle_url: Option<&str>) -> HttpResponse {
168 HttpResponse::Ok()
169 .content_type(ContentType::html())
170 .body(scalar_html(config, js_bundle_url))
171 }
172
173 pub fn scalar_response_from_json(
175 config_json: &str,
176 js_bundle_url: Option<&str>,
177 ) -> Result<HttpResponse, serde_json::Error> {
178 let config: Value = serde_json::from_str(config_json)?;
179 Ok(scalar_response(&config, js_bundle_url))
180 }
181
182 pub fn config(path: &str, config: &Value) -> impl Fn(&mut ServiceConfig) {
184 let scalar_path = path.to_string();
185 let js_path = format!("{}/scalar.js", path);
186 let config_clone = config.clone();
187 let js_path_clone = js_path.clone();
188
189 move |cfg: &mut ServiceConfig| {
190 let scalar_path = scalar_path.clone();
191 let js_path = js_path_clone.clone();
192 let config = config_clone.clone();
193 let js_path_for_asset = js_path.clone();
194
195 cfg.route(
196 &scalar_path,
197 web::get().to(move || {
198 let config = config.clone();
199 let js_path = js_path.clone();
200 async move { scalar_response(&config, Some(&js_path)) }
201 }),
202 )
203 .route(
204 &js_path_for_asset,
205 web::get().to(|| async {
206 match get_asset_with_mime("scalar.js") {
207 Some((mime_type, content)) => {
208 HttpResponse::Ok().content_type(mime_type).body(content)
209 }
210 None => HttpResponse::NotFound().body("Not found"),
211 }
212 }),
213 );
214 }
215 }
216}
217
218#[cfg(feature = "warp")]
219pub mod warp {
220 use super::{get_asset_with_mime, scalar_html};
221 use serde_json::Value;
222 use warp::{reply::html, Filter, Reply};
223
224 pub fn scalar_reply(config: &Value, js_bundle_url: Option<&str>) -> impl warp::Reply {
226 html(scalar_html(config, js_bundle_url))
227 }
228
229 pub fn scalar_reply_from_json(
231 config_json: &str,
232 js_bundle_url: Option<&str>,
233 ) -> Result<impl warp::Reply, serde_json::Error> {
234 let config: Value = serde_json::from_str(config_json)?;
235 Ok(scalar_reply(&config, js_bundle_url))
236 }
237
238 pub fn routes(
241 path: &'static str,
242 config: &Value,
243 ) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
244 let config_clone = config.clone();
245
246 let clean_path = path.trim_start_matches('/');
248
249 let asset_route = create_asset_route(clean_path);
251
252 let scalar_route = warp::path(clean_path).and(warp::path::end()).map(move || {
254 let config = config_clone.clone();
255 let js_path = format!("{}/scalar.js", clean_path);
256 scalar_reply(&config, Some(&js_path))
257 });
258
259 asset_route.or(scalar_route)
261 }
262
263 fn create_asset_route(
265 path: &'static str,
266 ) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
267 warp::path(path)
269 .and(warp::path("scalar.js"))
270 .and_then(move || async move {
271 match get_asset_with_mime("scalar.js") {
272 Some((mime_type, content)) => {
273 Ok::<_, warp::Rejection>(warp::reply::with_header(
274 warp::reply::with_status(content, warp::http::StatusCode::OK),
275 "content-type",
276 mime_type,
277 ))
278 }
279 None => Ok::<_, warp::Rejection>(warp::reply::with_header(
280 warp::reply::with_status(
281 Vec::<u8>::new(),
282 warp::http::StatusCode::NOT_FOUND,
283 ),
284 "content-type",
285 "text/plain",
286 )),
287 }
288 })
289 }
290
291 pub fn separate_routes(
294 path: &'static str,
295 config: &Value,
296 ) -> (
297 impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone,
298 impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone,
299 ) {
300 let config_clone = config.clone();
301
302 let clean_path = path.trim_start_matches('/');
304
305 let asset_route = warp::path(clean_path)
307 .and(warp::path("scalar.js"))
308 .and_then(move || async move {
309 match get_asset_with_mime("scalar.js") {
310 Some((mime_type, content)) => {
311 Ok::<_, warp::Rejection>(warp::reply::with_header(
312 warp::reply::with_status(content, warp::http::StatusCode::OK),
313 "content-type",
314 mime_type,
315 ))
316 }
317 None => Ok::<_, warp::Rejection>(warp::reply::with_header(
318 warp::reply::with_status(
319 Vec::<u8>::new(),
320 warp::http::StatusCode::NOT_FOUND,
321 ),
322 "content-type",
323 "text/plain",
324 )),
325 }
326 });
327
328 let scalar_route = warp::path(clean_path).and(warp::path::end()).map(move || {
330 let config = config_clone.clone();
331 let js_path = format!("{}/scalar.js", clean_path);
332 scalar_reply(&config, Some(&js_path))
333 });
334
335 (scalar_route, asset_route)
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use crate::{
342 get_asset, get_asset_with_mime, get_mime_type, scalar_html, scalar_html_default,
343 scalar_html_from_json, scalar_html_from_json_default,
344 };
345 use serde_json::json;
346
347 #[test]
348 fn test_scalar_html_generation() {
349 let config = json!({
350 "url": "/openapi.json",
351 "theme": "purple"
352 });
353
354 let html1 = scalar_html(&config, Some("/custom-scalar.js"));
356 assert!(html1.contains("/openapi.json"));
357 assert!(html1.contains("purple"));
358 assert!(html1.contains("/custom-scalar.js"));
359 assert!(html1.contains("<html"));
360 assert!(html1.contains("</html>"));
361
362 let html2 = scalar_html(&config, None);
364 assert!(html2.contains("/openapi.json"));
365 assert!(html2.contains("purple"));
366 assert!(html2.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
367 assert!(html2.contains("<html"));
368 assert!(html2.contains("</html>"));
369 }
370
371 #[test]
372 fn test_scalar_html_from_json() {
373 let config_json = r#"{"url": "/api.json", "theme": "purple"}"#;
374
375 let html1 = scalar_html_from_json(config_json, Some("/bundle.js")).unwrap();
377 assert!(html1.contains("/api.json"));
378 assert!(html1.contains("purple"));
379 assert!(html1.contains("/bundle.js"));
380
381 let html2 = scalar_html_from_json(config_json, None).unwrap();
383 assert!(html2.contains("/api.json"));
384 assert!(html2.contains("purple"));
385 assert!(html2.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
386 }
387
388 #[test]
389 fn test_convenience_functions() {
390 let config = json!({
391 "url": "/test.json",
392 "theme": "kepler"
393 });
394
395 let html1 = scalar_html_default(&config);
397 assert!(html1.contains("/test.json"));
398 assert!(html1.contains("kepler"));
399 assert!(html1.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
400
401 let config_json = r#"{"url": "/test2.json", "theme": "purple"}"#;
403 let html2 = scalar_html_from_json_default(config_json).unwrap();
404 assert!(html2.contains("/test2.json"));
405 assert!(html2.contains("purple"));
406 assert!(html2.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
407 }
408
409 #[test]
410 fn test_get_asset() {
411 let html_asset = get_asset("index.html");
413 assert!(html_asset.is_some());
414
415 let js_asset = get_asset("scalar.js");
417 assert!(js_asset.is_some());
418
419 let non_existent = get_asset("non-existent.txt");
421 assert!(non_existent.is_none());
422 }
423
424 #[test]
425 fn test_get_asset_with_mime() {
426 let (mime_type, content) = get_asset_with_mime("index.html").unwrap();
427 assert_eq!(mime_type, "text/html");
428 assert!(!content.is_empty());
429
430 let (mime_type, content) = get_asset_with_mime("scalar.js").unwrap();
431 assert_eq!(mime_type, "application/javascript");
432 assert!(!content.is_empty());
433 }
434
435 #[test]
436 fn test_mime_type_detection() {
437 assert_eq!(get_mime_type("index.html"), "text/html");
438 assert_eq!(get_mime_type("style.css"), "text/css");
439 assert_eq!(get_mime_type("script.js"), "application/javascript");
440 assert_eq!(get_mime_type("data.json"), "application/json");
441 assert_eq!(get_mime_type("image.png"), "image/png");
442 assert_eq!(get_mime_type("icon.svg"), "image/svg+xml");
443 assert_eq!(get_mime_type("favicon.ico"), "image/x-icon");
444 assert_eq!(get_mime_type("unknown.xyz"), "application/octet-stream");
445 }
446
447 #[test]
448 fn test_error_handling() {
449 let invalid_json = r#"{"url": "/api.json", "theme": "purple""#; let result = scalar_html_from_json(invalid_json, None);
452 assert!(result.is_err());
453
454 let empty_json = r#"{}"#;
456 let result = scalar_html_from_json(empty_json, None);
457 assert!(result.is_ok());
458 let html = result.unwrap();
459 assert!(html.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
460 }
461
462 #[test]
463 fn test_edge_cases() {
464 let empty_config = json!({});
466 let html = scalar_html(&empty_config, None);
467 assert!(html.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
468
469 let special_config = json!({
471 "url": "/api/v1/test?param=value&other=test",
472 "theme": "purple",
473 "description": "API with special chars: <>&\"'"
474 });
475 let html = scalar_html(&special_config, None);
476 assert!(html.contains("/api/v1/test?param=value&other=test"));
477 assert!(html.contains("purple"));
478
479 let config_with_special_path = json!({
481 "url": "/api/test",
482 "theme": "purple"
483 });
484 let html = scalar_html(&config_with_special_path, Some("/custom/path/scalar.js"));
485 assert!(html.contains("/custom/path/scalar.js"));
486 }
487}
488
489#[cfg(all(test, feature = "axum"))]
490mod axum_tests {
491 use crate::axum::{router, routes, scalar_response, scalar_response_from_json};
492 use serde_json::json;
493
494 #[test]
495 fn test_scalar_response() {
496 let config = json!({
497 "url": "/openapi.json",
498 "theme": "purple"
499 });
500
501 let response = scalar_response(&config, Some("/custom-scalar.js"));
503 let html = response.0;
504 assert!(html.contains("/openapi.json"));
505 assert!(html.contains("purple"));
506 assert!(html.contains("/custom-scalar.js"));
507
508 let response = scalar_response(&config, None);
510 let html = response.0;
511 assert!(html.contains("/openapi.json"));
512 assert!(html.contains("purple"));
513 assert!(html.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
514 }
515
516 #[test]
517 fn test_scalar_response_from_json() {
518 let config_json = r#"{"url": "/api.json", "theme": "purple"}"#;
519
520 let response = scalar_response_from_json(config_json, Some("/bundle.js")).unwrap();
522 let html = response.0;
523 assert!(html.contains("/api.json"));
524 assert!(html.contains("purple"));
525 assert!(html.contains("/bundle.js"));
526
527 let response = scalar_response_from_json(config_json, None).unwrap();
529 let html = response.0;
530 assert!(html.contains("/api.json"));
531 assert!(html.contains("purple"));
532 assert!(html.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
533
534 let invalid_json = r#"{"url": "/api.json", "theme": "purple""#;
536 let result = scalar_response_from_json(invalid_json, None);
537 assert!(result.is_err());
538 }
539
540 #[test]
541 fn test_router_creation() {
542 let config = json!({
543 "url": "/openapi.json",
544 "theme": "purple"
545 });
546
547 let _app = router("/scalar", &config);
549 }
551
552 #[test]
553 fn test_routes_creation() {
554 let config = json!({
555 "url": "/openapi.json",
556 "theme": "purple"
557 });
558
559 let (_scalar_route, _asset_route) = routes("/scalar", &config);
561 }
564}
565
566#[cfg(all(test, feature = "actix-web"))]
567mod actix_tests {
568 use crate::actix_web::{config, scalar_response, scalar_response_from_json};
569 use serde_json::json;
570
571 #[test]
572 fn test_scalar_response() {
573 let config = json!({
574 "url": "/openapi.json",
575 "theme": "purple"
576 });
577
578 let _response = scalar_response(&config, Some("/custom-scalar.js"));
580 let _response = scalar_response(&config, None);
584 }
586
587 #[test]
588 fn test_scalar_response_from_json() {
589 let config_json = r#"{"url": "/api.json", "theme": "purple"}"#;
590
591 let _response = scalar_response_from_json(config_json, Some("/bundle.js")).unwrap();
593 let _response = scalar_response_from_json(config_json, None).unwrap();
597 let invalid_json = r#"{"url": "/api.json", "theme": "purple""#;
601 let result = scalar_response_from_json(invalid_json, None);
602 assert!(result.is_err());
603 }
604
605 #[test]
606 fn test_config_creation() {
607 let config_json = json!({
608 "url": "/openapi.json",
609 "theme": "purple"
610 });
611
612 let _config_fn = config("/scalar", &config_json);
614 }
616}
617
618#[cfg(all(test, feature = "warp"))]
619mod warp_tests {
620 use crate::warp::{routes, scalar_reply, scalar_reply_from_json, separate_routes};
621 use serde_json::json;
622
623 #[test]
624 fn test_scalar_reply() {
625 let config = json!({
626 "url": "/openapi.json",
627 "theme": "purple"
628 });
629
630 let _reply = scalar_reply(&config, Some("/custom-scalar.js"));
632 let _reply = scalar_reply(&config, None);
637 }
639
640 #[test]
641 fn test_scalar_reply_from_json() {
642 let config_json = r#"{"url": "/api.json", "theme": "purple"}"#;
643
644 let _reply = scalar_reply_from_json(config_json, Some("/bundle.js")).unwrap();
646 let _reply = scalar_reply_from_json(config_json, None).unwrap();
650 let invalid_json = r#"{"url": "/api.json", "theme": "purple""#;
654 let result = scalar_reply_from_json(invalid_json, None);
655 assert!(result.is_err());
656 }
657
658 #[test]
659 fn test_routes_creation() {
660 let config = json!({
661 "url": "/openapi.json",
662 "theme": "purple"
663 });
664
665 let _filter = routes("scalar", &config);
667 }
669
670 #[test]
671 fn test_separate_routes_creation() {
672 let config = json!({
673 "url": "/openapi.json",
674 "theme": "purple"
675 });
676
677 let (_scalar_filter, _asset_filter) = separate_routes("scalar", &config);
679 }
681}