Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add flex to utoipa-swagger-ui build #845

Merged
merged 5 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions utoipa-swagger-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ 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"] }
openssl = { version = "0.10", features = ["vendored"] }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the openssl needed? I don't see any usecases?

Copy link
Contributor Author

@oscar6echo oscar6echo Jan 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think reqwest requires openssl (reqwest -> native-tls -> openssl) but then it should pull it so no need to add it to Cargo.tom. I removed it . Cf. commit.

NB: I came to install openssl vendored because I had problem with my SSL apt package on Ubuntu, then dnf on RHEL8 but these installs - I think these issues were independent - anyway a utoipa user can always import openssl vendored in their app if they want/need.

anyhow = "1.0"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably overkill to add anyhow error for just one error, instead I would maybe just return Box<&dyn Error> it is only a build code and there is necessity to know what the error is. 👇

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I full agree. I changed it. Cf. commit.

28 changes: 24 additions & 4 deletions utoipa-swagger-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -31,25 +31,43 @@ 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"] }
```

**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: <https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.3.1.zip>
juhaku marked this conversation as resolved.
Show resolved Hide resolved
* All versions: <https://github.com/swagger-api/swagger-ui/tags>

* `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()
Expand All @@ -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<Build> {
Expand All @@ -77,13 +96,14 @@ fn rocket() -> Rocket<Build> {

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.

Expand Down
132 changes: 103 additions & 29 deletions utoipa-swagger-ui/build.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::{
cmp::Ordering,
env,
fs::{self, File},
io,
Expand All @@ -9,46 +8,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.3.1.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::<PathBuf>(),
)
.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::<PathBuf>();

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::<PathBuf>()).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<const N: usize>(
zip: &mut ZipArchive<File>,
path_segments: [&str; N],
target_dir: &str,
) -> Result<(), ZipError> {
fn extract_within_path(zip: &mut ZipArchive<File>, target_dir: &str) -> Result<String, ZipError> {
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::<String>();
}

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::<String>();

if folder == "dist" {
let directory = [&target_dir].iter().collect::<PathBuf>();
let out_path = directory.join(filepath);

Expand All @@ -74,15 +121,15 @@ fn extract_within_path<const N: usize>(
}
}

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",
]
Expand All @@ -97,16 +144,43 @@ 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
#[derive(RustEmbed)]
#[folder = r"{}/{}/dist/"]
struct SwaggerUiDist;
"#,
target_dir, swagger_version
target_dir, zip_top_level_folder
);
let path = [target_dir, "embed.rs"].iter().collect::<PathBuf>();
fs::write(&path, &contents).unwrap();
}

fn download_file(url: &str, path: PathBuf) -> Result<(), anyhow::Error> {
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::<PathBuf>();

fs::write(&path, &content).unwrap();
}
Err(_) => {
println!("cannot read content from file: {:?}", path_in);
}
}
}
Binary file removed utoipa-swagger-ui/res/swagger-ui-5.3.1.zip
Binary file not shown.