diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a684caa7..348cdc84 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -23,6 +23,7 @@ jobs: - utoipa-swagger-ui - utoipa-redoc - utoipa-rapidoc + - utoipa-scalar fail-fast: true runs-on: ubuntu-latest @@ -56,6 +57,8 @@ jobs: changes=true elif [[ "$change" == "utoipa-rapidoc" && "${{ matrix.crate }}" == "utoipa-rapidoc" && $changes == false ]]; then changes=true + elif [[ "$change" == "utoipa-scalar" && "${{ matrix.crate }}" == "utoipa-scalar" && $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 d14cb68c..68be0983 100644 --- a/.github/workflows/draft.yaml +++ b/.github/workflows/draft.yaml @@ -18,6 +18,7 @@ jobs: - utoipa-swagger-ui - utoipa-redoc - utoipa-rapidoc + - utoipa-scalar runs-on: ubuntu-latest steps: diff --git a/Cargo.toml b/Cargo.toml index 62709bbe..91083abb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "utoipa-swagger-ui", "utoipa-redoc", "utoipa-rapidoc", + "utoipa-scalar", ] [workspace.metadata.publish] @@ -15,4 +16,5 @@ order = [ "utoipa-swagger-ui", "utoipa-redoc", "utoipa-rapidoc", + "utoipa-scalar", ] diff --git a/examples/README.md b/examples/README.md index 3f91b10e..8c68d0cb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,5 +8,5 @@ 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` or `http://localhost:8080/rapidoc`. -`Todo-actix`, `todo-axum` and `rocket-todo` have Swagger UI, Redoc and RapiDoc setup, others have Swagger UI +`todo-actix`, `todo-axum` and `rocket-todo` have Swagger UI, Redoc, RapiDoc, and Scalar 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 8f5c10dd..4a2bc59d 100644 --- a/examples/rocket-todo/Cargo.toml +++ b/examples/rocket-todo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket-todo" -description = "Simple rocket todo example api with utoipa and Swagger UI and Redoc" +description = "Simple rocket todo example api with utoipa and Swagger UI, Rapidoc, Redoc, and Scalar" version = "0.1.0" edition = "2021" license = "MIT" @@ -14,6 +14,7 @@ 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"] } +utoipa-scalar = { path = "../../utoipa-scalar", 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 8fb5b4ca..e410d009 100644 --- a/examples/rocket-todo/README.md +++ b/examples/rocket-todo/README.md @@ -11,6 +11,8 @@ If you prefer Redoc just head to `http://localhost:8000/redoc` and view the Open RapiDoc can be found from `http://localhost:8000/redoc`. +Scalar can be reached on `http://localhost:8000/scalar`. + ```bash cargo run ``` diff --git a/examples/rocket-todo/src/main.rs b/examples/rocket-todo/src/main.rs index 17189401..fb3f2f78 100644 --- a/examples/rocket-todo/src/main.rs +++ b/examples/rocket-todo/src/main.rs @@ -7,6 +7,7 @@ use utoipa::{ }; use utoipa_rapidoc::RapiDoc; use utoipa_redoc::{Redoc, Servable}; +use utoipa_scalar::{Scalar, Servable as ScalarServable}; use utoipa_swagger_ui::SwaggerUi; use crate::todo::TodoStore; @@ -62,6 +63,7 @@ fn rocket() -> Rocket { // RapiDoc::with_openapi("/api-docs/openapi2.json", ApiDoc::openapi()).path("/rapidoc") // ) .mount("/", Redoc::with_url("/redoc", ApiDoc::openapi())) + .mount("/", Scalar::with_url("/scalar", ApiDoc::openapi())) .mount( "/todo", routes![ diff --git a/examples/todo-actix/Cargo.toml b/examples/todo-actix/Cargo.toml index 7e029e08..febb60f9 100644 --- a/examples/todo-actix/Cargo.toml +++ b/examples/todo-actix/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "todo-actix" -description = "Simple actix-web todo example api with utoipa and Swagger UI and Redoc" +description = "Simple actix-web todo example api with utoipa and Swagger UI, Rapidoc, Redoc, and Scalar" version = "0.1.0" edition = "2021" license = "MIT" @@ -21,5 +21,6 @@ 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"] } +utoipa-scalar = { path = "../../utoipa-scalar", features = ["actix-web"] } [workspace] diff --git a/examples/todo-actix/README.md b/examples/todo-actix/README.md index 352ff24c..0b316d8c 100644 --- a/examples/todo-actix/README.md +++ b/examples/todo-actix/README.md @@ -11,6 +11,8 @@ If you prefer Redoc just head to `http://localhost:8080/redoc` and view the Open RapiDoc can be found from `http://localhost:8080/rapidoc`. +Scalar can be reached on `http://localhost:8080/scalar`. + ```bash cargo run ``` diff --git a/examples/todo-actix/src/main.rs b/examples/todo-actix/src/main.rs index a02b7bc8..deea73f5 100644 --- a/examples/todo-actix/src/main.rs +++ b/examples/todo-actix/src/main.rs @@ -17,6 +17,7 @@ use utoipa::{ }; use utoipa_rapidoc::RapiDoc; use utoipa_redoc::{Redoc, Servable}; +use utoipa_scalar::{Scalar, Servable as ScalarServable}; use utoipa_swagger_ui::SwaggerUi; use crate::todo::{ErrorResponse, TodoStore}; @@ -81,6 +82,7 @@ async fn main() -> Result<(), impl Error> { // If we wanted to serve the schema, the following would work: // .service(RapiDoc::with_openapi("/api-docs/openapi2.json", openapi.clone()).path("/rapidoc")) .service(RapiDoc::new("/api-docs/openapi.json").path("/rapidoc")) + .service(Scalar::with_url("/scalar", openapi.clone())) }) .bind((Ipv4Addr::UNSPECIFIED, 8080))? .run() diff --git a/examples/todo-axum/Cargo.toml b/examples/todo-axum/Cargo.toml index e26d5951..dcd719cc 100644 --- a/examples/todo-axum/Cargo.toml +++ b/examples/todo-axum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "todo-axum" -description = "Simple axum todo example api with utoipa and Swagger UI and Redoc" +description = "Simple axum todo example api with utoipa and Swagger UI, Rapidoc, Redoc, and Scalar" version = "0.1.0" edition = "2021" license = "MIT" @@ -19,6 +19,7 @@ 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"] } +utoipa-scalar = { path = "../../utoipa-scalar", 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 a265d3ce..aacc7b4e 100644 --- a/examples/todo-axum/README.md +++ b/examples/todo-axum/README.md @@ -11,6 +11,8 @@ If you prefer Redoc just head to `http://localhost:8080/redoc` and view the Open RapiDoc can be found from `http://localhost:8080/rapidoc`. +Scalar can be reached on `http://localhost:8080/scalar`. + ```bash cargo run ``` diff --git a/examples/todo-axum/src/main.rs b/examples/todo-axum/src/main.rs index dda235ef..399c44ff 100644 --- a/examples/todo-axum/src/main.rs +++ b/examples/todo-axum/src/main.rs @@ -12,6 +12,7 @@ use utoipa::{ }; use utoipa_rapidoc::RapiDoc; use utoipa_redoc::{Redoc, Servable}; +use utoipa_scalar::{Scalar, Servable as ScalarServable}; use utoipa_swagger_ui::SwaggerUi; use crate::todo::Store; @@ -59,6 +60,7 @@ async fn main() -> Result<(), Error> { .merge(RapiDoc::new("/api-docs/openapi.json").path("/rapidoc")) // Alternative to above // .merge(RapiDoc::with_openapi("/api-docs/openapi2.json", ApiDoc::openapi()).path("/rapidoc")) + .merge(Scalar::with_url("/scalar", ApiDoc::openapi())) .route( "/todo", routing::get(todo::list_todos).post(todo::create_todo), diff --git a/scripts/test.sh b/scripts/test.sh index eb5ffb8a..7898ea13 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -29,5 +29,7 @@ for crate in $crates; do $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 + elif [[ "$crate" == "utoipa-scalar" ]]; then + $CARGO test -p utoipa-scalar --features actix-web,rocket,axum fi done diff --git a/utoipa-scalar/Cargo.toml b/utoipa-scalar/Cargo.toml new file mode 100644 index 00000000..a9986e86 --- /dev/null +++ b/utoipa-scalar/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "utoipa-scalar" +description = "Scalar for utoipa" +version = "3.0.0" +edition = "2021" +license = "MIT OR Apache-2.0" +readme = "README.md" +keywords = ["scalar", "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 = "4", path = "../utoipa" } +actix-web = { version = "4", optional = true, default-features = false } +rocket = { version = "0.5", features = ["json"], optional = true } +axum = { version = "0.7", default-features = false, optional = true } diff --git a/utoipa-scalar/LICENSE-APACHE b/utoipa-scalar/LICENSE-APACHE new file mode 100644 index 00000000..f49a4e16 --- /dev/null +++ b/utoipa-scalar/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/utoipa-scalar/LICENSE-MIT b/utoipa-scalar/LICENSE-MIT new file mode 100644 index 00000000..63e5114b --- /dev/null +++ b/utoipa-scalar/LICENSE-MIT @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright © 2024 + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/utoipa-scalar/README.md b/utoipa-scalar/README.md new file mode 100644 index 00000000..7434a696 --- /dev/null +++ b/utoipa-scalar/README.md @@ -0,0 +1,102 @@ +# utoipa-scalar + +[![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-scalar.svg?label=crates.io&color=orange&logo=rust)](https://crates.io/crates/utoipa-scalar) +[![docs.rs](https://img.shields.io/static/v1?label=docs.rs&message=utoipa-scalar&color=blue&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K)](https://docs.rs/utoipa-scalar/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 [Scalar](https://scalar.com/) OpenAPI visualizer. + +Utoipa-scalar 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 `Scalar` via _**`actix-web`**_. `version >= 4` +* **rocket** Allows serving `Scalar` via _**`rocket`**_. `version >=0.5` +* **axum** Allows serving `Scalar` via _**`axum`**_. `version >=0.7` + +# Install + +Use Scalar only without any boiler plate implementation. +```toml +[dependencies] +utoipa-scalar = "3" +``` + +Enable actix-web integration with Scalar. +```toml +[dependencies] +utoipa-scalar = { version = "3", features = ["actix-web"] } +``` + +# Using standalone + +Utoipa-scalar can be used standalone as simply as creating a new `Scalar` instance and then +serving it by what ever means available as `text/html` from http handler in your favourite web +framework. + +`Scalar::to_html` method can be used to convert the `Scalar` instance to a servable html +file. +```rust +let scalar = Scalar::new(ApiDoc::openapi()); + +// Then somewhere in your application that handles http operation. +// Make sure you return correct content type `text/html`. +let scalar = move || async { + scalar.to_html() +}; +``` + +# Examples + +_**Serve `Scalar` via `actix-web` framework.**_ +```rust +use actix_web::App; +use utoipa_scalar::{Scalar, Servable}; + +App::new().service(Scalar::with_url("/scalar", ApiDoc::openapi())); +``` + +_**Serve `Scalar` via `rocket` framework.**_ +```rust +use utoipa_scalar::{Scalar, Servable}; + +rocket::build() + .mount( + "/", + Scalar::with_url("/scalar", ApiDoc::openapi()), + ); +``` + +_**Serve `Scalar` via `axum` framework.**_ + ```rust + use axum::Router; + use utoipa_scalar::{Scalar, Servable}; + + let app = Router::::new() + .merge(Scalar::with_url("/scalar", ApiDoc::openapi())); +``` + +_**Use `Scalar` to serve OpenAPI spec from url.**_ +```rust +Scalar::new( + "https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml") +``` + +_**Use `Scalar` to serve custom OpenAPI spec using serde's `json!()` macro.**_ +```rust +Scalar::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. + +[examples]: diff --git a/utoipa-scalar/res/scalar.html b/utoipa-scalar/res/scalar.html new file mode 100644 index 00000000..e35945b4 --- /dev/null +++ b/utoipa-scalar/res/scalar.html @@ -0,0 +1,19 @@ + + + + Scalar + + + + + + + + + diff --git a/utoipa-scalar/src/actix.rs b/utoipa-scalar/src/actix.rs new file mode 100644 index 00000000..669e2ea9 --- /dev/null +++ b/utoipa-scalar/src/actix.rs @@ -0,0 +1,26 @@ +#![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::{Scalar, Spec}; + +impl HttpServiceFactory for Scalar { + fn register(self, config: &mut actix_web::dev::AppService) { + let html = self.to_html(); + + async fn serve_scalar(scalar: Data) -> impl Responder { + HttpResponse::Ok() + .content_type("text/html") + .body(scalar.to_string()) + } + + Resource::new(self.url.as_ref()) + .guard(Get()) + .app_data(Data::new(html)) + .to(serve_scalar) + .register(config); + } +} diff --git a/utoipa-scalar/src/axum.rs b/utoipa-scalar/src/axum.rs new file mode 100644 index 00000000..4d3cd421 --- /dev/null +++ b/utoipa-scalar/src/axum.rs @@ -0,0 +1,19 @@ +#![cfg(feature = "axum")] + +use axum::response::Html; +use axum::{routing, Router}; + +use crate::{Scalar, Spec}; + +impl From> for Router +where + R: Clone + Send + Sync + 'static, +{ + fn from(value: Scalar) -> Self { + let html = value.to_html(); + Router::::new().route( + value.url.as_ref(), + routing::get(move || async { Html(html) }), + ) + } +} diff --git a/utoipa-scalar/src/lib.rs b/utoipa-scalar/src/lib.rs new file mode 100644 index 00000000..bd5d5707 --- /dev/null +++ b/utoipa-scalar/src/lib.rs @@ -0,0 +1,244 @@ +#![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 [Scalar](https://scalar.com/) OpenAPI visualizer. +//! +//! Utoipa-scalar 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 [`Scalar`] via _**`actix-web`**_. +//! * **rocket** Allows serving [`Scalar`] via _**`rocket`**_. +//! * **axum** Allows serving [`Scalar`] via _**`axum`**_. +//! +//! # Install +//! +//! Use Scalar only without any boiler plate implementation. +//! ```toml +//! [dependencies] +//! utoipa-scalar = "3" +//! ``` +//! +//! Enable actix-web integration with Scalar. +//! ```toml +//! [dependencies] +//! utoipa-scalar = { version = "3", features = ["actix-web"] } +//! ``` +//! +//! # Using standalone +//! +//! Utoipa-scalar can be used standalone as simply as creating a new [`Scalar`] instance and then +//! serving it by what ever means available as `text/html` from http handler in your favourite web +//! framework. +//! +//! [`Scalar::to_html`] method can be used to convert the [`Scalar`] instance to a servable html +//! file. +//! ``` +//! # use utoipa_scalar::Scalar; +//! # use utoipa::OpenApi; +//! # use serde_json::json; +//! # #[derive(OpenApi)] +//! # #[openapi()] +//! # struct ApiDoc; +//! # +//! let scalar = Scalar::new(ApiDoc::openapi()); +//! +//! // Then somewhere in your application that handles http operation. +//! // Make sure you return correct content type `text/html`. +//! let scalar_handler = move || { +//! scalar.to_html() +//! }; +//! ``` +//! +//! # Examples +//! +//! _**Serve [`Scalar`] via `actix-web` framework.**_ +//! ```no_run +//! use actix_web::App; +//! use utoipa_scalar::{Scalar, Servable}; +//! +//! # use utoipa::OpenApi; +//! # use std::net::Ipv4Addr; +//! # #[derive(OpenApi)] +//! # #[openapi()] +//! # struct ApiDoc; +//! App::new().service(Scalar::with_url("/scalar", ApiDoc::openapi())); +//! ``` +//! +//! _**Serve [`Scalar`] via `rocket` framework.**_ +//! ```no_run +//! # use rocket; +//! use utoipa_scalar::{Scalar, Servable}; +//! +//! # use utoipa::OpenApi; +//! # #[derive(OpenApi)] +//! # #[openapi()] +//! # struct ApiDoc; +//! rocket::build() +//! .mount( +//! "/", +//! Scalar::with_url("/scalar", ApiDoc::openapi()), +//! ); +//! ``` +//! +//! _**Serve [`Scalar`] via `axum` framework.**_ +//! ```no_run +//! use axum::Router; +//! use utoipa_scalar::{Scalar, Servable}; +//! # use utoipa::OpenApi; +//! # #[derive(OpenApi)] +//! # #[openapi()] +//! # struct ApiDoc; +//! # +//! # fn inner() +//! # where +//! # S: Clone + Send + Sync + 'static, +//! # { +//! +//! let app = Router::::new() +//! .merge(Scalar::with_url("/scalar", ApiDoc::openapi())); +//! # } +//! ``` +//! +//! _**Use [`Scalar`] to serve custom OpenAPI spec using serde's `json!()` macro.**_ +//! ```rust +//! # use utoipa_scalar::Scalar; +//! # use serde_json::json; +//! Scalar::new(json!({"openapi": "3.1.0"})); +//! ``` +//! +//! [examples]: + +use std::borrow::Cow; + +use serde::Serialize; +use serde_json::Value; +use utoipa::openapi::OpenApi; + +mod actix; +mod axum; +mod rocket; + +const DEFAULT_HTML: &str = include_str!("../res/scalar.html"); + +/// Trait makes [`Scalar`] to accept an _`URL`_ the [Scalar][scalar] will be served via predefined +/// web server. +/// +/// This is used **only** with **`actix-web`**, **`rocket`** or **`axum`** since they have implicit +/// implementation for serving the [`Scalar`] via the _`URL`_. +/// +/// [scalar]: +#[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))] +#[cfg_attr( + doc_cfg, + doc(cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))) +)] +pub trait Servable +where + S: Spec, +{ + /// Construct a new [`Servable`] instance of _`openapi`_ with given _`url`_. + /// + /// * **url** Must point to location where the [`Servable`] is served. + /// * **openapi** Is [`Spec`] that is served via this [`Servable`] from the _**url**_. + fn with_url>>(url: U, openapi: S) -> Self; +} + +#[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))] +impl Servable for Scalar { + fn with_url>>(url: U, openapi: S) -> Self { + Self { + html: Cow::Borrowed(DEFAULT_HTML), + url: url.into(), + openapi, + } + } +} + +/// Is standalone instance of [Scalar][scalar]. +/// +/// This can be used together with predefined web framework integration or standalone with +/// framework of your choice. [`Scalar::to_html`] method will convert this [`Scalar`] instance to +/// servable HTML file. +/// +/// [scalar]: +#[non_exhaustive] +#[derive(Clone)] +pub struct Scalar { + #[allow(unused)] + url: Cow<'static, str>, + html: Cow<'static, str>, + openapi: S, +} + +impl Scalar { + /// Constructs a new [`Scalar`] instance for given _`openapi`_ [`Spec`]. + /// + /// # Examples + /// + /// _**Create new [`Scalar`] instance.**_ + /// ``` + /// # use utoipa_scalar::Scalar; + /// # use serde_json::json; + /// Scalar::new(json!({"openapi": "3.1.0"})); + /// ``` + pub fn new(openapi: S) -> Self { + Self { + html: Cow::Borrowed(DEFAULT_HTML), + url: Cow::Borrowed("/"), + openapi, + } + } + + /// Converts this [`Scalar`] instance to servable HTML file. + /// + /// This will replace _**`$spec`**_ variable placeholder with [`Spec`] of this instance + /// provided to this instance serializing it to JSON from the HTML template used with the + /// [`Scalar`]. + /// + /// At this point in time, it is not possible to customize the HTML template used by the + /// [`Scalar`] instance. + pub fn to_html(&self) -> String { + self.html.replace( + "$spec", + &serde_json::to_string(&self.openapi).expect( + "Invalid OpenAPI spec, expected OpenApi, String, &str or serde_json::Value", + ), + ) + } +} + +/// Trait defines OpenAPI spec resource types supported by [`Scalar`]. +/// +/// By default this trait is implemented for [`utoipa::openapi::OpenApi`] and [`serde_json::Value`]. +/// +/// * **OpenApi** implementation allows using utoipa's OpenApi struct as a OpenAPI spec resource +/// for the [`Scalar`]. +/// * **Value** implementation enables the use of arbitrary JSON values with serde's `json!()` +/// macro as a OpenAPI spec for the [`Scalar`]. +/// +/// # Examples +/// +/// _**Use [`Scalar`] to serve utoipa's OpenApi.**_ +/// ```no_run +/// # use utoipa_scalar::Scalar; +/// # use utoipa::openapi::OpenApiBuilder; +/// # +/// Scalar::new(OpenApiBuilder::new().build()); +/// ``` +/// +/// _**Use [`Scalar`] to serve custom OpenAPI spec using serde's `json!()` macro.**_ +/// ```rust +/// # use utoipa_scalar::Scalar; +/// # use serde_json::json; +/// Scalar::new(json!({"openapi": "3.1.0"})); +/// ``` +pub trait Spec: Serialize {} + +impl Spec for OpenApi {} + +impl Spec for Value {} diff --git a/utoipa-scalar/src/rocket.rs b/utoipa-scalar/src/rocket.rs new file mode 100644 index 00000000..50e5039c --- /dev/null +++ b/utoipa-scalar/src/rocket.rs @@ -0,0 +1,28 @@ +#![cfg(feature = "rocket")] + +use rocket::http::Method; +use rocket::response::content::RawHtml; +use rocket::route::{Handler, Outcome}; +use rocket::{Data, Request, Route}; + +use crate::{Scalar, Spec}; + +impl From> for Vec { + fn from(value: Scalar) -> Self { + vec![Route::new( + Method::Get, + value.url.as_ref(), + ScalarHandler(value.to_html()), + )] + } +} + +#[derive(Clone)] +struct ScalarHandler(String); + +#[rocket::async_trait] +impl Handler for ScalarHandler { + async fn handle<'r>(&self, request: &'r Request<'_>, _: Data<'r>) -> Outcome<'r> { + Outcome::from(request, RawHtml(self.0.clone())) + } +}