diff --git a/utoipa-swagger-ui/Cargo.toml b/utoipa-swagger-ui/Cargo.toml index 098da77f..995d97e4 100644 --- a/utoipa-swagger-ui/Cargo.toml +++ b/utoipa-swagger-ui/Cargo.toml @@ -34,3 +34,4 @@ rustdoc-args = ["--cfg", "doc_cfg"] [build-dependencies] zip = { version = "0.6", default-features = false, features = ["deflate"] } regex = "1.7" +reqwest = { version = "0.11", features = ["blocking"] } diff --git a/utoipa-swagger-ui/README.md b/utoipa-swagger-ui/README.md index f4094239..0b6a742e 100644 --- a/utoipa-swagger-ui/README.md +++ b/utoipa-swagger-ui/README.md @@ -20,7 +20,7 @@ other frameworks as well. With other frameworks, there is a bit more manual impl more details at [serve](https://docs.rs/utoipa-swagger-ui/latest/utoipa_swagger_ui/fn.serve.html) or [examples](https://github.com/juhaku/utoipa/tree/master/examples). -# Crate Features +## Crate Features * **actix-web** Enables actix-web integration with pre-configured SwaggerUI service factory allowing users to use the Swagger UI without a hassle. @@ -31,15 +31,17 @@ more details at [serve](https://docs.rs/utoipa-swagger-ui/latest/utoipa_swagger_ * **debug-embed** Enables `debug-embed` feature on `rust_embed` crate to allow embedding files in debug builds as well. -# Install +## Install Use only the raw types without any boilerplate implementation. + ```toml [dependencies] utoipa-swagger-ui = "6" ``` Enable actix-web framework with Swagger UI you could define the dependency as follows. + ```toml [dependencies] utoipa-swagger-ui = { version = "6", features = ["actix-web"] } @@ -47,9 +49,25 @@ utoipa-swagger-ui = { version = "6", features = ["actix-web"] } **Note!** Also remember that you already have defined `utoipa` dependency in your `Cargo.toml` -# Examples +## Config + +The following configuration env variables are available at build time: + +* `SWAGGER_UI_DOWNLOAD_URL`: + + * the url from where to download the swagger-ui zip file + * default value: + * All versions: + +* `SWAGGER_UI_OVERWRITE_FOLDER`: + + * absolute path to a folder containing files to overwrite the default swagger-ui files + * typically you might want to overwrite `index.html` + +## Examples Serve Swagger UI with api doc via **`actix-web`**. See full example from [examples](https://github.com/juhaku/utoipa/tree/master/examples/todo-actix). + ```rust HttpServer::new(move || { App::new() @@ -63,6 +81,7 @@ HttpServer::new(move || { ``` Serve Swagger UI with api doc via **`rocket`**. See full example from [examples](https://github.com/juhaku/utoipa/tree/master/examples/rocket-todo). + ```rust #[rocket::launch] fn rocket() -> Rocket { @@ -77,13 +96,14 @@ fn rocket() -> Rocket { Setup Router to serve Swagger UI with **`axum`** framework. See full implementation of how to serve Swagger UI with axum from [examples](https://github.com/juhaku/utoipa/tree/master/examples/todo-axum). + ```rust let app = Router::new() .merge(SwaggerUi::new("/swagger-ui") .url("/api-docs/openapi.json", ApiDoc::openapi())); ``` -# License +## License Licensed under either of [Apache 2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT) license at your option. diff --git a/utoipa-swagger-ui/build.rs b/utoipa-swagger-ui/build.rs index 19af11f1..ccb30b0b 100644 --- a/utoipa-swagger-ui/build.rs +++ b/utoipa-swagger-ui/build.rs @@ -1,6 +1,6 @@ use std::{ - cmp::Ordering, env, + error::Error, fs::{self, File}, io, path::PathBuf, @@ -9,46 +9,94 @@ use std::{ use regex::Regex; use zip::{result::ZipError, ZipArchive}; -const SWAGGER_UI_DIST_ZIP: &str = "swagger-ui-5.3.1"; +/// the following env variables control the build process: +/// 1. SWAGGER_UI_DOWNLOAD_URL: +/// + the url from where to download the swagger-ui zip file +/// + default value is SWAGGER_UI_DOWNLOAD_URL_DEFAULT +/// + for other versions, check https://github.com/swagger-api/swagger-ui/tags +/// 2. SWAGGER_UI_OVERWRITE_FOLDER +/// + absolute path to a folder containing files to overwrite the default swagger-ui files -fn main() { - println!("cargo:rerun-if-changed=res/{SWAGGER_UI_DIST_ZIP}.zip"); +const SWAGGER_UI_DOWNLOAD_URL_DEFAULT: &str = + "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.11.0.zip"; +fn main() { let target_dir = env::var("OUT_DIR").unwrap(); + println!("OUT_DIR: {}", target_dir); - let swagger_ui_zip = File::open( - ["res", &format!("{SWAGGER_UI_DIST_ZIP}.zip")] - .iter() - .collect::(), - ) - .unwrap(); + let url = + env::var("SWAGGER_UI_DOWNLOAD_URL").unwrap_or(SWAGGER_UI_DOWNLOAD_URL_DEFAULT.to_string()); + + println!("SWAGGER_UI_DOWNLOAD_URL: {}", url); + let zip_filename = url.split("/").last().unwrap().to_string(); + let zip_path = [&target_dir, &zip_filename].iter().collect::(); + + if !zip_path.exists() { + println!("start download to : {:?}", zip_path); + download_file(&url, zip_path.clone()).unwrap(); + } else { + println!("already downloaded: {:?}", zip_path); + } + + println!("cargo:rerun-if-changed={:?}", zip_path.clone()); + + let swagger_ui_zip = + File::open([&target_dir, &zip_filename].iter().collect::()).unwrap(); let mut zip = ZipArchive::new(swagger_ui_zip).unwrap(); - extract_within_path(&mut zip, [SWAGGER_UI_DIST_ZIP, "dist"], &target_dir).unwrap(); - replace_default_url_with_config(&target_dir); + let zip_top_level_folder = extract_within_path(&mut zip, &target_dir).unwrap(); + println!("zip_top_level_folder: {:?}", zip_top_level_folder); - write_embed_code(&target_dir, &SWAGGER_UI_DIST_ZIP); + replace_default_url_with_config(&target_dir, &zip_top_level_folder); + + write_embed_code(&target_dir, &zip_top_level_folder); + + let overwrite_folder = + PathBuf::from(env::var("SWAGGER_UI_OVERWRITE_FOLDER").unwrap_or("overwrite".to_string())); + + if overwrite_folder.exists() { + println!("SWAGGER_UI_OVERWRITE_FOLDER: {:?}", overwrite_folder); + + for entry in fs::read_dir(overwrite_folder).unwrap() { + let entry = entry.unwrap(); + let path_in = entry.path(); + println!("replacing file: {:?}", path_in.clone()); + overwrite_target_file(&target_dir, &zip_top_level_folder, path_in); + } + } else { + println!( + "SWAGGER_UI_OVERWRITE_FOLDER not found: {:?}", + overwrite_folder + ); + } } -fn extract_within_path( - zip: &mut ZipArchive, - path_segments: [&str; N], - target_dir: &str, -) -> Result<(), ZipError> { +fn extract_within_path(zip: &mut ZipArchive, target_dir: &str) -> Result { + let mut zip_top_level_folder = String::new(); + for index in 0..zip.len() { let mut file = zip.by_index(index)?; let filepath = file .enclosed_name() .ok_or(ZipError::InvalidArchive("invalid path file"))?; - if filepath + if index == 0 { + zip_top_level_folder = filepath + .iter() + .take(1) + .map(|x| x.to_str().unwrap_or_default()) + .collect::(); + } + + let folder = filepath .iter() - .take(2) - .map(|s| s.to_str().unwrap_or_default()) - .cmp(path_segments) - == Ordering::Equal - { + .skip(1) + .take(1) + .map(|x| x.to_str().unwrap_or_default()) + .collect::(); + + if folder == "dist" { let directory = [&target_dir].iter().collect::(); let out_path = directory.join(filepath); @@ -74,15 +122,15 @@ fn extract_within_path( } } - Ok(()) + Ok(zip_top_level_folder) } -fn replace_default_url_with_config(target_dir: &str) { +fn replace_default_url_with_config(target_dir: &str, zip_top_level_folder: &str) { let regex = Regex::new(r#"(?ms)url:.*deep.*true,"#).unwrap(); let path = [ target_dir, - SWAGGER_UI_DIST_ZIP, + zip_top_level_folder, "dist", "swagger-initializer.js", ] @@ -97,7 +145,7 @@ fn replace_default_url_with_config(target_dir: &str) { fs::write(&path, replaced_swagger_initializer.as_ref()).unwrap(); } -fn write_embed_code(target_dir: &str, swagger_version: &str) { +fn write_embed_code(target_dir: &str, zip_top_level_folder: &str) { let contents = format!( r#" // This file is auto-generated during compilation, do not modify @@ -105,8 +153,35 @@ fn write_embed_code(target_dir: &str, swagger_version: &str) { #[folder = r"{}/{}/dist/"] struct SwaggerUiDist; "#, - target_dir, swagger_version + target_dir, zip_top_level_folder ); let path = [target_dir, "embed.rs"].iter().collect::(); fs::write(&path, &contents).unwrap(); } + +fn download_file(url: &str, path: PathBuf) -> Result<(), Box> { + let mut response = reqwest::blocking::get(url)?; + let mut file = File::create(path)?; + io::copy(&mut response, &mut file)?; + Ok(()) +} + +fn overwrite_target_file(target_dir: &str, swagger_ui_dist_zip: &str, path_in: PathBuf) { + let filename = path_in.file_name().unwrap().to_str().unwrap(); + println!("overwrite file: {:?}", path_in.file_name().unwrap()); + + let content = fs::read_to_string(path_in.clone()); + + match content { + Ok(content) => { + let path = [target_dir, swagger_ui_dist_zip, "dist", filename] + .iter() + .collect::(); + + fs::write(&path, &content).unwrap(); + } + Err(_) => { + println!("cannot read content from file: {:?}", path_in); + } + } +} diff --git a/utoipa-swagger-ui/res/swagger-ui-5.3.1.zip b/utoipa-swagger-ui/res/swagger-ui-5.3.1.zip deleted file mode 100644 index 87054f3e..00000000 Binary files a/utoipa-swagger-ui/res/swagger-ui-5.3.1.zip and /dev/null differ