diff --git a/Cargo.lock b/Cargo.lock index 8c7c315..3d0fa2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,7 +426,9 @@ dependencies = [ "libconductor", "libconfig", "log", + "mime_guess", "pretty_env_logger", + "rust-embed", "serde", "serde_json", "sqlx", @@ -1147,6 +1149,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1504,6 +1516,40 @@ dependencies = [ "serde", ] +[[package]] +name = "rust-embed" +version = "6.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "283ffe2f866869428c92e0d61c2f35dfb4355293cdfdc48f49e895c15f1333d1" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ab23d42d71fb9be1b643fe6765d292c5e14d46912d13f3ae2815ca048ea04d" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1669d81dfabd1b5f8e2856b8bbe146c6192b0ba22162edc738ac0a5de18f054" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.18.0" @@ -1564,6 +1610,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -2000,6 +2055,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.8" @@ -2057,6 +2121,17 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index e51af97..d606f73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ url = { version = "2.2.2", features = ["serde"]} serde_json = { version ="1", features = ["raw_value"]} clap = { vesrion = "3.2.20", features = ["derive"]} actix-web-httpauth = "0.8.0" +mime_guess = "2.0.4" +rust-embed = "6.4.2" [dependencies.libconductor] path = "./env/libconductor" diff --git a/Makefile b/Makefile index 5b553a1..109ab66 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ release: ## Build app with release optimizations cargo build --release run: ## Run app in debug mode - cargo run + cargo run -- serve #sqlx-offline-data: ## prepare sqlx offline data diff --git a/src/docs.rs b/src/docs.rs new file mode 100644 index 0000000..26b0bf7 --- /dev/null +++ b/src/docs.rs @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::borrow::Cow; + +use actix_web::body::BoxBody; +use actix_web::{http::header, web, HttpResponse, Responder}; +use mime_guess::from_path; +use rust_embed::RustEmbed; + +use crate::CACHE_AGE; + +pub const DOCS: routes::Docs = routes::Docs::new(); + +pub mod routes { + pub struct Docs { + pub home: &'static str, + pub spec: &'static str, + pub assets: &'static str, + } + + impl Docs { + pub const fn new() -> Self { + Docs { + home: "/docs/openapi", + spec: "/docs/openapi/openapi.yml", + assets: "/docs/openapi/{_:.*}", + } + } + } +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(index).service(spec).service(dist); +} + +#[derive(RustEmbed)] +#[folder = "docs/openapi/"] +struct Asset; + +pub fn handle_embedded_file(path: &str) -> HttpResponse { + match Asset::get(path) { + Some(content) => { + let body: BoxBody = match content.data { + Cow::Borrowed(bytes) => BoxBody::new(bytes), + Cow::Owned(bytes) => BoxBody::new(bytes), + }; + + HttpResponse::Ok() + .insert_header(header::CacheControl(vec![ + header::CacheDirective::Public, + header::CacheDirective::Extension("immutable".into(), None), + header::CacheDirective::MaxAge(CACHE_AGE), + ])) + .content_type(from_path(path).first_or_octet_stream().as_ref()) + .body(body) + } + None => HttpResponse::NotFound().body("404 Not Found"), + } +} + +#[actix_web_codegen_const_routes::get(path = "DOCS.assets")] +async fn dist(path: web::Path) -> impl Responder { + handle_embedded_file(&path) +} +const OPEN_API_SPEC: &str = include_str!("../docs/openapi/openapi.yml"); + +#[actix_web_codegen_const_routes::get(path = "DOCS.spec")] +async fn spec() -> HttpResponse { + HttpResponse::Ok() + .content_type("text/yaml") + .body(OPEN_API_SPEC) +} + +#[actix_web_codegen_const_routes::get(path = "DOCS.home")] +async fn index() -> HttpResponse { + handle_embedded_file("index.html") +} + +#[cfg(test)] +mod tests { + use actix_web::http::StatusCode; + use actix_web::test; + + use super::*; + use crate::*; + + #[actix_rt::test] + async fn docs_works() { + const FILE: &str = "openapi.yml"; + + let app = test::init_service( + App::new() + .wrap(actix_web::middleware::NormalizePath::new( + actix_web::middleware::TrailingSlash::Trim, + )) + .configure(services), + ) + .await; + + let resp = test::call_service( + &app, + test::TestRequest::get().uri(DOCS.home).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + let resp = test::call_service( + &app, + test::TestRequest::get().uri(DOCS.spec).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + let uri = format!("{}/{}", DOCS.home, "favicon-32x32.png"); + println!("{uri}"); + + let resp = + test::call_service(&app, test::TestRequest::get().uri(&uri).to_request()) + .await; + assert_eq!(resp.status(), StatusCode::OK); + } +} diff --git a/src/main.rs b/src/main.rs index e2179a3..ddecbc0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,7 @@ use lazy_static::lazy_static; mod api; mod ctx; -//mod docs; +mod docs; #[cfg(not(tarpaulin_include))] mod errors; //#[macro_use] diff --git a/src/routes.rs b/src/routes.rs index 7920a1d..3aba8d5 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -17,5 +17,6 @@ use actix_web::web; pub fn services(cfg: &mut web::ServiceConfig) { + crate::docs::services(cfg); crate::api::v1::services(cfg); }