diff --git a/examples/README.md b/examples/README.md index a676187..2ac6cec 100644 --- a/examples/README.md +++ b/examples/README.md @@ -21,3 +21,74 @@ web framework which supports decorating functions with macros similarly to the * - **[viz](https://github.com/viz-rs/viz/tree/main/examples/routing/openapi)** - **[ntex](https://github.com/leon3s/ntex-rest-api-example)** +## Examples Directory Structure + +``` +examples/ +├── README.md # High-level overview of all examples +├── actix-web/ # Grouping examples by framework/library +│ ├── README.md # Description of Actix-specific examples +│ ├── multiple-api-docs/ +│ │ ├── Cargo.toml +│ │ ├── src/ +│ │ └── README.md +│ └── scopes-binding/ +│ ├── Cargo.toml +│ ├── src/ +│ └── README.md +├── axum/ +│ ├── README.md +│ ├── fastapi-bindings/ +│ │ ├── Cargo.toml +│ │ ├── src/ +│ │ └── README.md +│ └── nesting-vendored/ +│ ├── Cargo.toml +│ ├── src/ +│ ├── Dockerfile +│ └── README.md +├── generic/ +│ ├── README.md # For framework-agnostic examples +│ ├── raw-json/ +│ │ ├── Cargo.toml +│ │ ├── src/ +│ │ └── README.md +│ └── todo/ +│ ├── actix/ +│ │ ├── Cargo.toml +│ │ ├── src/ +│ │ └── README.md +│ ├── axum/ +│ │ ├── Cargo.toml +│ │ ├── src/ +│ │ └── README.md +│ ├── tide/ +│ │ ├── Cargo.toml +│ │ ├── src/ +│ │ └── README.md +│ ├── warp/ +│ │ ├── Cargo.toml +│ │ ├── src/ +│ │ └── README.md +│ └── warp-rapidoc/ +│ ├── Cargo.toml +│ ├── src/ +│ └── README.md +├── rocket/ +│ ├── README.md +│ ├── todo/ +│ ├── Cargo.toml +│ ├── src/ +│ └── README.md +└── warp/ + ├── README.md + ├── multiple-api-docs/ + │ ├── Cargo.toml + │ ├── src/ + │ └── README.md + └── redoc-with-file-config/ + ├── Cargo.toml + ├── src/ + ├── redoc.json + └── README.md +``` diff --git a/examples/actix-web-multiple-api-docs-with-scopes/Cargo.toml b/examples/actix-web-multiple-api-docs-with-scopes/Cargo.toml deleted file mode 100644 index 0bd6448..0000000 --- a/examples/actix-web-multiple-api-docs-with-scopes/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "actix-web-multiple-api-docs-with-scopes" -description = "Simple actix-web with multiple scoped api docs" -version = "0.1.0" -edition = "2021" -license = "MIT" -authors = [ - "Example " -] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -actix-web = "4" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -env_logger = "0.11.0" -log = "0.4" -futures = "0.3" -fastapi = { path = "../../fastapi", features = ["actix_extras"] } -fastapi-swagger-ui = { path = "../../fastapi-swagger-ui", features = ["actix-web"] } - -[workspace] diff --git a/examples/actix-web-multiple-api-docs-with-scopes/README.md b/examples/actix-web-multiple-api-docs-with-scopes/README.md deleted file mode 100644 index 62a30e6..0000000 --- a/examples/actix-web-multiple-api-docs-with-scopes/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# actix-web-multiple-api-docs-with-scopes ~ fastapi with fastapi-swagger-ui example - -This is a demo `actix-web` application with multiple API docs with scope and context path. - -Just run command below to run the demo application and browse to `http://localhost:8080/swagger-ui/`. - -```bash -cargo run -``` - -On the Swagger-UI will be a drop-down labelled "Select a definition", containing "api1" and "api2". - -Alternatively, they can be loaded directly using - -- api1: http://localhost:8080/swagger-ui/?urls.primaryName=api1 -- api2: http://localhost:8080/swagger-ui/?urls.primaryName=api1 - -If you want to see some logging, you may prepend the command with `RUST_LOG=debug` as shown below. - -```bash -RUST_LOG=debug cargo run -``` diff --git a/examples/actix-web-multiple-api-docs-with-scopes/src/main.rs b/examples/actix-web-multiple-api-docs-with-scopes/src/main.rs deleted file mode 100644 index 0b39487..0000000 --- a/examples/actix-web-multiple-api-docs-with-scopes/src/main.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::{error::Error, net::Ipv4Addr}; - -use actix_web::{middleware::Logger, web, App, HttpServer}; -use fastapi::OpenApi; -use fastapi_swagger_ui::{SwaggerUi, Url}; - -#[actix_web::main] -async fn main() -> Result<(), impl Error> { - env_logger::init(); - - #[derive(OpenApi)] - #[openapi(paths(api1::hello1))] - struct ApiDoc1; - - #[derive(OpenApi)] - #[openapi(paths(api2::hello2))] - struct ApiDoc2; - - HttpServer::new(move || { - App::new() - .wrap(Logger::default()) - .service( - web::scope("/api") - .service(api1::hello1) - .service(api2::hello2), - ) - .service(SwaggerUi::new("/swagger-ui/{_:.*}").urls(vec![ - ( - Url::new("api1", "/api-docs/openapi1.json"), - ApiDoc1::openapi(), - ), - ( - Url::with_primary("api2", "/api-docs/openapi2.json", true), - ApiDoc2::openapi(), - ), - ])) - }) - .bind((Ipv4Addr::UNSPECIFIED, 8080))? - .run() - .await -} - -mod api1 { - use actix_web::get; - - #[fastapi::path( - context_path = "/api", - responses( - (status = 200, description = "Hello from api 1", body = String) - ) - )] - #[get("/api1/hello")] - pub(super) async fn hello1() -> String { - "hello from api 1".to_string() - } -} - -mod api2 { - use actix_web::get; - - #[fastapi::path( - context_path = "/api", - responses( - (status = 200, description = "Hello from api 2", body = String) - ) - )] - #[get("/api2/hello")] - pub(super) async fn hello2() -> String { - "hello from api 2".to_string() - } -} diff --git a/examples/actix-web-scopes-binding/Cargo.toml b/examples/actix-web-scopes-binding/Cargo.toml deleted file mode 100644 index d70d376..0000000 --- a/examples/actix-web-scopes-binding/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "actix-web-scopes-binding" -description = "Simple actix-web demo to demonstrate fastapi-actix-web bidings with scopes" -version = "0.1.0" -edition = "2021" - -[dependencies] -actix-web = "4" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -env_logger = "0.11.0" -log = "0.4" -futures = "0.3" -fastapi = { path = "../../fastapi", features = ["actix_extras"] } -fastapi-swagger-ui = { path = "../../fastapi-swagger-ui", features = [ - "actix-web", -] } -fastapi-actix-web = { path = "../../fastapi-actix-web" } - -[workspace] diff --git a/examples/actix-web-scopes-binding/README.md b/examples/actix-web-scopes-binding/README.md deleted file mode 100644 index db8d430..0000000 --- a/examples/actix-web-scopes-binding/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# actix-web-scopes-binding - - -This is a demo `actix-web` application with [`fastapi-actix-web`][../../fastapi-actix-web] bindings demonstrating scopes support. - -Run command below to run the demo application and browse to `http://localhost:8080/swagger-ui/`. - -```bash -cargo run -``` - -Or with more logs -```bash -RUST_LOG=debug cargo run -``` diff --git a/examples/actix-web-scopes-binding/src/main.rs b/examples/actix-web-scopes-binding/src/main.rs deleted file mode 100644 index 1380346..0000000 --- a/examples/actix-web-scopes-binding/src/main.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::{error::Error, net::Ipv4Addr}; - -use actix_web::{middleware::Logger, App, HttpServer}; -use fastapi_actix_web::{scope, AppExt}; -use fastapi_swagger_ui::SwaggerUi; - -#[actix_web::main] -async fn main() -> Result<(), impl Error> { - env_logger::init(); - - HttpServer::new(move || { - let (app, api) = App::new() - .into_fastapi_app() - .map(|app| app.wrap(Logger::default())) - .service( - scope::scope("/api") - .service(scope::scope("/v1").service(api1::hello1)) - .service(scope::scope("/v2").service(api2::hello2)), - ) - .split_for_parts(); - - app.service(SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", api)) - }) - .bind((Ipv4Addr::UNSPECIFIED, 8080))? - .run() - .await -} - -mod api1 { - use actix_web::get; - - #[fastapi::path( - responses( - (status = 200, description = "Hello from api 1", body = str) - ) - )] - #[get("/hello")] - pub(super) async fn hello1() -> &'static str { - "hello from api 1" - } -} - -mod api2 { - use actix_web::get; - - #[fastapi::path( - responses( - (status = 200, description = "Hello from api 2", body = str) - ) - )] - #[get("/hello")] - pub(super) async fn hello2() -> &'static str { - "hello from api 2" - } -} diff --git a/examples/axum-fastapi-bindings/Cargo.toml b/examples/axum-fastapi-bindings/Cargo.toml deleted file mode 100644 index 89fa0c0..0000000 --- a/examples/axum-fastapi-bindings/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "axum-fastapi-bindings" -version = "0.1.0" -edition = "2021" - -[dependencies] -axum = "0.7" -tokio = { version = "1", features = ["full"] } -tower = "0.5" -fastapi = { path = "../../fastapi", features = ["axum_extras", "debug"] } -fastapi-swagger-ui = { path = "../../fastapi-swagger-ui", features = ["axum"] } -fastapi-axum = { path = "../../fastapi-axum" ,features = ["debug"] } -serde = "1" -serde_json = "1" - -[workspace] diff --git a/examples/axum-fastapi-bindings/README.md b/examples/axum-fastapi-bindings/README.md deleted file mode 100644 index 42208c6..0000000 --- a/examples/axum-fastapi-bindings/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# fastapi with axum bindings - -This demo `axum` application demonstrates `fastapi` and `axum` seamless integration with `fastapi-axum` crate. -API doc is served via Swagger UI. - -Run the app -```bash -cargo run -``` - -Browse the API docs. -``` -http://localhost:8080/swagger-ui -``` diff --git a/examples/axum-fastapi-bindings/src/main.rs b/examples/axum-fastapi-bindings/src/main.rs deleted file mode 100644 index a982f1d..0000000 --- a/examples/axum-fastapi-bindings/src/main.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::io; -use std::net::Ipv4Addr; - -use fastapi::OpenApi; -use fastapi_axum::router::OpenApiRouter; -use fastapi_axum::routes; -use fastapi_swagger_ui::SwaggerUi; -use tokio::net::TcpListener; - -const CUSTOMER_TAG: &str = "customer"; -const ORDER_TAG: &str = "order"; - -#[derive(OpenApi)] -#[openapi( - tags( - (name = CUSTOMER_TAG, description = "Customer API endpoints"), - (name = ORDER_TAG, description = "Order API endpoints") - ) -)] -struct ApiDoc; - -/// Get health of the API. -#[fastapi::path( - method(get, head), - path = "/api/health", - responses( - (status = OK, description = "Success", body = str, content_type = "text/plain") - ) -)] -async fn health() -> &'static str { - "ok" -} - -#[tokio::main] -async fn main() -> Result<(), io::Error> { - let (router, api) = OpenApiRouter::with_openapi(ApiDoc::openapi()) - .routes(routes!(health)) - .nest("/api/customer", customer::router()) - .nest("/api/order", order::router()) - .routes(routes!( - inner::secret_handlers::get_secret, - inner::secret_handlers::post_secret - )) - .split_for_parts(); - - let router = router.merge(SwaggerUi::new("/swagger-ui").url("/apidoc/openapi.json", api)); - - let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 8080)).await?; - axum::serve(listener, router).await -} - -mod customer { - use axum::Json; - use fastapi::ToSchema; - use fastapi_axum::router::OpenApiRouter; - use fastapi_axum::routes; - use serde::Serialize; - - /// This is the customer - #[derive(ToSchema, Serialize)] - struct Customer { - name: String, - } - - /// expose the Customer OpenAPI to parent module - pub fn router() -> OpenApiRouter { - OpenApiRouter::new().routes(routes!(get_customer)) - } - - /// Get customer - /// - /// Just return a static Customer object - #[fastapi::path(get, path = "", responses((status = OK, body = Customer)), tag = super::CUSTOMER_TAG)] - async fn get_customer() -> Json { - Json(Customer { - name: String::from("Bill Book"), - }) - } -} - -mod order { - use axum::Json; - use fastapi::ToSchema; - use fastapi_axum::router::OpenApiRouter; - use fastapi_axum::routes; - use serde::{Deserialize, Serialize}; - - /// This is the order - #[derive(ToSchema, Serialize)] - struct Order { - id: i32, - name: String, - } - - #[derive(ToSchema, Deserialize, Serialize)] - struct OrderRequest { - name: String, - } - - /// expose the Order OpenAPI to parent module - pub fn router() -> OpenApiRouter { - OpenApiRouter::new().routes(routes!(get_order, create_order)) - } - - /// Get static order object - #[fastapi::path(get, path = "", responses((status = OK, body = Order)), tag = super::ORDER_TAG)] - async fn get_order() -> Json { - Json(Order { - id: 100, - name: String::from("Bill Book"), - }) - } - - /// Create an order. - /// - /// Create an order by basically passing through the name of the request with static id. - #[fastapi::path(post, path = "", responses((status = OK, body = Order)), tag = super::ORDER_TAG)] - async fn create_order(Json(order): Json) -> Json { - Json(Order { - id: 120, - name: order.name, - }) - } -} - -mod inner { - pub mod secret_handlers { - - /// This is some secret inner handler - #[fastapi::path(get, path = "/api/inner/secret", responses((status = OK, body = str)))] - pub async fn get_secret() -> &'static str { - "secret" - } - - /// Post some secret inner handler - #[fastapi::path(post, path = "/api/inner/secret", responses((status = OK)))] - pub async fn post_secret() { - println!("You posted a secret") - } - } -} diff --git a/examples/axum-fastapi-nesting-vendored/Cargo.toml b/examples/axum-fastapi-nesting-vendored/Cargo.toml deleted file mode 100644 index 6db5ced..0000000 --- a/examples/axum-fastapi-nesting-vendored/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "axum-fastapi-nesting" -description = "Axum nesting example with Swagger UI" -version = "0.1.0" -edition = "2021" -license = "MIT" -authors = ["Elli Example "] - - -[dependencies] -axum = "0.7" -hyper = { version = "1.0.1", features = ["full"] } -tokio = { version = "1.17", features = ["full"] } -tower = "0.5" -fastapi = { path = "../../fastapi", features = ["axum_extras"] } -fastapi-swagger-ui = { path = "../../fastapi-swagger-ui", features = ["axum", "vendored"], default-features = false } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -env_logger = "0.11" -log = "0.4" - -[workspace] diff --git a/examples/axum-fastapi-nesting-vendored/Dockerfile b/examples/axum-fastapi-nesting-vendored/Dockerfile deleted file mode 100644 index 689a1f0..0000000 --- a/examples/axum-fastapi-nesting-vendored/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM alpine:3.14 - -COPY target/x86_64-unknown-linux-musl/release/axum-fastapi-nesting /axum-fastapi-nesting - -ENTRYPOINT [ "/axum-fastapi-nesting" ] diff --git a/examples/axum-fastapi-nesting-vendored/README.md b/examples/axum-fastapi-nesting-vendored/README.md deleted file mode 100644 index e2b90a0..0000000 --- a/examples/axum-fastapi-nesting-vendored/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# axum-nesting-vendored ~ fastapi with fastapi-swagger-ui example - -This example demonstrates `axum` with programmatic and macro based nesting of OpenApis -using `fastapi-swagger-ui` for visualization. - -Example uses `fastapi-swagger-ui-vendored` to demonstrate vendored version of Swagger UI. - -Just run command below to run the demo application and browse to `http://localhost:8080/swagger-ui/`. - -```bash -cargo run -``` - -## Run with Docker - -You have to build the crate with `--release` or set `debug-embed` in order to embed Swagger UI. -```bash -cargo build --release --target x86_64-unknown-linux-musl -docker build -t axum-fastapi-nesting:latest . -docker run -p 8080:8080 -t axum-fastapi-nesting:latest -``` diff --git a/examples/axum-fastapi-nesting-vendored/src/main.rs b/examples/axum-fastapi-nesting-vendored/src/main.rs deleted file mode 100644 index 6c87d54..0000000 --- a/examples/axum-fastapi-nesting-vendored/src/main.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::net::{Ipv4Addr, SocketAddr}; - -use axum::{routing, Router}; -use fastapi::openapi::path::Operation; -use fastapi::openapi::{OpenApiBuilder, PathItem, PathsBuilder}; -use fastapi::OpenApi; -use fastapi_swagger_ui::SwaggerUi; -use std::io::Error; -use tokio::net::TcpListener; - -#[tokio::main] -async fn main() -> Result<(), Error> { - #[derive(OpenApi)] - #[openapi( - nest( - // you can nest sub apis here - (path = "/api/v1/ones", api = one::OneApi) - ) - )] - struct ApiDoc; - - #[derive(OpenApi)] - #[openapi()] - struct HelloApi; - - let hello_api = - Into::::into(HelloApi::openapi()).paths(PathsBuilder::new().path( - "", - PathItem::new(fastapi::openapi::HttpMethod::Get, Operation::new()), - )); - - let mut doc = ApiDoc::openapi(); - doc = doc.nest("/hello", hello_api); // you can even nest programmatically apis - - let app = Router::new() - .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", doc)) - .route("/hello", routing::get(|| async { "hello" })) - .nest("/api/v1/ones", one::router()); - - let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 8080)); - let listener = TcpListener::bind(&address).await?; - axum::serve(listener, app.into_make_service()).await -} - -mod one { - use axum::{routing, Router}; - use fastapi::OpenApi; - - #[derive(OpenApi)] - #[openapi(paths(get_one))] - pub(super) struct OneApi; - - pub(super) fn router() -> Router { - Router::new().route("/one", routing::get(get_one)) - } - - #[fastapi::path( - get, - path = "/one", - responses( - (status = OK, description = "One result ok", body = str) - ) - )] - async fn get_one() -> &'static str { - "one" - } -} diff --git a/examples/raw-json-actix/Cargo.toml b/examples/raw-json-actix/Cargo.toml deleted file mode 100644 index 437c58e..0000000 --- a/examples/raw-json-actix/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "raw-json-actix" -description = "Simple actix-web using raw JSON with fastapi and Swagger" -version = "0.1.0" -edition = "2021" -license = "MIT" -authors = [ - "Example " -] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -actix-web = "4" -env_logger = "0.10.0" -serde_json = "1.0" -fastapi = { path = "../../fastapi", features = ["actix_extras"] } -fastapi-swagger-ui = { path = "../../fastapi-swagger-ui", features = ["actix-web"] } - -[workspace] diff --git a/examples/raw-json-actix/README.md b/examples/raw-json-actix/README.md deleted file mode 100644 index f696dc3..0000000 --- a/examples/raw-json-actix/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# raw-json-actix - -This is a demo `actix-web` application showing using raw JSON in endpoints. -The API demonstrates `fastapi` with `fastapi-swagger-ui` functionalities. - -Just run command below to run the demo application and browse to `http://localhost:8080/swagger-ui/`. - -```bash -cargo run -``` - -In the swagger UI: - -1. Send body `"string"` and the console will show the body was a `serde_json::String`. -2. Send body `1` and the console will show the body was a `serde_json::Number`. -3. Send body `[1, 2]` and the console will show the body was a `serde_json::Array`. - -If you want to see some logging, you may prepend the command with `RUST_LOG=debug` as shown below. - -```bash -RUST_LOG=debug cargo run -``` diff --git a/examples/raw-json-actix/src/main.rs b/examples/raw-json-actix/src/main.rs deleted file mode 100644 index 722b2de..0000000 --- a/examples/raw-json-actix/src/main.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::{error::Error, net::Ipv4Addr}; - -use actix_web::{ - middleware::Logger, patch, web::Json, App, HttpResponse, HttpServer, Responder, Result, -}; -use fastapi::OpenApi; -use fastapi_swagger_ui::SwaggerUi; -use serde_json::Value; - -#[fastapi::path( - request_body = Value, - responses( - (status = 200, description = "Patch completed"), - (status = 406, description = "Not accepted"), - ), - security( - ("api_key" = []) - ), -)] -#[patch("/patch_raw")] -pub async fn patch_raw(body: Json) -> Result { - let value: Value = body.into_inner(); - eprintln!("body = {:?}", value); - Ok(HttpResponse::Ok()) -} - -#[actix_web::main] -async fn main() -> Result<(), impl Error> { - env_logger::init(); - - #[derive(OpenApi)] - #[openapi(paths(patch_raw))] - struct ApiDoc; - - let openapi = ApiDoc::openapi(); - - HttpServer::new(move || { - App::new() - .wrap(Logger::default()) - .service(patch_raw) - .service( - SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", openapi.clone()), - ) - }) - .bind((Ipv4Addr::UNSPECIFIED, 8080))? - .run() - .await -} diff --git a/examples/rocket-todo/Cargo.toml b/examples/rocket-todo/Cargo.toml deleted file mode 100644 index 13e4813..0000000 --- a/examples/rocket-todo/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "rocket-todo" -description = "Simple rocket todo example api with fastapi and Swagger UI, Rapidoc, Redoc, and Scalar" -version = "0.1.0" -edition = "2021" -license = "MIT" -authors = ["Elli Example "] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -rocket = { version = "0.5", features = ["json"] } -fastapi = { path = "../../fastapi", features = ["rocket_extras"] } -fastapi-swagger-ui = { path = "../../fastapi-swagger-ui", features = ["rocket"] } -fastapi-redoc = { path = "../../fastapi-redoc", features = ["rocket"] } -fastapi-rapidoc = { path = "../../fastapi-rapidoc", features = ["rocket"] } -fastapi-scalar = { path = "../../fastapi-scalar", features = ["rocket"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -log = "0.4" - -[workspace] diff --git a/examples/rocket-todo/README.md b/examples/rocket-todo/README.md deleted file mode 100644 index 18f224b..0000000 --- a/examples/rocket-todo/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# todo-rocket ~ fastapi with fastapi-swagger-ui, fastapi-redoc and fastapi-rapidoc example - -This is a demo `rocket` application with in-memory storage to manage Todo items. The API -demonstrates `fastapi` with `fastapi-swagger-ui` functionalities. - -For security restricted endpoints the super secret API key is: `fastapi-rocks`. - -Just run command below to run the demo application and browse to `http://localhost:8000/swagger-ui/`. - -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`. - -Scalar can be reached on `http://localhost:8000/scalar`. - -```bash -cargo run -``` - -If you want to see some logging, you may prepend the command with `RUST_LOG=debug` as shown below. - -```bash -RUST_LOG=debug cargo run -``` diff --git a/examples/rocket-todo/src/main.rs b/examples/rocket-todo/src/main.rs deleted file mode 100644 index fc504de..0000000 --- a/examples/rocket-todo/src/main.rs +++ /dev/null @@ -1,314 +0,0 @@ -use fastapi::{ - openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, - Modify, OpenApi, -}; -use fastapi_rapidoc::RapiDoc; -use fastapi_redoc::{Redoc, Servable}; -use fastapi_scalar::{Scalar, Servable as ScalarServable}; -use fastapi_swagger_ui::SwaggerUi; -use rocket::{catch, catchers, routes, Build, Request, Rocket}; -use serde_json::json; -use todo::RequireApiKey; - -use crate::todo::TodoStore; - -#[rocket::launch] -fn rocket() -> Rocket { - #[derive(OpenApi)] - #[openapi( - nest( - (path = "/api/todo", api = todo::TodoApi) - ), - tags( - (name = "todo", description = "Todo management endpoints.") - ), - modifiers(&SecurityAddon) - )] - struct ApiDoc; - - struct SecurityAddon; - - impl Modify for SecurityAddon { - fn modify(&self, openapi: &mut fastapi::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"))), - ) - } - } - - rocket::build() - .manage(TodoStore::default()) - .register("/api/todo", catchers![unauthorized]) - .mount( - "/", - 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("/", Scalar::with_url("/scalar", ApiDoc::openapi())) - .mount( - "/api/todo", - routes![ - todo::get_tasks, - todo::create_todo, - todo::mark_done, - todo::delete_todo, - todo::search_todos - ], - ) -} - -#[catch(401)] -async fn unauthorized(req: &Request<'_>) -> serde_json::Value { - let (_, todo_error) = req.guard::().await.failed().unwrap(); - - json!(todo_error) -} - -mod todo { - use std::sync::{Arc, Mutex}; - - use fastapi::{IntoParams, OpenApi, ToSchema}; - use rocket::{ - delete, get, - http::Status, - outcome::Outcome, - post, put, - request::{self, FromRequest}, - response::{status::Custom, Responder}, - serde::json::Json, - FromForm, Request, State, - }; - use serde::{Deserialize, Serialize}; - - #[derive(OpenApi)] - #[openapi(paths(get_tasks, create_todo, mark_done, delete_todo, search_todos,))] - pub struct TodoApi; - - pub(super) type TodoStore = Arc>>; - - /// Todo operation error. - #[derive(Serialize, ToSchema, Responder, Debug)] - pub(super) enum TodoError { - /// When there is conflict creating a new todo. - #[response(status = 409)] - Conflict(String), - - /// When todo item is not found from storage. - #[response(status = 404)] - NotFound(String), - - /// When unauthorized to complete operation - #[response(status = 401)] - Unauthorized(String), - } - - pub(super) struct RequireApiKey; - - #[rocket::async_trait] - impl<'r> FromRequest<'r> for RequireApiKey { - type Error = TodoError; - - async fn from_request(request: &'r Request<'_>) -> request::Outcome { - match request.headers().get("todo_apikey").next() { - Some("fastapi-rocks") => Outcome::Success(RequireApiKey), - None => Outcome::Error(( - Status::Unauthorized, - TodoError::Unauthorized(String::from("missing api key")), - )), - _ => Outcome::Error(( - Status::Unauthorized, - TodoError::Unauthorized(String::from("invalid api key")), - )), - } - } - } - - pub(super) struct LogApiKey; - - #[rocket::async_trait] - impl<'r> FromRequest<'r> for LogApiKey { - type Error = TodoError; - - async fn from_request(request: &'r Request<'_>) -> request::Outcome { - match request.headers().get("todo_apikey").next() { - Some("fastapi-rocks") => { - log::info!("authenticated"); - Outcome::Success(LogApiKey) - } - _ => { - log::info!("no api key"); - Outcome::Forward(Status::Unauthorized) - } - } - } - } - - /// Task to do. - #[derive(Serialize, Deserialize, ToSchema, Clone)] - pub(super) struct Todo { - /// Unique todo id. - #[schema(example = 1)] - id: i32, - /// Description of a tasks. - #[schema(example = "Buy groceries")] - value: String, - /// Indication whether task is done or not. - done: bool, - } - - /// List all available todo items. - #[fastapi::path( - responses( - (status = 200, description = "Get all todos", body = [Todo]) - ) - )] - #[get("/")] - pub(super) async fn get_tasks(store: &State) -> Json> { - Json(store.lock().unwrap().clone()) - } - - /// Create new todo item. - /// - /// Create new todo item and add it to the storage. - #[fastapi::path( - responses( - (status = 201, description = "Todo item created successfully", body = Todo), - (status = 409, description = "Todo already exists", body = TodoError, example = json!(TodoError::Conflict(String::from("id = 1")))) - ) - )] - #[post("/", data = "")] - pub(super) async fn create_todo( - todo: Json, - store: &State, - ) -> Result>, TodoError> { - let mut todos = store.lock().unwrap(); - todos - .iter() - .find(|existing| existing.id == todo.id) - .map(|todo| Err(TodoError::Conflict(format!("id = {}", todo.id)))) - .unwrap_or_else(|| { - todos.push(todo.0.clone()); - - Ok(Custom(Status::Created, Json(todo.0))) - }) - } - - /// Mark Todo item done by given id - /// - /// Tries to find todo item by given id and mark it done if found. Will return not found in case todo - /// item does not exists. - #[fastapi::path( - responses( - (status = 200, description = "Todo item marked done successfully"), - (status = 404, description = "Todo item not found from storage", body = TodoError, example = json!(TodoError::NotFound(String::from("id = 1")))) - ), - params( - ("id", description = "Todo item unique id") - ), - security( - (), - ("api_key" = []) - ) - )] - #[put("/")] - pub(super) async fn mark_done( - id: i32, - _api_key: LogApiKey, - store: &State, - ) -> Result { - store - .lock() - .unwrap() - .iter_mut() - .find(|todo| todo.id == id) - .map(|todo| { - todo.done = true; - - Ok(Status::Ok) - }) - .unwrap_or_else(|| Err(TodoError::NotFound(format!("id = {id}")))) - } - - /// Delete Todo by given id. - /// - /// Delete Todo from storage by Todo id if found. - #[fastapi::path( - responses( - (status = 200, description = "Todo deleted successfully"), - (status = 401, description = "Unauthorized to delete Todos", body = TodoError, example = json!(TodoError::Unauthorized(String::from("id = 1")))), - (status = 404, description = "Todo not found", body = TodoError, example = json!(TodoError::NotFound(String::from("id = 1")))) - ), - params( - ("id", description = "Todo item id") - ), - security( - ("api_key" = []) - ) - )] - #[delete("/")] - pub(super) async fn delete_todo( - id: i32, - _api_key: RequireApiKey, - store: &State, - ) -> Result { - let mut todos = store.lock().unwrap(); - let len = todos.len(); - todos.retain(|todo| todo.id != id); - - if len == todos.len() { - Err(TodoError::NotFound(format!("id = {id}"))) - } else { - Ok(Status::Ok) - } - } - - #[derive(Deserialize, FromForm, IntoParams)] - pub(super) struct SearchParams { - /// Value to be search form `Todo`s - value: String, - /// Search whether todo is done - done: Option, - } - - /// Search Todo items by their value. - /// - /// Search is performed in case sensitive manner from value of Todo. - #[fastapi::path( - params( - SearchParams - ), - responses( - (status = 200, description = "Found Todo items", body = [Todo]) - ) - )] - #[get("/search?")] - pub(super) async fn search_todos( - search: SearchParams, - store: &State, - ) -> Json> { - let SearchParams { value, done } = search; - - Json( - store - .lock() - .unwrap() - .iter() - .filter(|todo| { - todo.value.to_lowercase().contains(&value.to_lowercase()) - && done.map(|done| done == todo.done).unwrap_or(true) - }) - .cloned() - .collect(), - ) - } -} diff --git a/examples/todo-actix/Cargo.toml b/examples/todo-actix/Cargo.toml deleted file mode 100644 index c4a0a5e..0000000 --- a/examples/todo-actix/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "todo-actix" -description = "Simple actix-web todo example api with fastapi and Swagger UI, Rapidoc, Redoc, and Scalar" -version = "0.1.0" -edition = "2021" -license = "MIT" -authors = ["Example "] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -actix-web = "4" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -env_logger = "0.11" -log = "0.4" -futures = "0.3" -fastapi = { path = "../../fastapi", features = ["actix_extras"] } -fastapi-swagger-ui = { path = "../../fastapi-swagger-ui", features = [ - "actix-web", -] } -fastapi-redoc = { path = "../../fastapi-redoc", features = ["actix-web"] } -fastapi-rapidoc = { path = "../../fastapi-rapidoc", features = ["actix-web"] } -fastapi-scalar = { path = "../../fastapi-scalar", features = ["actix-web"] } -fastapi-actix-web = { path = "../../fastapi-actix-web" } - -[workspace] diff --git a/examples/todo-actix/README.md b/examples/todo-actix/README.md deleted file mode 100644 index d22b85a..0000000 --- a/examples/todo-actix/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# todo-actix ~ fastapi with fastapi-swagger-ui, fastapi-redoc and fastapi-rapidoc example - -This is a demo `actix-web` application with in-memory storage to manage Todo items. The API -demonstrates `fastapi` with `fastapi-swagger-ui` functionalities. - -For security restricted endpoints the super secret API key is: `fastapi-rocks`. - -Just run command below to run the demo application and browse to `http://localhost:8080/swagger-ui/`. - -If you prefer Redoc just head to `http://localhost:8080/redoc` and view the Open API. - -RapiDoc can be found from `http://localhost:8080/rapidoc`. - -Scalar can be reached on `http://localhost:8080/scalar`. - -```bash -cargo run -``` - -If you want to see some logging, you may prepend the command with `RUST_LOG=debug` as shown below. - -```bash -RUST_LOG=debug cargo run -``` diff --git a/examples/todo-actix/src/main.rs b/examples/todo-actix/src/main.rs deleted file mode 100644 index 41a5f9f..0000000 --- a/examples/todo-actix/src/main.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::{ - error::Error, - future::{self, Ready}, - net::Ipv4Addr, -}; - -use actix_web::{ - dev::{Service, ServiceRequest, ServiceResponse, Transform}, - middleware::Logger, - web::Data, - App, HttpResponse, HttpServer, -}; -use fastapi::{ - openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, - Modify, OpenApi, -}; -use fastapi_actix_web::AppExt; -use fastapi_rapidoc::RapiDoc; -use fastapi_redoc::{Redoc, Servable}; -use fastapi_scalar::{Scalar, Servable as ScalarServable}; -use fastapi_swagger_ui::SwaggerUi; -use futures::future::LocalBoxFuture; - -use crate::todo::TodoStore; - -use self::todo::ErrorResponse; - -mod todo; - -const API_KEY_NAME: &str = "todo_apikey"; -const API_KEY: &str = "fastapi-rocks"; - -#[actix_web::main] -async fn main() -> Result<(), impl Error> { - env_logger::init(); - - #[derive(OpenApi)] - #[openapi( - tags( - (name = "todo", description = "Todo management endpoints.") - ), - modifiers(&SecurityAddon) - )] - struct ApiDoc; - - struct SecurityAddon; - - impl Modify for SecurityAddon { - fn modify(&self, openapi: &mut fastapi::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 store = Data::new(TodoStore::default()); - - HttpServer::new(move || { - // This factory closure is called on each worker thread independently. - App::new() - .into_fastapi_app() - .openapi(ApiDoc::openapi()) - .map(|app| app.wrap(Logger::default())) - .service( - fastapi_actix_web::scope("/api/todo").configure(todo::configure(store.clone())), - ) - .openapi_service(|api| Redoc::with_url("/redoc", api)) - .openapi_service(|api| { - SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", api) - }) - // 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. - // - // If we wanted to serve the schema, the following would work: - // .openapi_service(|api| RapiDoc::with_openapi("/api-docs/openapi2.json", api).path("/rapidoc")) - .map(|app| app.service(RapiDoc::new("/api-docs/openapi.json").path("/rapidoc"))) - .openapi_service(|api| Scalar::with_url("/scalar", api)) - .into_app() - }) - .bind((Ipv4Addr::UNSPECIFIED, 8080))? - .run() - .await -} - -/// Require api key middleware will actually require valid api key -struct RequireApiKey; - -impl Transform for RequireApiKey -where - S: Service< - ServiceRequest, - Response = ServiceResponse, - Error = actix_web::Error, - >, - S::Future: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Transform = ApiKeyMiddleware; - type InitError = (); - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - future::ready(Ok(ApiKeyMiddleware { - service, - log_only: false, - })) - } -} - -/// Log api key middleware only logs about missing or invalid api keys -struct LogApiKey; - -impl Transform for LogApiKey -where - S: Service< - ServiceRequest, - Response = ServiceResponse, - Error = actix_web::Error, - >, - S::Future: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Transform = ApiKeyMiddleware; - type InitError = (); - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - future::ready(Ok(ApiKeyMiddleware { - service, - log_only: true, - })) - } -} - -struct ApiKeyMiddleware { - service: S, - log_only: bool, -} - -impl Service for ApiKeyMiddleware -where - S: Service< - ServiceRequest, - Response = ServiceResponse, - Error = actix_web::Error, - >, - S::Future: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Future = LocalBoxFuture<'static, Result>; - - fn poll_ready( - &self, - ctx: &mut core::task::Context<'_>, - ) -> std::task::Poll> { - self.service.poll_ready(ctx) - } - - fn call(&self, req: ServiceRequest) -> Self::Future { - let response = |req: ServiceRequest, response: HttpResponse| -> Self::Future { - Box::pin(async { Ok(req.into_response(response)) }) - }; - - match req.headers().get(API_KEY_NAME) { - Some(key) if key != API_KEY => { - if self.log_only { - log::debug!("Incorrect api api provided!!!") - } else { - return response( - req, - HttpResponse::Unauthorized().json(ErrorResponse::Unauthorized( - String::from("incorrect api key"), - )), - ); - } - } - None => { - if self.log_only { - log::debug!("Missing api key!!!") - } else { - return response( - req, - HttpResponse::Unauthorized() - .json(ErrorResponse::Unauthorized(String::from("missing api key"))), - ); - } - } - _ => (), // just passthrough - } - - if self.log_only { - log::debug!("Performing operation") - } - - let future = self.service.call(req); - - Box::pin(async move { - let response = future.await?; - - Ok(response) - }) - } -} diff --git a/examples/todo-actix/src/todo.rs b/examples/todo-actix/src/todo.rs deleted file mode 100644 index d522fac..0000000 --- a/examples/todo-actix/src/todo.rs +++ /dev/null @@ -1,273 +0,0 @@ -use std::sync::Mutex; - -use actix_web::{ - delete, get, post, put, - web::{Data, Json, Path, Query}, - HttpResponse, Responder, -}; -use fastapi::{IntoParams, ToSchema}; -use fastapi_actix_web::service_config::ServiceConfig; -use serde::{Deserialize, Serialize}; - -use crate::{LogApiKey, RequireApiKey}; - -#[derive(Default)] -pub(super) struct TodoStore { - todos: Mutex>, -} - -const TODO: &str = "todo"; - -pub(super) fn configure(store: Data) -> impl FnOnce(&mut ServiceConfig) { - |config: &mut ServiceConfig| { - config - .app_data(store) - .service(search_todos) - .service(get_todos) - .service(create_todo) - .service(delete_todo) - .service(get_todo_by_id) - .service(update_todo); - } -} - -/// Task to do. -#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] -struct Todo { - /// Unique id for the todo item. - #[schema(example = 1)] - id: i32, - /// Description of the tasks to do. - #[schema(example = "Remember to buy groceries")] - value: String, - /// Mark is the task done or not - checked: bool, -} - -/// Request to update existing `Todo` item. -#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] -struct TodoUpdateRequest { - /// Optional new value for the `Todo` task. - #[schema(example = "Dentist at 14.00")] - value: Option, - /// Optional check status to mark is the task done or not. - checked: Option, -} - -/// Todo endpoint error responses -#[derive(Serialize, Deserialize, Clone, ToSchema)] -pub(super) enum ErrorResponse { - /// When Todo is not found by search term. - NotFound(String), - /// When there is a conflict storing a new todo. - Conflict(String), - /// When todo endpoint was called without correct credentials - Unauthorized(String), -} - -/// Get list of todos. -/// -/// List todos from in-memory todo store. -/// -/// One could call the api endpoint with following curl. -/// ```text -/// curl localhost:8080/todo -/// ``` -#[fastapi::path( - tag = TODO, - responses( - (status = 200, description = "List current todo items", body = [Todo]) - ) -)] -#[get("")] -async fn get_todos(todo_store: Data) -> impl Responder { - let todos = todo_store.todos.lock().unwrap(); - - HttpResponse::Ok().json(todos.clone()) -} - -/// Create new Todo to shared in-memory storage. -/// -/// Post a new `Todo` in request body as json to store it. Api will return -/// created `Todo` on success or `ErrorResponse::Conflict` if todo with same id already exists. -/// -/// One could call the api with. -/// ```text -/// curl localhost:8080/todo -d '{"id": 1, "value": "Buy movie ticket", "checked": false}' -/// ``` -#[fastapi::path( - tag = TODO, - responses( - (status = 201, description = "Todo created successfully", body = Todo), - (status = 409, description = "Todo with id already exists", body = ErrorResponse, example = json!(ErrorResponse::Conflict(String::from("id = 1")))) - ) -)] -#[post("")] -async fn create_todo(todo: Json, todo_store: Data) -> impl Responder { - let mut todos = todo_store.todos.lock().unwrap(); - let todo = &todo.into_inner(); - - todos - .iter() - .find(|existing| existing.id == todo.id) - .map(|existing| { - HttpResponse::Conflict().json(ErrorResponse::Conflict(format!("id = {}", existing.id))) - }) - .unwrap_or_else(|| { - todos.push(todo.clone()); - - HttpResponse::Ok().json(todo) - }) -} - -/// Delete Todo by given path variable id. -/// -/// This endpoint needs `api_key` authentication in order to call. Api key can be found from README.md. -/// -/// Api will delete todo from shared in-memory storage by the provided id and return success 200. -/// If storage does not contain `Todo` with given id 404 not found will be returned. -#[fastapi::path( - tag = TODO, - responses( - (status = 200, description = "Todo deleted successfully"), - (status = 401, description = "Unauthorized to delete Todo", body = ErrorResponse, example = json!(ErrorResponse::Unauthorized(String::from("missing api key")))), - (status = 404, description = "Todo not found by id", body = ErrorResponse, example = json!(ErrorResponse::NotFound(String::from("id = 1")))) - ), - params( - ("id", description = "Unique storage id of Todo") - ), - security( - ("api_key" = []) - ) -)] -#[delete("/{id}", wrap = "RequireApiKey")] -async fn delete_todo(id: Path, todo_store: Data) -> impl Responder { - let mut todos = todo_store.todos.lock().unwrap(); - let id = id.into_inner(); - - let new_todos = todos - .iter() - .filter(|todo| todo.id != id) - .cloned() - .collect::>(); - - if new_todos.len() == todos.len() { - HttpResponse::NotFound().json(ErrorResponse::NotFound(format!("id = {id}"))) - } else { - *todos = new_todos; - HttpResponse::Ok().finish() - } -} - -/// Get Todo by given todo id. -/// -/// Return found `Todo` with status 200 or 404 not found if `Todo` is not found from shared in-memory storage. -#[fastapi::path( - tag = TODO, - responses( - (status = 200, description = "Todo found from storage", body = Todo), - (status = 404, description = "Todo not found by id", body = ErrorResponse, example = json!(ErrorResponse::NotFound(String::from("id = 1")))) - ), - params( - ("id", description = "Unique storage id of Todo") - ) -)] -#[get("/{id}")] -async fn get_todo_by_id(id: Path, todo_store: Data) -> impl Responder { - let todos = todo_store.todos.lock().unwrap(); - let id = id.into_inner(); - - todos - .iter() - .find(|todo| todo.id == id) - .map(|todo| HttpResponse::Ok().json(todo)) - .unwrap_or_else(|| { - HttpResponse::NotFound().json(ErrorResponse::NotFound(format!("id = {id}"))) - }) -} - -/// Update Todo with given id. -/// -/// This endpoint supports optional authentication. -/// -/// Tries to update `Todo` by given id as path variable. If todo is found by id values are -/// updated according `TodoUpdateRequest` and updated `Todo` is returned with status 200. -/// If todo is not found then 404 not found is returned. -#[fastapi::path( - tag = TODO, - responses( - (status = 200, description = "Todo updated successfully", body = Todo), - (status = 404, description = "Todo not found by id", body = ErrorResponse, example = json!(ErrorResponse::NotFound(String::from("id = 1")))) - ), - params( - ("id", description = "Unique storage id of Todo") - ), - security( - (), - ("api_key" = []) - ) -)] -#[put("/{id}", wrap = "LogApiKey")] -async fn update_todo( - id: Path, - todo: Json, - todo_store: Data, -) -> impl Responder { - let mut todos = todo_store.todos.lock().unwrap(); - let id = id.into_inner(); - let todo = todo.into_inner(); - - todos - .iter_mut() - .find_map(|todo| if todo.id == id { Some(todo) } else { None }) - .map(|existing_todo| { - if let Some(checked) = todo.checked { - existing_todo.checked = checked; - } - if let Some(value) = todo.value { - existing_todo.value = value; - } - - HttpResponse::Ok().json(existing_todo) - }) - .unwrap_or_else(|| { - HttpResponse::NotFound().json(ErrorResponse::NotFound(format!("id = {id}"))) - }) -} - -/// Search todos Query -#[derive(Deserialize, Debug, IntoParams)] -struct SearchTodos { - /// Content that should be found from Todo's value field - value: String, -} - -/// Search Todos with by value -/// -/// Perform search from `Todo`s present in in-memory storage by matching Todo's value to -/// value provided as query parameter. Returns 200 and matching `Todo` items. -#[fastapi::path( - tag = TODO, - params( - SearchTodos - ), - responses( - (status = 200, description = "Search Todos did not result error", body = [Todo]), - ) -)] -#[get("/search")] -async fn search_todos(query: Query, todo_store: Data) -> impl Responder { - let todos = todo_store.todos.lock().unwrap(); - - HttpResponse::Ok().json( - todos - .iter() - .filter(|todo| { - todo.value - .to_lowercase() - .contains(&query.value.to_lowercase()) - }) - .cloned() - .collect::>(), - ) -} diff --git a/examples/todo-axum/Cargo.toml b/examples/todo-axum/Cargo.toml deleted file mode 100644 index 93ac92e..0000000 --- a/examples/todo-axum/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "todo-axum" -description = "Simple axum todo example api with fastapi and Swagger UI, Rapidoc, Redoc, and Scalar" -version = "0.1.0" -edition = "2021" -license = "MIT" -authors = ["Elli Example "] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -axum = "0.7" -hyper = { version = "1.0.1", features = ["full"] } -tokio = { version = "1.17", features = ["full"] } -tower = "0.5" -fastapi = { path = "../../fastapi", features = ["axum_extras"] } -fastapi-swagger-ui = { path = "../../fastapi-swagger-ui", features = ["axum"] } -fastapi-axum = { path = "../../fastapi-axum" } -fastapi-redoc = { path = "../../fastapi-redoc", features = ["axum"] } -fastapi-rapidoc = { path = "../../fastapi-rapidoc", features = ["axum"] } -fastapi-scalar = { path = "../../fastapi-scalar", features = ["axum"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -[workspace] diff --git a/examples/todo-axum/README.md b/examples/todo-axum/README.md deleted file mode 100644 index 0cc46bd..0000000 --- a/examples/todo-axum/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# todo-axum ~ fastapi with fastapi-swagger-ui, fastapi-redoc and fastapi-rapidoc example - -This is a demo `axum` application with in-memory storage to manage Todo items. The API -demonstrates `fastapi` with `fastapi-swagger-ui` functionalities. - -For security restricted endpoints the super secret API key is: `fastapi-rocks`. - -Just run command below to run the demo application and browse to `http://localhost:8080/swagger-ui/`. - -If you prefer Redoc just head to `http://localhost:8080/redoc` and view the Open API. - -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 deleted file mode 100644 index dd10210..0000000 --- a/examples/todo-axum/src/main.rs +++ /dev/null @@ -1,311 +0,0 @@ -use std::net::{Ipv4Addr, SocketAddr}; - -use fastapi::{ - openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, - Modify, OpenApi, -}; -use fastapi_axum::router::OpenApiRouter; -use fastapi_rapidoc::RapiDoc; -use fastapi_redoc::{Redoc, Servable}; -use fastapi_scalar::{Scalar, Servable as ScalarServable}; -use fastapi_swagger_ui::SwaggerUi; -use std::io::Error; -use tokio::net::TcpListener; - -const TODO_TAG: &str = "todo"; - -#[tokio::main] -async fn main() -> Result<(), Error> { - #[derive(OpenApi)] - #[openapi( - modifiers(&SecurityAddon), - tags( - (name = TODO_TAG, description = "Todo items management API") - ) - )] - struct ApiDoc; - - struct SecurityAddon; - - impl Modify for SecurityAddon { - fn modify(&self, openapi: &mut fastapi::openapi::OpenApi) { - if let Some(components) = openapi.components.as_mut() { - components.add_security_scheme( - "api_key", - SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))), - ) - } - } - } - - let (router, api) = OpenApiRouter::with_openapi(ApiDoc::openapi()) - .nest("/api/v1/todos", todo::router()) - .split_for_parts(); - - let router = router - .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", api.clone())) - .merge(Redoc::with_url("/redoc", api.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. - .merge(RapiDoc::new("/api-docs/openapi.json").path("/rapidoc")) - // Alternative to above - // .merge(RapiDoc::with_openapi("/api-docs/openapi2.json", api).path("/rapidoc")) - .merge(Scalar::with_url("/scalar", api)); - - let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 8080)); - let listener = TcpListener::bind(&address).await?; - axum::serve(listener, router.into_make_service()).await -} - -mod todo { - use std::sync::Arc; - - use axum::{ - extract::{Path, Query, State}, - response::IntoResponse, - Json, - }; - use fastapi::{IntoParams, ToSchema}; - use fastapi_axum::{router::OpenApiRouter, routes}; - use hyper::{HeaderMap, StatusCode}; - use serde::{Deserialize, Serialize}; - use tokio::sync::Mutex; - - use crate::TODO_TAG; - - /// In-memory todo store - type Store = Mutex>; - - /// Item to do. - #[derive(Serialize, Deserialize, ToSchema, Clone)] - struct Todo { - id: i32, - #[schema(example = "Buy groceries")] - value: String, - done: bool, - } - - /// Todo operation errors - #[derive(Serialize, Deserialize, ToSchema)] - enum TodoError { - /// Todo already exists conflict. - #[schema(example = "Todo already exists")] - Conflict(String), - /// Todo not found by id. - #[schema(example = "id = 1")] - NotFound(String), - /// Todo operation unauthorized - #[schema(example = "missing api key")] - Unauthorized(String), - } - - pub(super) fn router() -> OpenApiRouter { - let store = Arc::new(Store::default()); - OpenApiRouter::new() - .routes(routes!(list_todos, create_todo)) - .routes(routes!(search_todos)) - .routes(routes!(mark_done, delete_todo)) - .with_state(store) - } - - /// List all Todo items - /// - /// List all Todo items from in-memory storage. - #[fastapi::path( - get, - path = "", - tag = TODO_TAG, - responses( - (status = 200, description = "List all todos successfully", body = [Todo]) - ) - )] - async fn list_todos(State(store): State>) -> Json> { - let todos = store.lock().await.clone(); - - Json(todos) - } - - /// Todo search query - #[derive(Deserialize, IntoParams)] - struct TodoSearchQuery { - /// Search by value. Search is incase sensitive. - value: String, - /// Search by `done` status. - done: bool, - } - - /// Search Todos by query params. - /// - /// Search `Todo`s by query params and return matching `Todo`s. - #[fastapi::path( - get, - path = "/search", - tag = TODO_TAG, - params( - TodoSearchQuery - ), - responses( - (status = 200, description = "List matching todos by query", body = [Todo]) - ) - )] - async fn search_todos( - State(store): State>, - query: Query, - ) -> Json> { - Json( - store - .lock() - .await - .iter() - .filter(|todo| { - todo.value.to_lowercase() == query.value.to_lowercase() - && todo.done == query.done - }) - .cloned() - .collect(), - ) - } - - /// Create new Todo - /// - /// Tries to create a new Todo item to in-memory storage or fails with 409 conflict if already exists. - #[fastapi::path( - post, - path = "", - tag = TODO_TAG, - responses( - (status = 201, description = "Todo item created successfully", body = Todo), - (status = 409, description = "Todo already exists", body = TodoError) - ) - )] - async fn create_todo( - State(store): State>, - Json(todo): Json, - ) -> impl IntoResponse { - let mut todos = store.lock().await; - - todos - .iter_mut() - .find(|existing_todo| existing_todo.id == todo.id) - .map(|found| { - ( - StatusCode::CONFLICT, - Json(TodoError::Conflict(format!( - "todo already exists: {}", - found.id - ))), - ) - .into_response() - }) - .unwrap_or_else(|| { - todos.push(todo.clone()); - - (StatusCode::CREATED, Json(todo)).into_response() - }) - } - - /// Mark Todo item done by id - /// - /// Mark Todo item done by given id. Return only status 200 on success or 404 if Todo is not found. - #[fastapi::path( - put, - path = "/{id}", - tag = TODO_TAG, - responses( - (status = 200, description = "Todo marked done successfully"), - (status = 404, description = "Todo not found") - ), - params( - ("id" = i32, Path, description = "Todo database id") - ), - security( - (), // <-- make optional authentication - ("api_key" = []) - ) - )] - async fn mark_done( - Path(id): Path, - State(store): State>, - headers: HeaderMap, - ) -> StatusCode { - match check_api_key(false, headers) { - Ok(_) => (), - Err(_) => return StatusCode::UNAUTHORIZED, - } - - let mut todos = store.lock().await; - - todos - .iter_mut() - .find(|todo| todo.id == id) - .map(|todo| { - todo.done = true; - StatusCode::OK - }) - .unwrap_or(StatusCode::NOT_FOUND) - } - - /// Delete Todo item by id - /// - /// Delete Todo item from in-memory storage by id. Returns either 200 success of 404 with TodoError if Todo is not found. - #[fastapi::path( - delete, - path = "/{id}", - tag = TODO_TAG, - responses( - (status = 200, description = "Todo marked done successfully"), - (status = 401, description = "Unauthorized to delete Todo", body = TodoError, example = json!(TodoError::Unauthorized(String::from("missing api key")))), - (status = 404, description = "Todo not found", body = TodoError, example = json!(TodoError::NotFound(String::from("id = 1")))) - ), - params( - ("id" = i32, Path, description = "Todo database id") - ), - security( - ("api_key" = []) - ) - )] - async fn delete_todo( - Path(id): Path, - State(store): State>, - headers: HeaderMap, - ) -> impl IntoResponse { - match check_api_key(true, headers) { - Ok(_) => (), - Err(error) => return error.into_response(), - } - - let mut todos = store.lock().await; - - let len = todos.len(); - - todos.retain(|todo| todo.id != id); - - if todos.len() != len { - StatusCode::OK.into_response() - } else { - ( - StatusCode::NOT_FOUND, - Json(TodoError::NotFound(format!("id = {id}"))), - ) - .into_response() - } - } - - // normally you should create a middleware for this but this is sufficient for sake of example. - fn check_api_key( - require_api_key: bool, - headers: HeaderMap, - ) -> Result<(), (StatusCode, Json)> { - match headers.get("todo_apikey") { - Some(header) if header != "fastapi-rocks" => Err(( - StatusCode::UNAUTHORIZED, - Json(TodoError::Unauthorized(String::from("incorrect api key"))), - )), - None if require_api_key => Err(( - StatusCode::UNAUTHORIZED, - Json(TodoError::Unauthorized(String::from("missing api key"))), - )), - _ => Ok(()), - } - } -} diff --git a/examples/todo-tide/Cargo.toml b/examples/todo-tide/Cargo.toml deleted file mode 100644 index c9fe0fe..0000000 --- a/examples/todo-tide/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "todo-tide" -description = "Simple tide todo example api with fastapi and Swagger UI" -version = "0.1.0" -edition = "2021" -license = "MIT" -authors = [ - "Elli Example " -] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -tide = "0.16.0" -async-std = { version = "1.8.0", features = ["attributes"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -env_logger = "0.11.0" -log = "0.4" -futures = "0.3" -fastapi = { path = "../../fastapi" } -fastapi-swagger-ui = { path = "../../fastapi-swagger-ui" } - -[workspace] diff --git a/examples/todo-tide/README.md b/examples/todo-tide/README.md deleted file mode 100644 index 7e338f8..0000000 --- a/examples/todo-tide/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# todo-tide ~ fastapi with fastapi-swagger-ui example - -This is a demo `tide` application with in-memory storage to manage Todo items. The API -demonstrates `fastapi` with `fastapi-swagger-ui` functionalities. - -For security restricted endpoints the super secret API key is: `fastapi-rocks`. - -Just run command below to run the demo application and browse to `http://localhost:8080/swagger-ui/index.html`. - -```bash -cargo run -``` - -If you want to see some logging, you may prepend the command with `RUST_LOG=debug` as shown below. - -```bash -RUST_LOG=debug cargo run -``` diff --git a/examples/todo-tide/src/main.rs b/examples/todo-tide/src/main.rs deleted file mode 100644 index a689caf..0000000 --- a/examples/todo-tide/src/main.rs +++ /dev/null @@ -1,250 +0,0 @@ -use std::sync::Arc; - -use fastapi::{ - openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, - Modify, OpenApi, -}; -use fastapi_swagger_ui::Config; -use serde_json::json; -use tide::{http::Mime, Redirect, Response}; - -use crate::todo::Store; - -#[async_std::main] -async fn main() -> std::io::Result<()> { - env_logger::init(); - let config = Arc::new(Config::from("/api-docs/openapi.json")); - let mut app = tide::with_state(config); - - #[derive(OpenApi)] - #[openapi( - nest( - (path = "/api/todo", api = todo::TodoApi) - ), - modifiers(&SecurityAddon), - tags( - (name = "todo", description = "Todo items management endpoints.") - ) - )] - struct ApiDoc; - - struct SecurityAddon; - - impl Modify for SecurityAddon { - fn modify(&self, openapi: &mut fastapi::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"))), - ) - } - } - - // serve OpenApi json - app.at("/api-docs/openapi.json") - .get(|_| async move { Ok(Response::builder(200).body(json!(ApiDoc::openapi()))) }); - - // serve Swagger UI - app.at("/swagger-ui") - .get(|_| async move { Ok(Redirect::new("/swagger-ui/index.html")) }); - app.at("/swagger-ui/*").get(serve_swagger); - - app.at("/api").nest({ - let mut todos = tide::with_state(Store::default()); - - todos.at("/todo").get(todo::list_todos); - todos.at("/todo").post(todo::create_todo); - todos.at("/todo/:id").delete(todo::delete_todo); - todos.at("/todo/:id").put(todo::mark_done); - - todos - }); - - app.listen("0.0.0.0:8080").await -} - -async fn serve_swagger(request: tide::Request>>) -> tide::Result { - let config = request.state().clone(); - let path = request.url().path().to_string(); - let tail = path.strip_prefix("/swagger-ui/").unwrap(); - - match fastapi_swagger_ui::serve(tail, config) { - Ok(swagger_file) => swagger_file - .map(|file| { - Ok(Response::builder(200) - .body(file.bytes.to_vec()) - .content_type(file.content_type.parse::()?) - .build()) - }) - .unwrap_or_else(|| Ok(Response::builder(404).build())), - Err(error) => Ok(Response::builder(500).body(error.to_string()).build()), - } -} - -mod todo { - use std::sync::{Arc, Mutex}; - - use fastapi::{OpenApi, ToSchema}; - use serde::{Deserialize, Serialize}; - use serde_json::json; - use tide::{Request, Response}; - - #[derive(OpenApi)] - #[openapi( - paths( - list_todos, - create_todo, - delete_todo, - mark_done - ), - tags( - (name = "todo", description = "Todo items management endpoints.") - ) - )] - pub struct TodoApi; - - /// Item to complete - #[derive(Serialize, Deserialize, ToSchema, Clone)] - pub(super) struct Todo { - /// Unique database id for `Todo` - #[schema(example = 1)] - id: i32, - /// Description of task to complete - #[schema(example = "Buy coffee")] - value: String, - /// Indicates whether task is done or not - done: bool, - } - - /// Error that might occur when managing `Todo` items - #[derive(Serialize, Deserialize, ToSchema)] - pub(super) enum TodoError { - /// Happens when Todo item already exists - Config(String), - /// Todo not found from storage - NotFound(String), - } - - pub(super) type Store = Arc>>; - - /// List todos from in-memory storage. - /// - /// List all todos from in memory storage. - #[fastapi::path( - get, - path = "", - responses( - (status = 200, description = "List all todos successfully", body = [Todo]) - ) - )] - pub(super) async fn list_todos(req: Request) -> tide::Result { - let todos = req.state().lock().unwrap().clone(); - - Ok(Response::builder(200).body(json!(todos)).build()) - } - - /// Create new todo - /// - /// Create new todo to in-memory storage if not exists. - #[fastapi::path( - post, - path = "", - request_body = Todo, - responses( - (status = 201, description = "Todo created successfully", body = Todo), - (status = 409, description = "Todo already exists", body = TodoError, example = json!(TodoError::Config(String::from("id = 1")))) - ) - )] - pub(super) async fn create_todo(mut req: Request) -> tide::Result { - let new_todo = req.body_json::().await?; - let mut todos = req.state().lock().unwrap(); - - todos - .iter() - .find(|existing| existing.id == new_todo.id) - .map(|existing| { - Ok(Response::builder(409) - .body(json!(TodoError::Config(format!("id = {}", existing.id)))) - .build()) - }) - .unwrap_or_else(|| { - todos.push(new_todo.clone()); - - Ok(Response::builder(200).body(json!(new_todo)).build()) - }) - } - - /// Delete todo by id. - /// - /// Delete todo from in-memory storage. - #[fastapi::path( - delete, - path = "/{id}", - responses( - (status = 200, description = "Todo deleted successfully"), - (status = 401, description = "Unauthorized to delete Todo"), - (status = 404, description = "Todo not found", body = TodoError, example = json!(TodoError::NotFound(String::from("id = 1")))) - ), - params( - ("id" = i32, Path, description = "Id of todo item to delete") - ), - security( - ("api_key" = []) - ) - )] - pub(super) async fn delete_todo(req: Request) -> tide::Result { - let id = req.param("id")?.parse::()?; - let api_key = req - .header("todo_apikey") - .map(|header| header.as_str().to_string()) - .unwrap_or_default(); - - if api_key != "fastapi-rocks" { - return Ok(Response::new(401)); - } - - let mut todos = req.state().lock().unwrap(); - - let old_size = todos.len(); - - todos.retain(|todo| todo.id != id); - - if old_size == todos.len() { - Ok(Response::builder(404) - .body(json!(TodoError::NotFound(format!("id = {id}")))) - .build()) - } else { - Ok(Response::new(200)) - } - } - - /// Mark todo done by id - #[fastapi::path( - put, - path = "/{id}", - responses( - (status = 200, description = "Todo marked done successfully"), - (status = 404, description = "Todo not found", body = TodoError, example = json!(TodoError::NotFound(String::from("id = 1")))) - ), - params( - ("id" = i32, Path, description = "Id of todo item to mark done") - ) - )] - pub(super) async fn mark_done(req: Request) -> tide::Result { - let id = req.param("id")?.parse::()?; - let mut todos = req.state().lock().unwrap(); - - todos - .iter_mut() - .find(|todo| todo.id == id) - .map(|todo| { - todo.done = true; - Ok(Response::new(200)) - }) - .unwrap_or_else(|| { - Ok(Response::builder(404) - .body(json!(TodoError::NotFound(format!("id = {id}")))) - .build()) - }) - } -} diff --git a/examples/todo-warp-rapidoc/Cargo.toml b/examples/todo-warp-rapidoc/Cargo.toml deleted file mode 100644 index 92c1504..0000000 --- a/examples/todo-warp-rapidoc/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "todo-warp-rapidoc" -description = "Simple warp todo example api with fastapi and fastapi-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.11.0" -log = "0.4" -futures = "0.3" -fastapi = { path = "../../fastapi" } -fastapi-rapidoc = { path = "../../fastapi-rapidoc" } - -[workspace] - diff --git a/examples/todo-warp-rapidoc/README.md b/examples/todo-warp-rapidoc/README.md deleted file mode 100644 index 5c819b9..0000000 --- a/examples/todo-warp-rapidoc/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# warp with fastapi-rapidoc - -This is simple Todo app example with warp and fastapi-rapidoc OpenAPI viewer. - -For security restricted endpoints the super secret API key is: `fastapi-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 deleted file mode 100644 index 34932e1..0000000 --- a/examples/todo-warp-rapidoc/src/main.rs +++ /dev/null @@ -1,243 +0,0 @@ -use std::net::Ipv4Addr; - -use fastapi::{ - openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, - Modify, OpenApi, -}; -use fastapi_rapidoc::RapiDoc; -use warp::Filter; - -#[tokio::main] -async fn main() { - env_logger::init(); - - #[derive(OpenApi)] - #[openapi( - nest( - (path = "/api", api = todo::TodoApi) - ), - modifiers(&SecurityAddon), - tags( - (name = "todo", description = "Todo items management API") - ) - )] - struct ApiDoc; - - struct SecurityAddon; - - impl Modify for SecurityAddon { - fn modify(&self, openapi: &mut fastapi::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(warp::path("api").and(todo::handlers())), - ) - .run((Ipv4Addr::UNSPECIFIED, 8080)) - .await -} - -mod todo { - use std::{ - convert::Infallible, - sync::{Arc, Mutex}, - }; - - use fastapi::{IntoParams, OpenApi, ToSchema}; - use serde::{Deserialize, Serialize}; - use warp::{hyper::StatusCode, Filter, Rejection, Reply}; - - #[derive(OpenApi)] - #[openapi(paths(list_todos, create_todo, delete_todo))] - pub struct TodoApi; - - 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. - #[fastapi::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. - #[fastapi::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. - #[fastapi::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 != "fastapi-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/examples/todo-warp-redoc-with-file-config/Cargo.toml b/examples/todo-warp-redoc-with-file-config/Cargo.toml deleted file mode 100644 index 0127f6e..0000000 --- a/examples/todo-warp-redoc-with-file-config/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "todo-warp-redoc-with-file-config" -description = "Simple warp todo example api with fastapi and fastapi-redoc" -version = "0.1.0" -edition = "2021" -license = "MIT" -authors = ["Elli Example "] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -tokio = { version = "1", features = ["full"] } -warp = "0.3" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -env_logger = "0.11.0" -log = "0.4" -futures = "0.3" -fastapi = { path = "../../fastapi" } -fastapi-redoc = { path = "../../fastapi-redoc" } - -[workspace] diff --git a/examples/todo-warp-redoc-with-file-config/README.md b/examples/todo-warp-redoc-with-file-config/README.md deleted file mode 100644 index dd859ee..0000000 --- a/examples/todo-warp-redoc-with-file-config/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# todo-warp-redoc-with-file-config ~ fastapi with fastapi-redoc example - -This is a demo `warp` application with in-memory storage to manage Todo items. - -This example is more bare minimum compared to `todo-actix`, since similarly same macro syntax is -supported, no matter the framework. - - -This how `fastapi-redoc` can be used as standalone without pre-existing framework integration with additional -file configuration for the Redoc UI. The configuration is applicable in any other `fastapi-redoc` setup as well. - -See the `build.rs` file that defines the Redoc config file and `redoc.json` where the [configuration options](https://redocly.com/docs/api-reference-docs/configuration/functionality/#configuration-options-for-api-docs) -are defined. - -For security restricted endpoints the super secret API key is: `fastapi-rocks`. - -Just run command below to run the demo application and browse to `http://localhost:8080/redoc`. - -```bash -cargo run -``` - -If you want to see some logging, you may prepend the command with `RUST_LOG=debug` as shown below. - -```bash -RUST_LOG=debug cargo run -``` diff --git a/examples/todo-warp-redoc-with-file-config/build.rs b/examples/todo-warp-redoc-with-file-config/build.rs deleted file mode 100644 index 3b2699f..0000000 --- a/examples/todo-warp-redoc-with-file-config/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("cargo:rustc-env=FASTAPI_REDOC_CONFIG_FILE=redoc.json") -} diff --git a/examples/todo-warp-redoc-with-file-config/redoc.json b/examples/todo-warp-redoc-with-file-config/redoc.json deleted file mode 100644 index 318f0e1..0000000 --- a/examples/todo-warp-redoc-with-file-config/redoc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "disableSearch": true -} diff --git a/examples/todo-warp-redoc-with-file-config/src/main.rs b/examples/todo-warp-redoc-with-file-config/src/main.rs deleted file mode 100644 index b772a87..0000000 --- a/examples/todo-warp-redoc-with-file-config/src/main.rs +++ /dev/null @@ -1,239 +0,0 @@ -use std::net::Ipv4Addr; - -use fastapi::{ - openapi::{ - security::{ApiKey, ApiKeyValue, SecurityScheme}, - Components, - }, - Modify, OpenApi, -}; -use fastapi_redoc::{FileConfig, Redoc}; -use warp::Filter; - -#[tokio::main] -async fn main() { - env_logger::init(); - - #[derive(OpenApi)] - #[openapi( - nest( - (path = "/api", api = todo::TodoApi) - ), - modifiers(&SecurityAddon), - tags( - (name = "todo", description = "Todo items management API") - ) - )] - struct ApiDoc; - - struct SecurityAddon; - - impl Modify for SecurityAddon { - fn modify(&self, openapi: &mut fastapi::openapi::OpenApi) { - let components = openapi.components.get_or_insert(Components::new()); - components.add_security_scheme( - "api_key", - SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))), - ) - } - } - - let redoc_ui = Redoc::with_config(ApiDoc::openapi(), FileConfig); - let redoc = warp::path("redoc") - .and(warp::get()) - .map(move || warp::reply::html(redoc_ui.to_html())); - - warp::serve(redoc.or(warp::path("api").and(todo::handlers()))) - .run((Ipv4Addr::UNSPECIFIED, 8080)) - .await -} - -mod todo { - use std::{ - convert::Infallible, - sync::{Arc, Mutex}, - }; - - use fastapi::{IntoParams, OpenApi, ToSchema}; - use serde::{Deserialize, Serialize}; - use warp::{hyper::StatusCode, Filter, Rejection, Reply}; - - #[derive(OpenApi)] - #[openapi(paths(list_todos, create_todo, delete_todo))] - pub struct TodoApi; - - 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. - #[fastapi::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. - #[fastapi::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. - #[fastapi::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 != "fastapi-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/examples/todo-warp/Cargo.toml b/examples/todo-warp/Cargo.toml deleted file mode 100644 index 9697ed5..0000000 --- a/examples/todo-warp/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "todo-warp" -description = "Simple warp todo example api with fastapi and fastapi-swagger-ui" -version = "0.1.0" -edition = "2021" -license = "MIT" -authors = [ - "Elli Example " -] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -tokio = { version = "1", features = ["full"] } -warp = "0.3" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -env_logger = "0.11.0" -log = "0.4" -futures = "0.3" -fastapi = { path = "../../fastapi" } -fastapi-swagger-ui = { path = "../../fastapi-swagger-ui" } - -[workspace] diff --git a/examples/todo-warp/README.md b/examples/todo-warp/README.md deleted file mode 100644 index 428239d..0000000 --- a/examples/todo-warp/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# todo-warp ~ fastapi with fastapi-swagger-ui example - -This is a demo `warp` application with in-memory storage to manage Todo items. -The API demonstrates `fastapi` with `fastapi-swagger-ui` functionalities. - -This example is more bare minimum compared to `todo-actix`, since similarly same macro syntax is -supported, no matter the framework. - -Purpose of this `warp` demo is to mainly demonstrate how `fastapi` and `fastapi-swagger-ui` can be integrated -with other frameworks as well. - -For security restricted endpoints the super secret API key is: `fastapi-rocks`. - -Just run command below to run the demo application and browse to `http://localhost:8080/swagger-ui/`. - -```bash -cargo run -``` - -If you want to see some logging, you may prepend the command with `RUST_LOG=debug` as shown below. - -```bash -RUST_LOG=debug cargo run -``` diff --git a/examples/todo-warp/src/main.rs b/examples/todo-warp/src/main.rs deleted file mode 100644 index 0347a81..0000000 --- a/examples/todo-warp/src/main.rs +++ /dev/null @@ -1,285 +0,0 @@ -use std::{net::Ipv4Addr, sync::Arc}; - -use fastapi::{ - openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, - Modify, OpenApi, -}; -use fastapi_swagger_ui::Config; -use warp::{ - http::Uri, - hyper::{Response, StatusCode}, - path::{FullPath, Tail}, - Filter, Rejection, Reply, -}; - -#[tokio::main] -async fn main() { - env_logger::init(); - - let config = Arc::new(Config::from("/api-doc.json")); - - #[derive(OpenApi)] - #[openapi( - nest( - (path = "/api", api = todo::TodoApi) - ), - modifiers(&SecurityAddon), - tags( - (name = "todo", description = "Todo items management API") - ) - )] - struct ApiDoc; - - struct SecurityAddon; - - impl Modify for SecurityAddon { - fn modify(&self, openapi: &mut fastapi::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 swagger_ui = warp::path("swagger-ui") - .and(warp::get()) - .and(warp::path::full()) - .and(warp::path::tail()) - .and(warp::any().map(move || config.clone())) - .and_then(serve_swagger); - - warp::serve( - api_doc - .or(swagger_ui) - .or(warp::path("api").and(todo::handlers())), - ) - .run((Ipv4Addr::UNSPECIFIED, 8080)) - .await -} - -async fn serve_swagger( - full_path: FullPath, - tail: Tail, - config: Arc>, -) -> Result, Rejection> { - if full_path.as_str() == "/swagger-ui" { - return Ok(Box::new(warp::redirect::found(Uri::from_static( - "/swagger-ui/", - )))); - } - - let path = tail.as_str(); - match fastapi_swagger_ui::serve(path, config) { - Ok(file) => { - if let Some(file) = file { - Ok(Box::new( - Response::builder() - .header("Content-Type", file.content_type) - .body(file.bytes), - )) - } else { - Ok(Box::new(StatusCode::NOT_FOUND)) - } - } - Err(error) => Ok(Box::new( - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(error.to_string()), - )), - } -} - -mod todo { - use std::{ - convert::Infallible, - sync::{Arc, Mutex}, - }; - - use fastapi::{IntoParams, OpenApi, ToSchema}; - use serde::{Deserialize, Serialize}; - use warp::{hyper::StatusCode, Filter, Rejection, Reply}; - - #[derive(OpenApi)] - #[openapi(paths(list_todos, create_todo, delete_todo))] - pub struct TodoApi; - - 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. - #[fastapi::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. - #[fastapi::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. - #[fastapi::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 != "fastapi-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/examples/warp-multiple-api-docs/Cargo.toml b/examples/warp-multiple-api-docs/Cargo.toml deleted file mode 100644 index 92e8f15..0000000 --- a/examples/warp-multiple-api-docs/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "warp-multiple-api-docs" -description = "Simple warp example api with multiple api docs" -version = "0.1.0" -edition = "2021" -license = "MIT" -authors = [ - "Elli Example " -] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[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" -fastapi = { path = "../../fastapi" } -fastapi-swagger-ui = { path = "../../fastapi-swagger-ui" } - -[workspace] diff --git a/examples/warp-multiple-api-docs/README.md b/examples/warp-multiple-api-docs/README.md deleted file mode 100644 index 77bbbc5..0000000 --- a/examples/warp-multiple-api-docs/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# warp-multiple-api-docs ~ fastapi with fastapi-swagger-ui example - -This is a demo `warp` application with multiple API docs to demonstrate splitting APIs with `fastapi` and `fastapi-swagger-ui`. - -Just run command below to run the demo application and browse to `http://localhost:8080/swagger-ui/`. - -```bash -cargo run -``` - -On the Swagger-UI will be a drop-down labelled "Select a definition", containing `/api-doc1.json` and `/api-doc2.json`. - -Alternatively, they can be loaded directly using - -- api1: http://localhost:8080/swagger-ui/?urls.primaryName=%2Fapi-doc1.json -- api2: http://localhost:8080/swagger-ui/?urls.primaryName=%2Fapi-doc2.json - -If you want to see some logging, you may prepend the command with `RUST_LOG=debug` as shown below. - -```bash -RUST_LOG=debug cargo run -``` diff --git a/examples/warp-multiple-api-docs/src/main.rs b/examples/warp-multiple-api-docs/src/main.rs deleted file mode 100644 index 5e4c3bf..0000000 --- a/examples/warp-multiple-api-docs/src/main.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::{net::Ipv4Addr, sync::Arc}; - -use fastapi::OpenApi; -use fastapi_swagger_ui::Config; -use warp::{ - http::Uri, - hyper::{Response, StatusCode}, - path::{FullPath, Tail}, - Filter, Rejection, Reply, -}; - -#[tokio::main] -async fn main() { - env_logger::init(); - - let config = Arc::new(Config::new(["/api-doc1.json", "/api-doc2.json"])); - - #[derive(OpenApi)] - #[openapi(paths(api1::hello1))] - struct ApiDoc1; - - #[derive(OpenApi)] - #[openapi(paths(api2::hello2))] - struct ApiDoc2; - - let api_doc1 = warp::path("api-doc1.json") - .and(warp::get()) - .map(|| warp::reply::json(&ApiDoc1::openapi())); - - let api_doc2 = warp::path("api-doc2.json") - .and(warp::get()) - .map(|| warp::reply::json(&ApiDoc2::openapi())); - - let swagger_ui = warp::path("swagger-ui") - .and(warp::get()) - .and(warp::path::full()) - .and(warp::path::tail()) - .and(warp::any().map(move || config.clone())) - .and_then(serve_swagger); - - let hello1 = warp::path("hello1") - .and(warp::get()) - .and(warp::path::end()) - .and_then(api1::hello1); - - let hello2 = warp::path("hello2") - .and(warp::get()) - .and(warp::path::end()) - .and_then(api2::hello2); - - warp::serve(api_doc1.or(api_doc2).or(swagger_ui).or(hello1).or(hello2)) - .run((Ipv4Addr::UNSPECIFIED, 8080)) - .await -} - -async fn serve_swagger( - full_path: FullPath, - tail: Tail, - config: Arc>, -) -> Result, Rejection> { - if full_path.as_str() == "/swagger-ui" { - return Ok(Box::new(warp::redirect::found(Uri::from_static( - "/swagger-ui/", - )))); - } - - let path = tail.as_str(); - match fastapi_swagger_ui::serve(path, config) { - Ok(file) => { - if let Some(file) = file { - Ok(Box::new( - Response::builder() - .header("Content-Type", file.content_type) - .body(file.bytes), - )) - } else { - Ok(Box::new(StatusCode::NOT_FOUND)) - } - } - Err(error) => Ok(Box::new( - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(error.to_string()), - )), - } -} - -mod api1 { - use std::convert::Infallible; - - use warp::{hyper::Response, Reply}; - - #[fastapi::path( - get, - path = "/hello1", - responses( - (status = 200, body = String) - ) - )] - pub async fn hello1() -> Result { - Ok(Response::builder() - .header("content-type", "text/plain") - .body("hello 1")) - } -} - -mod api2 { - use std::convert::Infallible; - - use warp::{hyper::Response, Reply}; - - #[fastapi::path( - get, - path = "/hello2", - responses( - (status = 200, body = String) - ) - )] - pub async fn hello2() -> Result { - Ok(Response::builder() - .header("content-type", "text/plain") - .body("hello 2")) - } -}