diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 529d0e0c..63ff1f2c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,6 +22,7 @@ jobs: - utoipa-gen - utoipa-swagger-ui - utoipa-redoc + - utoipa-rapidoc fail-fast: true runs-on: ubuntu-latest @@ -53,6 +54,8 @@ jobs: changes=true elif [[ "$change" == "utoipa-redoc" && "${{ matrix.crate }}" == "utoipa-redoc" && $changes == false ]]; then changes=true + elif [[ "$change" == "utoipa-rapidoc" && "${{ matrix.crate }}" == "utoipa-rapidoc" && $changes == false ]]; then + changes=true fi done < <(git diff --name-only ${{ github.sha }}~ ${{ github.sha }} | grep .rs | awk -F \/ '{print $1}') echo "${{ matrix.crate }} changes: $changes" diff --git a/.github/workflows/draft.yaml b/.github/workflows/draft.yaml index 5fa01537..d14cb68c 100644 --- a/.github/workflows/draft.yaml +++ b/.github/workflows/draft.yaml @@ -17,6 +17,7 @@ jobs: - utoipa-gen - utoipa-swagger-ui - utoipa-redoc + - utoipa-rapidoc runs-on: ubuntu-latest steps: diff --git a/Cargo.toml b/Cargo.toml index c3809615..62709bbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,18 @@ [workspace] resolver = "2" -members = ["utoipa", "utoipa-gen", "utoipa-swagger-ui", "utoipa-redoc"] +members = [ + "utoipa", + "utoipa-gen", + "utoipa-swagger-ui", + "utoipa-redoc", + "utoipa-rapidoc", +] [workspace.metadata.publish] -order = ["utoipa-gen", "utoipa", "utoipa-swagger-ui", "utoipa-redoc"] +order = [ + "utoipa-gen", + "utoipa", + "utoipa-swagger-ui", + "utoipa-redoc", + "utoipa-rapidoc", +] diff --git a/README.md b/README.md index 5229a1a3..fc298b8a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Refer to the existing [examples](./examples) for building the "todo" app in the - **[tide](https://github.com/http-rs/tide)** - **[rocket](https://github.com/SergioBenitez/Rocket)** (`0.4` and `0.5.0-rc3`) -All examples include a [Swagger-UI](https://github.com/swagger-api/swagger-ui) or [Redoc](https://github.com/Redocly/redoc). +All examples include a [Swagger-UI](https://github.com/swagger-api/swagger-ui) unless stated otherwise. There are also examples of building multiple OpenAPI docs in one application, each separated in Swagger UI. These examples exist only for the **actix** and **warp** frameworks. diff --git a/examples/README.md b/examples/README.md index fa6651e8..3f91b10e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,7 +6,7 @@ with the library. All examples have their own README.md, and can be seen using two steps: 1. Run `cargo run` -2. Browse to `http://localhost:8080/swagger-ui/` or `http://localhost:8080/redoc`. +2. Browse to `http://localhost:8080/swagger-ui/` or `http://localhost:8080/redoc` or `http://localhost:8080/rapidoc`. -`Todo-actix`, `todo-axum` and `rocket-todo` has both Swagger UI and Redoc setup others have Swagger UI +`Todo-actix`, `todo-axum` and `rocket-todo` have Swagger UI, Redoc and RapiDoc setup, others have Swagger UI if not explicitly stated otherwise. diff --git a/examples/rocket-todo/Cargo.toml b/examples/rocket-todo/Cargo.toml index b09f845c..5190adf2 100644 --- a/examples/rocket-todo/Cargo.toml +++ b/examples/rocket-todo/Cargo.toml @@ -13,6 +13,7 @@ rocket = { version = "0.5.0-rc.3", features = ["json"] } utoipa = { path = "../../utoipa", features = ["rocket_extras"] } utoipa-swagger-ui = { path = "../../utoipa-swagger-ui", features = ["rocket"] } utoipa-redoc = { path = "../../utoipa-redoc", features = ["rocket"] } +utoipa-rapidoc = { path = "../../utoipa-rapidoc", features = ["rocket"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" env_logger = "0.10.0" diff --git a/examples/rocket-todo/README.md b/examples/rocket-todo/README.md index 0eee5620..8fb5b4ca 100644 --- a/examples/rocket-todo/README.md +++ b/examples/rocket-todo/README.md @@ -1,4 +1,4 @@ -# todo-rocket ~ utoipa with utoipa-swagger-ui and utoipa-redoc example +# todo-rocket ~ utoipa with utoipa-swagger-ui, utoipa-redoc and utoipa-rapidoc example This is a demo `rocket` application with in-memory storage to manage Todo items. The API demonstrates `utoipa` with `utoipa-swagger-ui` functionalities. @@ -9,6 +9,8 @@ Just run command below to run the demo application and browse to `http://localho If you prefer Redoc just head to `http://localhost:8000/redoc` and view the Open API. +RapiDoc can be found from `http://localhost:8000/redoc`. + ```bash cargo run ``` diff --git a/examples/rocket-todo/src/main.rs b/examples/rocket-todo/src/main.rs index 73985a33..7b4d2b1d 100644 --- a/examples/rocket-todo/src/main.rs +++ b/examples/rocket-todo/src/main.rs @@ -5,6 +5,7 @@ use utoipa::{ openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, Modify, OpenApi, }; +use utoipa_rapidoc::RapiDoc; use utoipa_redoc::{Redoc, Servable}; use utoipa_swagger_ui::SwaggerUi; @@ -52,6 +53,17 @@ fn rocket() -> Rocket { "/", SwaggerUi::new("/swagger-ui/<_..>").url("/api-docs/openapi.json", ApiDoc::openapi()), ) + // There is no need to create RapiDoc::with_openapi because the OpenApi is served + // via SwaggerUi instead we only make rapidoc to point to the existing doc. + .mount( + "/", + RapiDoc::new("/api-docs/openapi.json").path("/rapidoc") + ) + // Alternative to above + // .mount( + // "/", + // RapiDoc::with_openapi("/api-docs/openapi2.json", ApiDoc::openapi()).path("/rapidoc") + // ) .mount("/", Redoc::with_url("/redoc", ApiDoc::openapi())) .mount( "/todo", diff --git a/examples/todo-actix/Cargo.toml b/examples/todo-actix/Cargo.toml index 9571b13a..7e029e08 100644 --- a/examples/todo-actix/Cargo.toml +++ b/examples/todo-actix/Cargo.toml @@ -20,5 +20,6 @@ futures = "0.3" utoipa = { path = "../../utoipa", features = ["actix_extras"] } utoipa-swagger-ui = { path = "../../utoipa-swagger-ui", features = ["actix-web"] } utoipa-redoc = { path = "../../utoipa-redoc", features = ["actix-web"] } +utoipa-rapidoc = { path = "../../utoipa-rapidoc", features = ["actix-web"] } [workspace] diff --git a/examples/todo-actix/README.md b/examples/todo-actix/README.md index 6906b6f7..29b54766 100644 --- a/examples/todo-actix/README.md +++ b/examples/todo-actix/README.md @@ -1,4 +1,4 @@ -# todo-actix ~ utoipa with utoipa-swagger-ui and utoipa-redoc example +# todo-actix ~ utoipa with utoipa-swagger-ui, utoipa-redoc and utoipa-rapidoc example This is a demo `actix-web` application with in-memory storage to manage Todo items. The API demonstrates `utoipa` with `utoipa-swagger-ui` functionalities. @@ -9,6 +9,8 @@ Just run command below to run the demo application and browse to `http://localho If you prefer Redoc just head to `http://localhost:8000/redoc` and view the Open API. +RapiDoc can be found from `http://localhost:8000/rapidoc`. + ```bash cargo run ``` diff --git a/examples/todo-actix/src/main.rs b/examples/todo-actix/src/main.rs index 8f4c0493..fe2b978d 100644 --- a/examples/todo-actix/src/main.rs +++ b/examples/todo-actix/src/main.rs @@ -15,6 +15,7 @@ use utoipa::{ openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, Modify, OpenApi, }; +use utoipa_rapidoc::RapiDoc; use utoipa_redoc::{Redoc, Servable}; use utoipa_swagger_ui::SwaggerUi; @@ -74,6 +75,11 @@ async fn main() -> Result<(), impl Error> { .service( SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", openapi.clone()), ) + // There is no need to create RapiDoc::with_openapi because the OpenApi is served + // via SwaggerUi instead we only make rapidoc to point to the existing doc. + .service(RapiDoc::new("/api-docs/openapi.json").path("/rapidoc")) + // Alternative to above + // .service(RapiDoc::with_openapi("/api-docs/openapi2.json", openapi.clone()).path("/rapidoc")) }) .bind((Ipv4Addr::UNSPECIFIED, 8080))? .run() diff --git a/examples/todo-axum/Cargo.toml b/examples/todo-axum/Cargo.toml index 56b57ca1..e8504898 100644 --- a/examples/todo-axum/Cargo.toml +++ b/examples/todo-axum/Cargo.toml @@ -18,6 +18,7 @@ tower = "0.4" utoipa = { path = "../../utoipa", features = ["axum_extras"] } utoipa-swagger-ui = { path = "../../utoipa-swagger-ui", features = ["axum"] } utoipa-redoc = { path = "../../utoipa-redoc", features = ["axum"] } +utoipa-rapidoc = { path = "../../utoipa-rapidoc", features = ["axum"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" env_logger = "0.10.0" diff --git a/examples/todo-axum/README.md b/examples/todo-axum/README.md index 5a724993..fdb4e8f0 100644 --- a/examples/todo-axum/README.md +++ b/examples/todo-axum/README.md @@ -1,4 +1,4 @@ -# todo-axum ~ utoipa with utoipa-swagger-ui and utoipa-redoc example +# todo-axum ~ utoipa with utoipa-swagger-ui, utoipa-redoc and utoipa-rapidoc example This is a demo `axum` application with in-memory storage to manage Todo items. The API demonstrates `utoipa` with `utoipa-swagger-ui` functionalities. @@ -9,6 +9,8 @@ Just run command below to run the demo application and browse to `http://localho If you prefer Redoc just head to `http://localhost:8000/redoc` and view the Open API. +RapiDoc can be found from `http://localhost:8000/rapidoc`. + ```bash cargo run ``` diff --git a/examples/todo-axum/src/main.rs b/examples/todo-axum/src/main.rs index b8e5db1c..adb66d09 100644 --- a/examples/todo-axum/src/main.rs +++ b/examples/todo-axum/src/main.rs @@ -9,6 +9,7 @@ use utoipa::{ openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, Modify, OpenApi, }; +use utoipa_rapidoc::RapiDoc; use utoipa_redoc::{Redoc, Servable}; use utoipa_swagger_ui::SwaggerUi; @@ -52,6 +53,11 @@ async fn main() -> Result<(), Error> { let app = Router::new() .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) .merge(Redoc::with_url("/redoc", ApiDoc::openapi())) + // There is no need to create `RapiDoc::with_openapi` because the OpenApi is served + // via SwaggerUi instead we only make rapidoc to point to the existing doc. + .merge(RapiDoc::new("/api-docs/openapi.json").path("/rapidoc")) + // Alternative to above + // .merge(RapiDoc::with_openapi("/api-docs/openapi2.json", ApiDoc::openapi()).path("/rapidoc")) .route( "/todo", routing::get(todo::list_todos).post(todo::create_todo), diff --git a/examples/todo-warp-rapidoc/Cargo.toml b/examples/todo-warp-rapidoc/Cargo.toml new file mode 100644 index 00000000..3e947581 --- /dev/null +++ b/examples/todo-warp-rapidoc/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "todo-warp-rapidoc" +description = "Simple warp todo example api with utoipa and utoipa-rapidoc" +version = "0.1.0" +edition = "2021" +license = "MIT" +authors = [ + "Elli Example " +] + +[dependencies] +tokio = { version = "1", features = ["full"] } +warp = "0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +env_logger = "0.10.0" +log = "0.4" +futures = "0.3" +utoipa = { path = "../../utoipa" } +utoipa-rapidoc = { path = "../../utoipa-rapidoc" } + +[workspace] + diff --git a/examples/todo-warp-rapidoc/README.md b/examples/todo-warp-rapidoc/README.md new file mode 100644 index 00000000..962faf89 --- /dev/null +++ b/examples/todo-warp-rapidoc/README.md @@ -0,0 +1,12 @@ +# warp with utoipa-rapidoc + +This is simple Todo app example with warp and utoipa-rapidoc OpenAPI viewer. + +For security restricted endpoints the super secret API key is: `utoipa-rocks`. + +Head to `http://localhost:8080/rapidoc` for the demo. + +run +```rust +RUST_LOG=debug cargo run +``` diff --git a/examples/todo-warp-rapidoc/src/main.rs b/examples/todo-warp-rapidoc/src/main.rs new file mode 100644 index 00000000..3f9ea737 --- /dev/null +++ b/examples/todo-warp-rapidoc/src/main.rs @@ -0,0 +1,236 @@ +use std::net::Ipv4Addr; + +use utoipa::{ + openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, + Modify, OpenApi, +}; +use utoipa_rapidoc::RapiDoc; +use warp::Filter; + +#[tokio::main] +async fn main() { + env_logger::init(); + + #[derive(OpenApi)] + #[openapi( + paths(todo::list_todos, todo::create_todo, todo::delete_todo), + components( + schemas(todo::Todo) + ), + modifiers(&SecurityAddon), + tags( + (name = "todo", description = "Todo items management API") + ) + )] + struct ApiDoc; + + struct SecurityAddon; + + impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + let components = openapi.components.as_mut().unwrap(); // we can unwrap safely since there already is components registered. + components.add_security_scheme( + "api_key", + SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))), + ) + } + } + + let api_doc = warp::path("api-doc.json") + .and(warp::get()) + .map(|| warp::reply::json(&ApiDoc::openapi())); + + let rapidoc_handler = warp::path("rapidoc") + .and(warp::get()) + .map(|| warp::reply::html(RapiDoc::new("/api-doc.json").to_html())); + + warp::serve(api_doc.or(rapidoc_handler).or(todo::handlers())) + .run((Ipv4Addr::UNSPECIFIED, 8080)) + .await +} + +mod todo { + use std::{ + convert::Infallible, + sync::{Arc, Mutex}, + }; + + use serde::{Deserialize, Serialize}; + use utoipa::{IntoParams, ToSchema}; + use warp::{hyper::StatusCode, Filter, Rejection, Reply}; + + pub type Store = Arc>>; + + /// Item to complete. + #[derive(Serialize, Deserialize, ToSchema, Clone)] + pub struct Todo { + /// Unique database id. + #[schema(example = 1)] + id: i64, + /// Description of what need to be done. + #[schema(example = "Buy movie tickets")] + value: String, + } + + #[derive(Debug, Deserialize, ToSchema)] + #[serde(rename_all = "snake_case")] + pub enum Order { + AscendingId, + DescendingId, + } + + #[derive(Debug, Deserialize, IntoParams)] + #[into_params(parameter_in = Query)] + pub struct ListQueryParams { + /// Filters the returned `Todo` items according to whether they contain the specified string. + #[param(style = Form, example = json!("task"))] + contains: Option, + /// Order the returned `Todo` items. + #[param(inline)] + order: Option, + } + + pub fn handlers() -> impl Filter + Clone { + let store = Store::default(); + + let list = warp::path("todo") + .and(warp::get()) + .and(warp::path::end()) + .and(with_store(store.clone())) + .and(warp::query::()) + .and_then(list_todos); + + let create = warp::path("todo") + .and(warp::post()) + .and(warp::path::end()) + .and(warp::body::json()) + .and(with_store(store.clone())) + .and_then(create_todo); + + let delete = warp::path!("todo" / i64) + .and(warp::delete()) + .and(warp::path::end()) + .and(with_store(store)) + .and(warp::header::header("todo_apikey")) + .and_then(delete_todo); + + list.or(create).or(delete) + } + + fn with_store(store: Store) -> impl Filter + Clone { + warp::any().map(move || store.clone()) + } + + /// List todos from in-memory storage. + /// + /// List all todos from in-memory storage. + #[utoipa::path( + get, + path = "/todo", + params(ListQueryParams), + responses( + (status = 200, description = "List todos successfully", body = [Todo]) + ) + )] + pub async fn list_todos( + store: Store, + query: ListQueryParams, + ) -> Result { + let todos = store.lock().unwrap(); + + let mut todos: Vec = if let Some(contains) = query.contains { + todos + .iter() + .filter(|todo| todo.value.contains(&contains)) + .cloned() + .collect() + } else { + todos.clone() + }; + + if let Some(order) = query.order { + match order { + Order::AscendingId => { + todos.sort_by_key(|todo| todo.id); + } + Order::DescendingId => { + todos.sort_by_key(|todo| todo.id); + todos.reverse(); + } + } + } + + Ok(warp::reply::json(&todos)) + } + + /// Create new todo item. + /// + /// Creates new todo item to in-memory storage if it is unique by id. + #[utoipa::path( + post, + path = "/todo", + request_body = Todo, + responses( + (status = 200, description = "Todo created successfully", body = Todo), + (status = 409, description = "Todo already exists") + ) + )] + pub async fn create_todo(todo: Todo, store: Store) -> Result, Infallible> { + let mut todos = store.lock().unwrap(); + + if todos + .iter() + .any(|existing_todo| existing_todo.id == todo.id) + { + Ok(Box::new(StatusCode::CONFLICT)) + } else { + todos.push(todo.clone()); + + Ok(Box::new(warp::reply::with_status( + warp::reply::json(&todo), + StatusCode::CREATED, + ))) + } + } + + /// Delete todo item by id. + /// + /// Delete todo item by id from in-memory storage. + #[utoipa::path( + delete, + path = "/todo/{id}", + responses( + (status = 200, description = "Delete successful"), + (status = 400, description = "Missing todo_apikey request header"), + (status = 401, description = "Unauthorized to delete todo"), + (status = 404, description = "Todo not found to delete"), + ), + params( + ("id" = i64, Path, description = "Todo's unique id") + ), + security( + ("api_key" = []) + ) + )] + pub async fn delete_todo( + id: i64, + store: Store, + api_key: String, + ) -> Result { + if api_key != "utoipa-rocks" { + return Ok(StatusCode::UNAUTHORIZED); + } + + let mut todos = store.lock().unwrap(); + + let size = todos.len(); + + todos.retain(|existing| existing.id != id); + + if size == todos.len() { + Ok(StatusCode::NOT_FOUND) + } else { + Ok(StatusCode::OK) + } + } +} diff --git a/scripts/test.sh b/scripts/test.sh index db1f4d0f..82155575 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -23,4 +23,6 @@ elif [[ "$crate" == "utoipa-swagger-ui" ]]; then cargo test -p utoipa-swagger-ui --features actix-web,rocket,axum elif [[ "$crate" == "utoipa-redoc" ]]; then cargo test -p utoipa-redoc --features actix-web,rocket,axum +elif [[ "$crate" == "utoipa-rapidoc" ]]; then + cargo test -p utoipa-rapidoc --features actix-web,rocket,axum fi diff --git a/utoipa-rapidoc/Cargo.toml b/utoipa-rapidoc/Cargo.toml new file mode 100644 index 00000000..8e79907a --- /dev/null +++ b/utoipa-rapidoc/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "utoipa-rapidoc" +description = "RapiDoc for utoipa" +edition = "2021" +version = "0.1.0" +license = "MIT OR Apache-2.0" +readme = "README.md" +keywords = ["rapidoc", "openapi", "documentation"] +repository = "https://github.com/juhaku/utoipa" +categories = ["web-programming"] +authors = ["Juha Kukkonen "] + +[package.metadata.docs.rs] +features = ["actix-web", "axum", "rocket"] +rustdoc-args = ["--cfg", "doc_cfg"] + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0" } +utoipa = { version = "3", path = "../utoipa" } +actix-web = { version = "4", optional = true, default-features = false } +rocket = { version = "0.5.0-rc.3", features = ["json"], optional = true } +axum = { version = "0.6", optional = true } diff --git a/utoipa-rapidoc/LICENSE-APACHE b/utoipa-rapidoc/LICENSE-APACHE new file mode 120000 index 00000000..965b606f --- /dev/null +++ b/utoipa-rapidoc/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/utoipa-rapidoc/LICENSE-MIT b/utoipa-rapidoc/LICENSE-MIT new file mode 120000 index 00000000..76219eb7 --- /dev/null +++ b/utoipa-rapidoc/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/utoipa-rapidoc/README.md b/utoipa-rapidoc/README.md new file mode 100644 index 00000000..2fc7b9fb --- /dev/null +++ b/utoipa-rapidoc/README.md @@ -0,0 +1,110 @@ +# utoipa-rapidoc + +This crate works as a bridge between [utoipa](https://docs.rs/utoipa/latest/utoipa/) and [RapiDoc](https://rapidocweb.com/) OpenAPI visualizer. + +[![Utoipa build](https://github.com/juhaku/utoipa/actions/workflows/build.yaml/badge.svg)](https://github.com/juhaku/utoipa/actions/workflows/build.yaml) +[![crates.io](https://img.shields.io/crates/v/utoipa-rapidoc.svg?label=crates.io&color=orange&logo=rust)](https://crates.io/crates/utoipa-rapidoc) +[![docs.rs](https://img.shields.io/static/v1?label=docs.rs&message=utoipa-rapidoc&color=blue&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K)](https://docs.rs/utoipa-rapidoc/latest/) +![rustc](https://img.shields.io/static/v1?label=rustc&message=1.60%2B&color=orange&logo=rust) + +Utoipa-rapidoc provides simple mechanism to transform OpenAPI spec resource to a servable HTML +file which can be served via [predefined framework integration](#examples) or used +[standalone](#using-standalone) and served manually. + +You may find fullsize examples from utoipa's Github [repository][examples]. + +# Crate Features + +* **actix-web** Allows serving `RapiDoc` via _**`actix-web`**_. `version >= 4` +* **rocket** Allows serving `RapiDoc` via _**`rocket`**_. `version >=0.5.0-rc.3` +* **axum** Allows serving `RapiDoc` via _**`axum`**_. `version >=0.6` + +# Install + +Use RapiDoc only without any boiler plate implementation. +```toml +[dependencies] +utoipa-rapidoc = "0.1" +``` + +Enable actix-web integration with RapiDoc. +```toml +[dependencies] +utoipa-rapidoc = { version = "0.1", features = ["actix-web"] } +``` + +# Using standalone + +Utoipa-rapidoc can be used standalone as simply as creating a new `RapiDoc` instance and then +serving it by what ever means available as `text/html` from http handler in your favourite web +framework. + +`RapiDoc::to_html` method can be used to convert the `RapiDoc` instance to a servable html +file. +```rust +let rapidoc = RapiDoc::new("/api-docs/openapi.json"); + +// Then somewhere in your application that handles http operation. +// Make sure you return correct content type `text/html`. +let rapidoc_handler = move || { + rapidoc.to_html() +}; +``` + +# Customization + +Utoipa-rapidoc can be customized and configured only via `RapiDoc::custom_html` method. This +method empowers users to use a custom HTML template to modify the looks of the RapiDoc UI. + +* [All allowed RapiDoc configuration options][rapidoc_api] +* [Default HTML template][rapidoc_quickstart] + +The template should contain _**`$specUrl`**_ variable which will be replaced with user defined +OpenAPI spec url provided with `RapiDoc::new` function when creating a new `RapiDoc` +instance. Variable will be replaced during `RapiDoc::to_html` function execution. + +_**Overiding the HTML template with a custom one.**_ +```rust +let html = "..."; +RapiDoc::new("/api-docs/openapi.json").custom_html(html); +``` + +# Examples + +_**Serve `RapiDoc` via `actix-web` framework.**_ +```rust +use actix_web::App; +use utoipa_rapidoc::RapiDoc; + +App::new().service(RapiDoc::with_openapi("/rapidoc", ApiDoc::openapi())); +``` + +_**Serve `RapiDoc` via `rocket` framework.**_ +```rust +use utoipa_rapidoc::RapiDoc; + +rocket::build() + .mount( + "/", + RapiDoc::with_openapi("/rapidoc", ApiDoc::openapi()), + ); +``` + +_**Serve `RapiDoc` via `axum` framework.**_ + ```rust + use axum::{Router, body::HttpBody}; + use utoipa_rapidoc::RapiDoc; + let app = Router::::new() + .merge(RapiDoc::with_openapi("/rapidoc", ApiDoc::openapi())); +``` + +# License + +Licensed under either of [Apache 2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT) license at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate +by you, shall be dual licensed, without any additional terms or conditions. + +[rapidoc_api]: +[examples]: +[rapidoc_quickstart]: diff --git a/utoipa-rapidoc/res/rapidoc.html b/utoipa-rapidoc/res/rapidoc.html new file mode 100644 index 00000000..104ef199 --- /dev/null +++ b/utoipa-rapidoc/res/rapidoc.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/utoipa-rapidoc/src/lib.rs b/utoipa-rapidoc/src/lib.rs new file mode 100644 index 00000000..938c9e90 --- /dev/null +++ b/utoipa-rapidoc/src/lib.rs @@ -0,0 +1,387 @@ +#![warn(missing_docs)] +#![warn(rustdoc::broken_intra_doc_links)] +#![cfg_attr(doc_cfg, feature(doc_cfg))] +//! This crate works as a bridge between [utoipa](https://docs.rs/utoipa/latest/utoipa/) and [RapiDoc](https://rapidocweb.com/) OpenAPI visualizer. +//! +//! Utoipa-rapidoc provides simple mechanism to transform OpenAPI spec resource to a servable HTML +//! file which can be served via [predefined framework integration][Self#examples] or used +//! [standalone][Self#using-standalone] and served manually. +//! +//! You may find fullsize examples from utoipa's Github [repository][examples]. +//! +//! # Crate Features +//! +//! * **actix-web** Allows serving [`RapiDoc`] via _**`actix-web`**_. +//! * **rocket** Allows serving [`RapiDoc`] via _**`rocket`**_. +//! * **axum** Allows serving [`RapiDoc`] via _**`axum`**_. +//! +//! # Install +//! +//! Use RapiDoc only without any boiler plate implementation. +//! ```toml +//! [dependencies] +//! utoipa-rapidoc = "0.1" +//! ``` +//! +//! Enable actix-web integration with RapiDoc. +//! ```toml +//! [dependencies] +//! utoipa-rapidoc = { version = "0.1", features = ["actix-web"] } +//! ``` +//! +//! # Using standalone +//! +//! Utoipa-rapidoc can be used standalone as simply as creating a new [`RapiDoc`] instance and then +//! serving it by what ever means available as `text/html` from http handler in your favourite web +//! framework. +//! +//! [`RapiDoc::to_html`] method can be used to convert the [`RapiDoc`] instance to a servable html +//! file. +//! ``` +//! # use utoipa_rapidoc::RapiDoc; +//! # use utoipa::OpenApi; +//! # use serde_json::json; +//! # #[derive(OpenApi)] +//! # #[openapi()] +//! # struct ApiDoc; +//! # +//! let rapidoc = RapiDoc::new("/api-docs/openapi.json"); +//! +//! // Then somewhere in your application that handles http operation. +//! // Make sure you return correct content type `text/html`. +//! let rapidoc_handler = move || { +//! rapidoc.to_html() +//! }; +//! ``` +//! +//! # Customization +//! +//! Utoipa-rapidoc can be customized and configured only via [`RapiDoc::custom_html`] method. This +//! method empowers users to use a custom HTML template to modify the looks of the RapiDoc UI. +//! +//! * [All allowed RapiDoc configuration options][rapidoc_api] +//! * [Default HTML template][rapidoc_quickstart] +//! +//! The template should contain _**`$specUrl`**_ variable which will be replaced with user defined +//! OpenAPI spec url provided with [`RapiDoc::new`] function when creating a new [`RapiDoc`] +//! instance. Variable will be replaced during [`RapiDoc::to_html`] function execution. +//! +//! _**Overiding the HTML template with a custom one.**_ +//! ```rust +//! # use utoipa_rapidoc::RapiDoc; +//! # use utoipa::OpenApi; +//! # use serde_json::json; +//! # #[derive(OpenApi)] +//! # #[openapi()] +//! # struct ApiDoc; +//! # +//! let html = "..."; +//! RapiDoc::new("/api-docs/openapi.json").custom_html(html); +//! ``` +//! +//! # Examples +//! +//! _**Serve [`RapiDoc`] via `actix-web` framework.**_ +//! ```no_run +//! use actix_web::App; +//! use utoipa_rapidoc::RapiDoc; +//! +//! # use utoipa::OpenApi; +//! # use std::net::Ipv4Addr; +//! # #[derive(OpenApi)] +//! # #[openapi()] +//! # struct ApiDoc; +//! App::new().service(RapiDoc::with_openapi("/rapidoc", ApiDoc::openapi())); +//! ``` +//! +//! _**Serve [`RapiDoc`] via `rocket` framework.**_ +//! ```no_run +//! # use rocket; +//! use utoipa_rapidoc::RapiDoc; +//! +//! # use utoipa::OpenApi; +//! # #[derive(OpenApi)] +//! # #[openapi()] +//! # struct ApiDoc; +//! rocket::build() +//! .mount( +//! "/", +//! RapiDoc::with_openapi("/rapidoc", ApiDoc::openapi()), +//! ); +//! ``` +//! +//! _**Serve [`RapiDoc`] via `axum` framework.**_ +//! ```no_run +//! use axum::{Router, body::HttpBody}; +//! use utoipa_rapidoc::RapiDoc; +//! # use utoipa::OpenApi; +//! # #[derive(OpenApi)] +//! # #[openapi()] +//! # struct ApiDoc; +//! # +//! # fn inner() +//! # where +//! # B: HttpBody + Send + 'static, +//! # S: Clone + Send + Sync + 'static, +//! # { +//! +//! let app = Router::::new() +//! .merge(RapiDoc::with_openapi("/rapidoc", ApiDoc::openapi())); +//! # } +//! ``` +//! +//! [rapidoc_api]: +//! [examples]: +//! [rapidoc_quickstart]: + +const DEFAULT_HTML: &str = include_str!("../res/rapidoc.html"); + +/// Is [RapiDoc][rapidoc] UI. +/// +/// This is an antry point for serving [RapiDoc][rapidoc] via predefined framework integration or +/// in standalone fashion by calling [`RapiDoc::to_html`] within custom HTTP handler handles +/// serving the [RapiDoc][rapidoc] UI. See more at [running standalone][standalone] +/// +/// [rapidoc]: +/// [standalone]: index.html#using-standalone +#[non_exhaustive] +pub struct RapiDoc<'u, 's, 'h> { + #[allow(unused)] + path: &'u str, + spec_url: &'s str, + html: &'h str, + #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))] + openapi: Option, +} + +impl<'u, 's, 'h> RapiDoc<'u, 's, 'h> { + /// Construct a new [`RapiDoc`] that points to given `spec_url`. Spec url must be valid URL and + /// available for RapiDoc to consume. + /// + /// # Examples + /// + /// _**Create new [`RapiDoc`].**_ + /// + /// ``` + /// # use utoipa_rapidoc::RapiDoc; + /// RapiDoc::new("https://petstore3.swagger.io/api/v3/openapi.json"); + /// ``` + pub fn new(spec_url: &'s str) -> Self { + Self { + path: "", + spec_url, + html: DEFAULT_HTML, + #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))] + openapi: None, + } + } + + /// Construct a new [`RapiDoc`] with given `spec_url` and `openapi`. The spec url must point to + /// the location where the `openapi` will be served. + /// + /// [`RapiDoc`] is only able to create endpoint that serves the `openapi` JSON for predefined + /// frameworks. _**For other frameworks such endoint must be created manually.**_ + /// + /// # Examples + /// + /// _**Create new [`RapiDoc`].**_ + /// + /// ``` + /// # use utoipa_rapidoc::RapiDoc; + /// # use utoipa::OpenApi; + /// # #[derive(OpenApi)] + /// # #[openapi()] + /// # struct ApiDoc; + /// RapiDoc::with_openapi( + /// "/api-docs/openapi.json", + /// ApiDoc::openapi() + /// ); + /// ``` + #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))] + #[cfg_attr( + doc_cfg, + doc(cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))) + )] + pub fn with_openapi(spec_url: &'s str, openapi: utoipa::openapi::OpenApi) -> Self { + Self { + path: "", + spec_url, + html: DEFAULT_HTML, + openapi: Some(openapi), + } + } + + /// Override the [default HTML template][rapidoc_quickstart] with new one. See + /// [customization] for more details. + /// + /// [rapidoc_quickstart]: + /// [customization]: index.html#customization + pub fn custom_html(mut self, html: &'h str) -> Self { + self.html = html; + + self + } + + /// Add `path` the [`RapiDoc`] will be served from. + /// + /// # Examples + /// + /// _**Make [`RapiDoc`] servable from `/rapidoc` path.**_ + /// ``` + /// # use utoipa_rapidoc::RapiDoc; + /// + /// RapiDoc::new("https://petstore3.swagger.io/api/v3/openapi.json") + /// .path("/rapidoc"); + /// ``` + #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))] + pub fn path(mut self, path: &'u str) -> Self { + self.path = path; + + self + } + + /// Converts this [`RapiDoc`] instance to servable HTML file. + /// + /// This will replace _**`$specUrl`**_ variable placeholder with the spec + /// url provided to the [`RapiDoc`] instance. If HTML template is not overridden with + /// [`RapiDoc::custom_html`] then the [default HTML template][rapidoc_quickstart] + /// will be used. + /// + /// See more details in [customization][customization]. + /// + /// [rapidoc_quickstart]: + /// [customization]: index.html#customization + pub fn to_html(&self) -> String { + self.html.replace("$specUrl", self.spec_url) + } +} + +mod acitx { + #![cfg(feature = "actix-web")] + + use actix_web::dev::HttpServiceFactory; + use actix_web::guard::Get; + use actix_web::web::Data; + use actix_web::{HttpResponse, Resource, Responder}; + + use crate::RapiDoc; + + impl HttpServiceFactory for RapiDoc<'_, '_, '_> { + fn register(self, config: &mut actix_web::dev::AppService) { + let html = self.to_html(); + + async fn serve_rapidoc(rapidoc: Data) -> impl Responder { + HttpResponse::Ok() + .content_type("text/html") + .body(rapidoc.to_string()) + } + + Resource::new(self.path) + .guard(Get()) + .app_data(Data::new(html)) + .to(serve_rapidoc) + .register(config); + + if let Some(openapi) = self.openapi { + async fn serve_openapi(openapi: Data) -> impl Responder { + HttpResponse::Ok() + .content_type("application/json") + .body(openapi.into_inner().to_string()) + } + + Resource::new(self.spec_url) + .guard(Get()) + .app_data(Data::new( + openapi.to_json().expect("Should serialize to JSON"), + )) + .to(serve_openapi) + .register(config); + } + } + } +} + +mod axum { + #![cfg(feature = "axum")] + + use axum::body::HttpBody; + use axum::response::Html; + use axum::{routing, Json, Router}; + + use crate::RapiDoc; + + impl From> for Router + where + R: Clone + Send + Sync + 'static, + B: HttpBody + Send + 'static, + { + fn from(value: RapiDoc<'_, '_, '_>) -> Self { + let html = value.to_html(); + let openapi = value.openapi; + + let mut router = + Router::::new().route(value.path, routing::get(move || async { Html(html) })); + + if let Some(openapi) = openapi { + router = router.route( + value.spec_url, + routing::get(move || async { Json(openapi) }), + ); + } + + router + } + } +} + +mod rocket { + #![cfg(feature = "rocket")] + + use rocket::http::Method; + use rocket::response::content::RawHtml; + use rocket::route::{Handler, Outcome}; + use rocket::serde::json::Json; + use rocket::{Data, Request, Route}; + + use crate::RapiDoc; + + impl From> for Vec { + fn from(value: RapiDoc<'_, '_, '_>) -> Self { + let mut routes = vec![Route::new( + Method::Get, + value.path, + RapiDocHandler(value.to_html()), + )]; + + if let Some(openapi) = value.openapi { + routes.push(Route::new( + Method::Get, + value.spec_url, + OpenApiHandler(openapi.to_json().expect("Should serialize to JSON")), + )); + } + + routes + } + } + + #[derive(Clone)] + struct RapiDocHandler(String); + + #[rocket::async_trait] + impl Handler for RapiDocHandler { + async fn handle<'r>(&self, request: &'r Request<'_>, _: Data<'r>) -> Outcome<'r> { + Outcome::from(request, RawHtml(self.0.clone())) + } + } + + #[derive(Clone)] + struct OpenApiHandler(String); + + #[rocket::async_trait] + impl Handler for OpenApiHandler { + async fn handle<'r>(&self, request: &'r Request<'_>, _: Data<'r>) -> Outcome<'r> { + Outcome::from(request, Json(self.0.clone())) + } + } +} diff --git a/utoipa-redoc/Cargo.toml b/utoipa-redoc/Cargo.toml index 4c79c1e3..ef3bee35 100644 --- a/utoipa-redoc/Cargo.toml +++ b/utoipa-redoc/Cargo.toml @@ -18,8 +18,6 @@ rustdoc-args = ["--cfg", "doc_cfg"] serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } utoipa = { version = "3", path = "../utoipa" } -actix-web = { version = "4", features = [ - "macros", -], optional = true, default-features = false } +actix-web = { version = "4", optional = true, default-features = false } rocket = { version = "0.5.0-rc.3", features = ["json"], optional = true } axum = { version = "0.6", optional = true } diff --git a/utoipa-redoc/README.md b/utoipa-redoc/README.md index 3957e908..217a49f3 100644 --- a/utoipa-redoc/README.md +++ b/utoipa-redoc/README.md @@ -2,7 +2,7 @@ [![Utoipa build](https://github.com/juhaku/utoipa/actions/workflows/build.yaml/badge.svg)](https://github.com/juhaku/utoipa/actions/workflows/build.yaml) [![crates.io](https://img.shields.io/crates/v/utoipa-redoc.svg?label=crates.io&color=orange&logo=rust)](https://crates.io/crates/utoipa-redoc) -[![docs.rs](https://img.shields.io/static/v1?label=docs.rs&message=utoipa-redoc&color=blue&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K)](https://docs.rs/utoipa-redoc/latest/utoipa_swagger_ui/) +[![docs.rs](https://img.shields.io/static/v1?label=docs.rs&message=utoipa-redoc&color=blue&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K)](https://docs.rs/utoipa-redoc/latest/) ![rustc](https://img.shields.io/static/v1?label=rustc&message=1.60%2B&color=orange&logo=rust) This crate works as a bridge between [utoipa](https://docs.rs/utoipa/latest/utoipa/) and [Redoc](https://redocly.com/) OpenAPI visualizer. @@ -15,9 +15,9 @@ You may find fullsize examples from utoipa's Github [repository][examples]. # Crate Features -* **actix-web** Allows serving `Redoc` via _**`actix-web`**_. -* **rocket** Allows serving `Redoc` via _**`rocket`**_. -* **axum** Allows serving `Redoc` via _**`axum`**_. +* **actix-web** Allows serving `Redoc` via _**`actix-web`**_. `version >= 4` +* **rocket** Allows serving `Redoc` via _**`rocket`**_. `version >=0.5.0-rc.3` +* **axum** Allows serving `Redoc` via _**`axum`**_. `version >=0.6` # Install @@ -58,7 +58,7 @@ customized by modifying the HTML template and [configuration options](#configura The default [HTML template][redoc_html_quickstart] can be fully overridden to ones liking with `Redoc::custom_html` method. The HTML template **must** contain **`$spec`** and **`$config`** -variables which are replaced during `Redoc::to_html` evaluation. +variables which are replaced during `Redoc::to_html` execution. * **`$spec`** Will be the `Spec` that will be rendered via [Redoc][redoc]. * **`$config`** Will be the current `Config`. By default this is `EmptyConfig`. @@ -129,6 +129,13 @@ _**Use `Redoc` to serve custom OpenAPI spec using serde's `json!()` macro.**_ Redoc::new(json!({"openapi": "3.1.0"})); ``` +# License + +Licensed under either of [Apache 2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT) license at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate +by you, shall be dual licensed, without any additional terms or conditions. + [redoc]: [redoc_html_quickstart]: [redoc_config]: diff --git a/utoipa-redoc/src/lib.rs b/utoipa-redoc/src/lib.rs index eba575f9..bad67aa0 100644 --- a/utoipa-redoc/src/lib.rs +++ b/utoipa-redoc/src/lib.rs @@ -61,7 +61,7 @@ //! //! The default [HTML template][redoc_html_quickstart] can be fully overridden to ones liking with //! [`Redoc::custom_html`] method. The HTML template **must** contain **`$spec`** and **`$config`** -//! variables which are replaced during [`Redoc::to_html`] evaluation. +//! variables which are replaced during [`Redoc::to_html`] execution. //! //! * **`$spec`** Will be the [`Spec`] that will be rendered via [Redoc][redoc]. //! * **`$config`** Will be the current [`Config`]. By default this is [`EmptyConfig`]. @@ -193,7 +193,6 @@ mod actix; mod axum; mod rocket; -#[doc(hidden)] const DEFAULT_HTML: &str = include_str!("../res/redoc.html"); /// Trait makes [`Redoc`] to accept an _`URL`_ the [Redoc][redoc] will be served via predefined web diff --git a/utoipa-swagger-ui/Cargo.toml b/utoipa-swagger-ui/Cargo.toml index 8c21ee9e..2658cf6e 100644 --- a/utoipa-swagger-ui/Cargo.toml +++ b/utoipa-swagger-ui/Cargo.toml @@ -17,9 +17,7 @@ debug-embed = ["rust-embed/debug-embed"] [dependencies] rust-embed = { version = "6.6", features = ["interpolate-folder-path"] } mime_guess = { version = "2.0" } -actix-web = { version = "4", features = [ - "macros", -], optional = true, default-features = false } +actix-web = { version = "4", optional = true, default-features = false } rocket = { version = "0.5.0-rc.3", features = ["json"], optional = true } axum = { version = "0.6", optional = true } utoipa = { version = "3", path = "../utoipa" }