From 8ed93482cc3b129ecb1fbbfb12e60f0b86462f2b Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Tue, 12 Nov 2024 01:42:13 -0800 Subject: [PATCH 01/44] inti --- Cargo.toml | 16 ++++++++++++++++ src/controller/app_routes.rs | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c1426717e..27d1e9e53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,13 @@ cli = ["dep:clap"] testing = ["dep:axum-test"] with-db = ["dep:sea-orm", "dep:sea-orm-migration", "loco-gen/with-db"] channels = ["dep:socketioxide"] +openapi = [ + "dep:utoipa", + "dep:utoipa-axum", + "dep:utoipa-swagger-ui", + "dep:utoipa-redoc", + "dep:utoipa-scalar", +] # Storage features all_storage = ["storage_aws_s3", "storage_azure", "storage_gcp"] storage_aws_s3 = ["object_store/aws"] @@ -123,6 +130,15 @@ uuid = { version = "1.10.0", features = ["v4", "fast-rng"] } # A socket.io server implementation socketioxide = { version = "0.14.0", features = ["state"], optional = true } +# OpenAPI +utoipa = { version = "5.0.0", optional = true } +utoipa-axum = { version = "0.1.0", optional = true } +utoipa-swagger-ui = { version = "8.0", features = [ + "axum", + "vendored", +], optional = true } +utoipa-redoc = { version = "5.0.0", features = ["axum"], optional = true } +utoipa-scalar = { version = "0.2.0", features = ["axum"], optional = true } # File Upload object_store = { version = "0.11.0", default-features = false } diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 32b6cfa5e..4d17a6797 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -6,6 +6,19 @@ use std::{fmt, sync::OnceLock}; use axum::Router as AXRouter; use regex::Regex; +#[cfg(feature = "openapi")] +use utoipa::{ + openapi::security::{ApiKey, ApiKeyValue, HttpAuthScheme, HttpBuilder, SecurityScheme}, + Modify, OpenApi, +}; +#[cfg(feature = "openapi")] +use utoipa_axum::{router::OpenApiRouter, routes}; +#[cfg(feature = "openapi")] +use utoipa_redoc::{Redoc, Servable}; +#[cfg(feature = "openapi")] +use utoipa_scalar::{Scalar, Servable as ScalarServable}; +#[cfg(feature = "openapi")] +use utoipa_swagger_ui::SwaggerUi; #[cfg(feature = "channels")] use super::channels::AppChannels; @@ -202,11 +215,30 @@ impl AppRoutes { // using the router directly, and ServiceBuilder has been reported to give // issues in compile times itself (https://github.com/rust-lang/crates.io/pull/7443). // + #[cfg(feature = "openapi")] + let mut api_router = { + #[derive(OpenApi)] + #[openapi(info(title = "API Documentation", description = "API Documentation"))] + struct ApiDoc; + OpenApiRouter::with_openapi(ApiDoc::openapi()) + }; + for router in self.collect() { tracing::info!("{}", router.to_string()); - app = app.route(&router.uri, router.method); + #[cfg(not(feature = "openapi"))] + { + app = app.route(&router.uri, router.method); + } + #[cfg(feature = "openapi")] + { + app = app.route(&router.uri, router.method.clone()); + api_router = api_router.route(&router.uri, router.method); + } } + #[cfg(feature = "openapi")] + let (_, api) = api_router.split_for_parts(); + #[cfg(feature = "channels")] if let Some(channels) = self.channels.as_ref() { tracing::info!("[Middleware] +channels"); From 6deca2b583f808a4967205948b53a5d9bb096d75 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Tue, 12 Nov 2024 17:15:23 -0800 Subject: [PATCH 02/44] hook initial_openapi_spec --- src/app.rs | 56 ++++++++++++++++++++++++++++++++++++ src/controller/app_routes.rs | 7 +---- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index 26cd1c7ff..6ae7790c6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -219,6 +219,62 @@ pub trait Hooks: Send { /// This function allows users to perform any necessary cleanup or final /// actions before the application stops completely. async fn on_shutdown(_ctx: &AppContext) {} + + /// Modify the OpenAPI spec before the routes are added, allowing you to edit (openapi::info)[https://docs.rs/utoipa/latest/utoipa/openapi/info/struct.Info.html] + /// # Examples + /// ```rust ignore + /// fn inital_openapi_spec() { + /// #[derive(OpenApi)] + /// #[openapi(info( + /// title = "Loco Demo", + /// description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." + /// ))] + /// struct ApiDoc; + /// ApiDoc::openapi() + /// } + /// ``` + /// + /// With SecurityAddon + /// ```rust ignore + /// fn inital_openapi_spec() { + /// #[derive(OpenApi)] + /// #[openapi(modifiers(&SecurityAddon), info( + /// title = "Loco Demo", + /// description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." + /// ))] + /// struct ApiDoc; + /// + /// // TODO set the jwt token location + /// // let auth_location = ctx.config.auth.as_ref(); + /// + /// struct SecurityAddon; + /// impl Modify for SecurityAddon { + /// fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + /// if let Some(components) = openapi.components.as_mut() { + /// components.add_security_schemes_from_iter([ + /// ( + /// "jwt_token", + /// SecurityScheme::Http( + /// HttpBuilder::new() + /// .scheme(HttpAuthScheme::Bearer) + /// .bearer_format("JWT") + /// .build(), + /// ), + /// ), + /// ( + /// "api_key", + /// SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("apikey"))), + /// ), + /// ]); + /// } + /// } + /// } + /// ApiDoc::openapi() + /// } + /// ``` + #[cfg(feature = "openapi")] + #[must_use] + fn inital_openapi_spec(_ctx: &AppContext) -> utoipa::openapi::OpenApi; } /// An initializer. diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 4d17a6797..2ce3c2455 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -216,12 +216,7 @@ impl AppRoutes { // issues in compile times itself (https://github.com/rust-lang/crates.io/pull/7443). // #[cfg(feature = "openapi")] - let mut api_router = { - #[derive(OpenApi)] - #[openapi(info(title = "API Documentation", description = "API Documentation"))] - struct ApiDoc; - OpenApiRouter::with_openapi(ApiDoc::openapi()) - }; + let mut api_router = OpenApiRouter::with_openapi(H::inital_openapi_spec(&ctx)); for router in self.collect() { tracing::info!("{}", router.to_string()); From def3dd54d9050acbffb67a5f7567ab04bf62cc07 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 13 Nov 2024 01:55:11 -0800 Subject: [PATCH 03/44] config.yaml and merge hosted doc with router --- src/config.rs | 26 ++++++++++++++++++++++++++ src/controller/app_routes.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/config.rs b/src/config.rs index 31eeccb1a..5dc2139d5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -414,6 +414,9 @@ pub struct Server { /// logging, and error handling. #[serde(default)] pub middlewares: middleware::Config, + /// OpenAPI configuration + #[cfg(feature = "openapi")] + pub openapi: OpenAPI, } fn default_binding() -> String { @@ -426,6 +429,29 @@ impl Server { format!("{}:{}", self.host, self.port) } } + +/// OpenAPI configuration +#[cfg(feature = "openapi")] +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct OpenAPI { + /// URL for where to host the redoc OpenAPI doc, example: /redoc + pub redoc_url: String, + /// URL for where to host the swagger OpenAPI doc, example: /scalar + pub scalar_url: String, + /// Swagger configuration + pub swagger: Swagger, +} + +/// OpenAPI Swagger configuration +#[cfg(feature = "openapi")] +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Swagger { + /// URL for where to host the swagger OpenAPI doc, example: /swagger-ui + pub swagger_url: String, + /// URL for openapi.json, for example: /api-docs/openapi.json + pub openapi_url: String, +} + /// Background worker configuration /// Example (development): /// ```yaml diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 2ce3c2455..1aec8f3ee 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -234,6 +234,32 @@ impl AppRoutes { #[cfg(feature = "openapi")] let (_, api) = api_router.split_for_parts(); + #[cfg(feature = "openapi")] + { + app = app.merge(Redoc::with_url( + ctx.config.server.openapi.redoc_url.clone(), + api.clone(), + )) + } + + #[cfg(feature = "openapi")] + { + app = app.merge(Scalar::with_url( + ctx.config.server.openapi.scalar_url.clone(), + api.clone(), + )) + } + + #[cfg(feature = "openapi")] + { + app = app.merge( + SwaggerUi::new(ctx.config.server.openapi.swagger.swagger_url.clone()).url( + ctx.config.server.openapi.swagger.openapi_url.clone(), + api.clone(), + ), + ) + } + #[cfg(feature = "channels")] if let Some(channels) = self.channels.as_ref() { tracing::info!("[Middleware] +channels"); From 8db44438cbcdcf48ea2d6f20d76fe2132583add5 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 13 Nov 2024 01:55:35 -0800 Subject: [PATCH 04/44] update existing tests --- src/environment.rs | 1 + src/tests_cfg/config.rs | 9 +++++++++ src/tests_cfg/db.rs | 14 ++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/src/environment.rs b/src/environment.rs index 4beb8fd08..b56d72846 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -131,6 +131,7 @@ mod tests { } #[test] + #[cfg(not(feature = "openapi"))] fn test_from_folder() { let config = Environment::Development.load_from_folder(Path::new("examples/demo/config")); assert!(config.is_ok()); diff --git a/src/tests_cfg/config.rs b/src/tests_cfg/config.rs index e27ab38a3..8216a4b34 100644 --- a/src/tests_cfg/config.rs +++ b/src/tests_cfg/config.rs @@ -23,6 +23,15 @@ pub fn test_config() -> Config { host: "localhost".to_string(), ident: None, middlewares: middleware::Config::default(), + #[cfg(feature = "openapi")] + openapi: config::OpenAPI { + redoc_url: "/redoc".to_string(), + scalar_url: "/scalar".to_string(), + swagger: config::Swagger { + swagger_url: "/swagger-ui".to_string(), + openapi_url: "/api-docs/openapi.json".to_string(), + }, + }, }, #[cfg(feature = "with-db")] database: config::Database { diff --git a/src/tests_cfg/db.rs b/src/tests_cfg/db.rs index a609b4420..b63def806 100644 --- a/src/tests_cfg/db.rs +++ b/src/tests_cfg/db.rs @@ -4,6 +4,9 @@ use async_trait::async_trait; use sea_orm::DatabaseConnection; pub use sea_orm_migration::prelude::*; +#[cfg(feature = "openapi")] +use utoipa::OpenApi; + #[cfg(feature = "channels")] use crate::controller::channels::AppChannels; use crate::{ @@ -126,4 +129,15 @@ impl Hooks for AppHook { fn register_channels(_ctx: &AppContext) -> AppChannels { unimplemented!(); } + + #[cfg(feature = "openapi")] + fn inital_openapi_spec(_ctx: &AppContext) -> utoipa::openapi::OpenApi { + #[derive(OpenApi)] + #[openapi(info( + title = "Loco Demo", + description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." + ))] + struct ApiDoc; + ApiDoc::openapi() + } } From 34d10e6b098680916303be28d2c4ea89f055ff64 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 13 Nov 2024 17:18:26 -0800 Subject: [PATCH 05/44] test: OpenAPI config from file --- examples/demo/config/OpenAPI.yaml | 160 ++++++++++++++++++++++++++++++ src/environment.rs | 8 ++ 2 files changed, 168 insertions(+) create mode 100644 examples/demo/config/OpenAPI.yaml diff --git a/examples/demo/config/OpenAPI.yaml b/examples/demo/config/OpenAPI.yaml new file mode 100644 index 000000000..d8ee881bd --- /dev/null +++ b/examples/demo/config/OpenAPI.yaml @@ -0,0 +1,160 @@ +# Loco configuration file documentation + +# Application logging configuration +logger: + # Enable or disable logging. + enable: false + # Log level, options: trace, debug, info, warn or error. + level: error + # Define the logging format. options: compact, pretty or json + format: compact + # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries + # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters. + # override_filter: trace + +# Web server configuration +server: + # Port on which the server will listen. the server binding is 0.0.0.0:{PORT} + port: 5150 + # The UI hostname or IP address that mailers will point to. + host: http://localhost + # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block + middlewares: + # Allows to limit the payload size request. payload that bigger than this file will blocked the request. + limit_payload: + # Enable/Disable the middleware. + enable: true + # the limit size. can be b,kb,kib,mb,mib,gb,gib + body_limit: 5mb + # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. + logger: + # Enable/Disable the middleware. + enable: true + # when your code is panicked, the request still returns 500 status code. + catch_panic: + # Enable/Disable the middleware. + enable: true + # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. + timeout_request: + # Enable/Disable the middleware. + enable: true + # Duration time in milliseconds. + timeout: 5000 + static_assets: + enable: true + must_exist: true + precompressed: true + folder: + path: assets + fallback: index.html + compression: + enable: true + cors: + enable: true + # Set the value of the [`Access-Control-Allow-Origin`][mdn] header + # allow_origins: + # - https://loco.rs + # Set the value of the [`Access-Control-Allow-Headers`][mdn] header + # allow_headers: + # - Content-Type + # Set the value of the [`Access-Control-Allow-Methods`][mdn] header + # allow_methods: + # - POST + # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds + # max_age: 3600 + openapi: + redoc_url: "/redoc" + scalar_url: "/scalar" + swagger: + swagger_url: "/swagger-ui" + openapi_url: "/api-docs/openapi.json" + +# Worker Configuration +workers: + # specifies the worker mode. Options: + # - BackgroundQueue - Workers operate asynchronously in the background, processing queued. + # - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed. + # - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities. + mode: ForegroundBlocking + +# Mailer Configuration. +mailer: + # SMTP mailer configuration. + smtp: + # Enable/Disable smtp mailer. + enable: true + # SMTP server host. e.x localhost, smtp.gmail.com + host: localhost + # SMTP server port + port: 1025 + # Use secure connection (SSL/TLS). + secure: false + # auth: + # user: + # password: + stub: true + +# Initializers Configuration +# initializers: +# oauth2: +# authorization_code: # Authorization code grant type +# - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config. +# ... other fields + +# Database Configuration +database: + # Database connection URI + uri: {{get_env(name="DATABASE_URL", default="postgres://loco:loco@localhost:5432/loco_app")}} + # When enabled, the sql query will be logged. + enable_logging: false + # Set the timeout duration when acquiring a connection. + connect_timeout: 500 + # Set the idle duration before closing a connection. + idle_timeout: 500 + # Minimum number of connections for a pool. + min_connections: 1 + # Maximum number of connections for a pool. + max_connections: 1 + # Run migration up when application loaded + auto_migrate: true + # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_truncate: true + # Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_recreate: true + +# Queue Configuration +queue: + kind: Redis + # Redis connection URI + uri: {{get_env(name="REDIS_URL", default="redis://127.0.0.1")}} + # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_flush: false + +# Authentication Configuration +auth: + # JWT authentication + jwt: + # Secret key for token generation and verification + secret: PqRwLF2rhHe8J22oBeHy + # Token expiration time in seconds + expiration: 604800 # 7 days + +scheduler: + output: stdout + jobs: + write_content: + shell: true + run: "echo loco >> ./scheduler.txt" + schedule: run every 1 second + output: silent + tags: ['base', 'infra'] + + run_task: + run: "foo" + schedule: "at 10:00 am" + + list_if_users: + run: "user_report" + shell: true + schedule: "* 2 * * * *" + tags: ['base', 'users'] \ No newline at end of file diff --git a/src/environment.rs b/src/environment.rs index b56d72846..08c8c8dec 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -136,4 +136,12 @@ mod tests { let config = Environment::Development.load_from_folder(Path::new("examples/demo/config")); assert!(config.is_ok()); } + + #[test] + #[cfg(feature = "openapi")] + fn test_from_folder_openapi() { + let config = Environment::Any("OpenAPI".to_string()) + .load_from_folder(Path::new("examples/demo/config")); + assert!(config.is_ok()); + } } From c88704211d7be9d345a604ca38379d8b5a633767 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 13 Nov 2024 18:24:32 -0800 Subject: [PATCH 06/44] snapshot tests --- tests/controller/mod.rs | 1 + tests/controller/openapi.rs | 51 +++++++++++++++++++++++++++++++++++++ tests/infra_cfg/server.rs | 30 ++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 tests/controller/openapi.rs diff --git a/tests/controller/mod.rs b/tests/controller/mod.rs index a350ef0c7..d98d4fe52 100644 --- a/tests/controller/mod.rs +++ b/tests/controller/mod.rs @@ -1 +1,2 @@ mod middlewares; +mod openapi; diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs new file mode 100644 index 000000000..1434da316 --- /dev/null +++ b/tests/controller/openapi.rs @@ -0,0 +1,51 @@ +use insta::assert_debug_snapshot; +use loco_rs::{prelude::*, tests_cfg}; +use rstest::rstest; +use serial_test::serial; + +use crate::infra_cfg; + +macro_rules! configure_insta { + ($($expr:expr),*) => { + let mut settings = insta::Settings::clone_current(); + settings.set_prepend_module_to_snapshot(false); + settings.set_snapshot_suffix("openapi"); + let _guard = settings.bind_to_scope(); + }; +} + +#[rstest] +#[case("/redoc")] +#[case("/scalar")] +#[case("/swagger-ui")] +#[tokio::test] +#[serial] +async fn openapi(#[case] test_name: &str) { + configure_insta!(); + + let ctx: AppContext = tests_cfg::app::get_app_context().await; + + match test_name { + "/redoc" => assert_eq!(ctx.config.server.openapi.redoc_url, test_name), + "/scalar" => assert_eq!(ctx.config.server.openapi.scalar_url, test_name), + _ => assert_eq!(ctx.config.server.openapi.swagger.swagger_url, test_name), + } + + let handle = infra_cfg::server::start_from_ctx(ctx).await; + + let res = reqwest::Client::new() + .request( + reqwest::Method::OPTIONS, + infra_cfg::server::get_base_url() + test_name, + ) + .send() + .await + .expect("valid response") + .text() + .await + .unwrap(); + + assert_debug_snapshot!(format!("openapi_[{test_name}]"), res); + + handle.abort(); +} diff --git a/tests/infra_cfg/server.rs b/tests/infra_cfg/server.rs index 771d9088f..71181cb22 100644 --- a/tests/infra_cfg/server.rs +++ b/tests/infra_cfg/server.rs @@ -7,6 +7,8 @@ //! hardcoded ports and bindings. use loco_rs::{boot, controller::AppRoutes, prelude::*, tests_cfg::db::AppHook}; +#[cfg(feature = "openapi")] +use {serde::Serialize, utoipa::ToSchema}; /// The port on which the test server will run. const TEST_PORT_SERVER: i32 = 5555; @@ -29,6 +31,28 @@ async fn post_action(_body: axum::body::Bytes) -> Result { format::render().text("text response") } +#[cfg(feature = "openapi")] +#[derive(Serialize, Debug, ToSchema)] +pub struct Album { + title: String, + rating: u32, +} + +#[cfg(feature = "openapi")] +#[utoipa::path( + get, + path = "/album", + responses( + (status = 200, description = "Album found", body = Album), + ), +)] +async fn get_action_openapi() -> Result { + format::json(Album { + title: "VH II".to_string(), + rating: 10, + }) +} + /// Starts the server using the provided Loco [`boot::BootResult`] result. /// It uses hardcoded server parameters such as the port and binding address. /// @@ -57,9 +81,15 @@ pub async fn start_from_boot(boot_result: boot::BootResult) -> tokio::task::Join pub async fn start_from_ctx(ctx: AppContext) -> tokio::task::JoinHandle<()> { let app_router = AppRoutes::empty() .add_route( + #[cfg(not(feature = "openapi"))] Routes::new() .add("/", get(get_action)) .add("/", post(post_action)), + #[cfg(feature = "openapi")] + Routes::new() + .add("/", get(get_action)) + .add("/", post(post_action)) + .add("/album", get(get_action_openapi)), ) .to_router::(ctx.clone(), axum::Router::new()) .expect("to router"); From 4ed2668d1a8dee129228d6df95664578d57aed7d Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 13 Nov 2024 18:50:22 -0800 Subject: [PATCH 07/44] fix snapshot path --- tests/controller/openapi.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs index 1434da316..30f3ea049 100644 --- a/tests/controller/openapi.rs +++ b/tests/controller/openapi.rs @@ -20,7 +20,7 @@ macro_rules! configure_insta { #[case("/swagger-ui")] #[tokio::test] #[serial] -async fn openapi(#[case] test_name: &str) { +async fn openapi(#[case] mut test_name: &str) { configure_insta!(); let ctx: AppContext = tests_cfg::app::get_app_context().await; @@ -33,19 +33,24 @@ async fn openapi(#[case] test_name: &str) { let handle = infra_cfg::server::start_from_ctx(ctx).await; + test_name = test_name.trim_start_matches("/"); let res = reqwest::Client::new() .request( - reqwest::Method::OPTIONS, + reqwest::Method::GET, infra_cfg::server::get_base_url() + test_name, ) .send() .await - .expect("valid response") - .text() - .await - .unwrap(); - - assert_debug_snapshot!(format!("openapi_[{test_name}]"), res); + .expect("valid response"); + + assert_debug_snapshot!( + format!("openapi_[{test_name}]"), + ( + res.status().to_string(), + res.url().to_string(), + res.text().await.unwrap(), + ) + ); handle.abort(); } From 2aa3d66f42c47102d1519a848bfad2e3cbc45f8f Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 13 Nov 2024 19:08:31 -0800 Subject: [PATCH 08/44] match title, upload snapshots --- tests/controller/openapi.rs | 9 ++++++++- tests/controller/snapshots/openapi_[redoc]@openapi.snap | 9 +++++++++ tests/controller/snapshots/openapi_[scalar]@openapi.snap | 9 +++++++++ .../snapshots/openapi_[swagger-ui]@openapi.snap | 9 +++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/controller/snapshots/openapi_[redoc]@openapi.snap create mode 100644 tests/controller/snapshots/openapi_[scalar]@openapi.snap create mode 100644 tests/controller/snapshots/openapi_[swagger-ui]@openapi.snap diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs index 30f3ea049..8f1603fbd 100644 --- a/tests/controller/openapi.rs +++ b/tests/controller/openapi.rs @@ -48,7 +48,14 @@ async fn openapi(#[case] mut test_name: &str) { ( res.status().to_string(), res.url().to_string(), - res.text().await.unwrap(), + res.text() + .await + .unwrap() + .lines() + .find(|line| line.contains("")) + .and_then(|line| { line.split("<title>").nth(1)?.split("").next() }) + .unwrap_or_default() + .to_string(), ) ); diff --git a/tests/controller/snapshots/openapi_[redoc]@openapi.snap b/tests/controller/snapshots/openapi_[redoc]@openapi.snap new file mode 100644 index 000000000..206aee7d8 --- /dev/null +++ b/tests/controller/snapshots/openapi_[redoc]@openapi.snap @@ -0,0 +1,9 @@ +--- +source: tests/controller/openapi.rs +expression: "(res.status().to_string(), res.url().to_string(),\nres.text().await.unwrap().lines().find(|line|\nline.contains(\"\")).map(|line|\n{\n let start = line.find(\"<title>\").unwrap() + 7; let end =\n line.find(\"\").unwrap(); line[start..end].to_string()\n}).unwrap_or_default(),)" +--- +( + "200 OK", + "http://localhost:5555/redoc", + "Redoc", +) diff --git a/tests/controller/snapshots/openapi_[scalar]@openapi.snap b/tests/controller/snapshots/openapi_[scalar]@openapi.snap new file mode 100644 index 000000000..b2cd8a853 --- /dev/null +++ b/tests/controller/snapshots/openapi_[scalar]@openapi.snap @@ -0,0 +1,9 @@ +--- +source: tests/controller/openapi.rs +expression: "(res.status().to_string(), res.url().to_string(),\nres.text().await.unwrap().lines().find(|line|\nline.contains(\"\")).map(|line|\n{\n let start = line.find(\"<title>\").unwrap() + 7; let end =\n line.find(\"\").unwrap(); line[start..end].to_string()\n}).unwrap_or_default(),)" +--- +( + "200 OK", + "http://localhost:5555/scalar", + "Scalar", +) diff --git a/tests/controller/snapshots/openapi_[swagger-ui]@openapi.snap b/tests/controller/snapshots/openapi_[swagger-ui]@openapi.snap new file mode 100644 index 000000000..1683ddcfa --- /dev/null +++ b/tests/controller/snapshots/openapi_[swagger-ui]@openapi.snap @@ -0,0 +1,9 @@ +--- +source: tests/controller/openapi.rs +expression: "(res.status().to_string(), res.url().to_string(),\nres.text().await.unwrap().lines().find(|line|\nline.contains(\"\")).map(|line|\n{\n let start = line.find(\"<title>\").unwrap() + 7; let end =\n line.find(\"\").unwrap(); line[start..end].to_string()\n}).unwrap_or_default(),)" +--- +( + "200 OK", + "http://localhost:5555/swagger-ui/", + "Swagger UI", +) From 0a304c149e1a3d5feb375a588d4fa17edf246972 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 13 Nov 2024 19:53:08 -0800 Subject: [PATCH 09/44] OpenAPI json snapshot test --- tests/controller/openapi.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs index 8f1603fbd..8a4c9ca62 100644 --- a/tests/controller/openapi.rs +++ b/tests/controller/openapi.rs @@ -61,3 +61,33 @@ async fn openapi(#[case] mut test_name: &str) { handle.abort(); } + +#[tokio::test] +#[serial] +async fn openapi_json() { + configure_insta!(); + + let ctx: AppContext = tests_cfg::app::get_app_context().await; + + let handle = infra_cfg::server::start_from_ctx(ctx).await; + + let res = reqwest::Client::new() + .request( + reqwest::Method::GET, + infra_cfg::server::get_base_url() + "api-docs/openapi.json", + ) + .send() + .await + .expect("valid response"); + + assert_debug_snapshot!( + "openapi_json", + ( + res.status().to_string(), + res.url().to_string(), + res.text().await.unwrap(), + ) + ); + + handle.abort(); +} From 8c0fd96c1cb7412311c686eb0381f603e07150a9 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Fri, 15 Nov 2024 19:04:33 -0800 Subject: [PATCH 10/44] Another snapshot --- tests/controller/snapshots/openapi_json@openapi.snap | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/controller/snapshots/openapi_json@openapi.snap diff --git a/tests/controller/snapshots/openapi_json@openapi.snap b/tests/controller/snapshots/openapi_json@openapi.snap new file mode 100644 index 000000000..dff6658c9 --- /dev/null +++ b/tests/controller/snapshots/openapi_json@openapi.snap @@ -0,0 +1,9 @@ +--- +source: tests/controller/openapi.rs +expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +--- +( + "200 OK", + "http://localhost:5555/api-docs/openapi.json", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.12.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}}}}", +) From 5f63b08f6311be3f1b5ba86f40522c1dd48b4981 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Fri, 15 Nov 2024 19:06:23 -0800 Subject: [PATCH 11/44] LocoMethodRouter --- src/controller/app_routes.rs | 29 +++++++++------ src/controller/describe.rs | 4 +- src/controller/routes.rs | 71 +++++++++++++++++++++++++++++++++--- tests/infra_cfg/server.rs | 4 +- 4 files changed, 85 insertions(+), 23 deletions(-) diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 1aec8f3ee..af2d9d1d3 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -2,9 +2,9 @@ //! configuring routes in an Axum application. It allows you to define route //! prefixes, add routes, and configure middlewares for the application. -use std::{fmt, sync::OnceLock}; +use std::{borrow::Borrow, fmt, sync::OnceLock}; -use axum::Router as AXRouter; +use axum::{handler::Handler, Router as AXRouter}; use regex::Regex; #[cfg(feature = "openapi")] use utoipa::{ @@ -12,7 +12,10 @@ use utoipa::{ Modify, OpenApi, }; #[cfg(feature = "openapi")] -use utoipa_axum::{router::OpenApiRouter, routes}; +use utoipa_axum::{ + router::{OpenApiRouter, UtoipaMethodRouterExt}, + routes, +}; #[cfg(feature = "openapi")] use utoipa_redoc::{Redoc, Servable}; #[cfg(feature = "openapi")] @@ -22,6 +25,7 @@ use utoipa_swagger_ui::SwaggerUi; #[cfg(feature = "channels")] use super::channels::AppChannels; +use super::routes::LocoMethodRouter; use crate::{ app::{AppContext, Hooks}, controller::{middleware::MiddlewareLayer, routes::Routes}, @@ -47,7 +51,7 @@ pub struct AppRoutes { pub struct ListRoutes { pub uri: String, pub actions: Vec, - pub method: axum::routing::MethodRouter, + pub method: LocoMethodRouter, } impl fmt::Display for ListRoutes { @@ -220,14 +224,15 @@ impl AppRoutes { for router in self.collect() { tracing::info!("{}", router.to_string()); - #[cfg(not(feature = "openapi"))] - { - app = app.route(&router.uri, router.method); - } - #[cfg(feature = "openapi")] - { - app = app.route(&router.uri, router.method.clone()); - api_router = api_router.route(&router.uri, router.method); + match router.method { + LocoMethodRouter::Axum(method) => { + app = app.route(&router.uri, method); + } + #[cfg(feature = "openapi")] + LocoMethodRouter::Utoipa(method) => { + app = app.route(&router.uri, method.2.clone().with_state::(())); + api_router = api_router.routes(method.with_state::(())); + } } } diff --git a/src/controller/describe.rs b/src/controller/describe.rs index dc168cf35..a46876df2 100644 --- a/src/controller/describe.rs +++ b/src/controller/describe.rs @@ -3,8 +3,6 @@ use std::sync::OnceLock; use axum::{http, routing::MethodRouter}; use regex::Regex; -use crate::app::AppContext; - static DESCRIBE_METHOD_ACTION: OnceLock = OnceLock::new(); fn get_describe_method_action() -> &'static Regex { @@ -16,7 +14,7 @@ fn get_describe_method_action() -> &'static Regex { /// Currently axum not exposed the action type of the router. for hold extra /// information about routers we need to convert the `method` to string and /// capture the details -pub fn method_action(method: &MethodRouter) -> Vec { +pub fn method_action(method: &MethodRouter) -> Vec { let method_str = format!("{method:?}"); get_describe_method_action() diff --git a/src/controller/routes.rs b/src/controller/routes.rs index fc482e739..02496589c 100644 --- a/src/controller/routes.rs +++ b/src/controller/routes.rs @@ -1,10 +1,13 @@ -use std::convert::Infallible; +use std::{convert::Infallible, fmt}; use axum::{extract::Request, response::IntoResponse, routing::Route}; use tower::{Layer, Service}; +#[cfg(feature = "openapi")] +use utoipa_axum::router::{UtoipaMethodRouter, UtoipaMethodRouterExt}; use super::describe; use crate::app::AppContext; + #[derive(Clone, Default, Debug)] pub struct Routes { pub prefix: Option, @@ -12,10 +15,17 @@ pub struct Routes { // pub version: Option, } -#[derive(Clone, Default, Debug)] +#[derive(Clone)] +pub enum LocoMethodRouter { + Axum(axum::routing::MethodRouter), + #[cfg(feature = "openapi")] + Utoipa(UtoipaMethodRouter), +} + +#[derive(Clone, Debug)] pub struct Handler { pub uri: String, - pub method: axum::routing::MethodRouter, + pub method: LocoMethodRouter, pub actions: Vec, } @@ -78,11 +88,17 @@ impl Routes { /// Routes::new().add("/_ping", get(ping)); /// ```` #[must_use] - pub fn add(mut self, uri: &str, method: axum::routing::MethodRouter) -> Self { - describe::method_action(&method); + pub fn add(mut self, uri: &str, method: impl Into) -> Self { + let method = method.into(); + let actions = match &method { + LocoMethodRouter::Axum(m) => describe::method_action(m), + #[cfg(feature = "openapi")] + LocoMethodRouter::Utoipa(m) => describe::method_action(&m.2), + }; + self.handlers.push(Handler { uri: uri.to_owned(), - actions: describe::method_action(&method), + actions, method, }); self @@ -156,3 +172,46 @@ impl Routes { } } } + +impl fmt::Debug for LocoMethodRouter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Axum(router) => write!(f, "{:?}", router), + #[cfg(feature = "openapi")] + Self::Utoipa(router) => { + // Get the axum::routing::MethodRouter from the UtoipaMethodRouter wrapper + write!(f, "{:?}", router.2) + } + } + } +} + +impl LocoMethodRouter { + pub fn layer(self, layer: L) -> Self + where + L: Layer + Clone + Send + 'static, + L::Service: Service + Clone + Send + 'static, + >::Response: IntoResponse + 'static, + >::Error: Into + 'static, + >::Future: Send + 'static, + { + match self { + LocoMethodRouter::Axum(router) => LocoMethodRouter::Axum(router.layer(layer)), + #[cfg(feature = "openapi")] + LocoMethodRouter::Utoipa(router) => LocoMethodRouter::Utoipa(router.layer(layer)), + } + } +} + +impl From> for LocoMethodRouter { + fn from(router: axum::routing::MethodRouter) -> Self { + LocoMethodRouter::Axum(router) + } +} + +#[cfg(feature = "openapi")] +impl From for LocoMethodRouter { + fn from(router: UtoipaMethodRouter) -> Self { + LocoMethodRouter::Utoipa(router) + } +} diff --git a/tests/infra_cfg/server.rs b/tests/infra_cfg/server.rs index 71181cb22..6709dd006 100644 --- a/tests/infra_cfg/server.rs +++ b/tests/infra_cfg/server.rs @@ -8,7 +8,7 @@ use loco_rs::{boot, controller::AppRoutes, prelude::*, tests_cfg::db::AppHook}; #[cfg(feature = "openapi")] -use {serde::Serialize, utoipa::ToSchema}; +use {serde::Serialize, utoipa::ToSchema, utoipa_axum::routes}; /// The port on which the test server will run. const TEST_PORT_SERVER: i32 = 5555; @@ -89,7 +89,7 @@ pub async fn start_from_ctx(ctx: AppContext) -> tokio::task::JoinHandle<()> { Routes::new() .add("/", get(get_action)) .add("/", post(post_action)) - .add("/album", get(get_action_openapi)), + .add("/album", routes!(get_action_openapi)), ) .to_router::(ctx.clone(), axum::Router::new()) .expect("to router"); From 1c67cc68c305d357a96fec696ed517b3bf112a3c Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Mon, 18 Nov 2024 21:53:06 -0800 Subject: [PATCH 12/44] fix AppContext --- src/controller/app_routes.rs | 7 ++++--- src/controller/describe.rs | 4 +++- src/controller/routes.rs | 18 +++++++++++------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index af2d9d1d3..d041db24e 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -220,7 +220,8 @@ impl AppRoutes { // issues in compile times itself (https://github.com/rust-lang/crates.io/pull/7443). // #[cfg(feature = "openapi")] - let mut api_router = OpenApiRouter::with_openapi(H::inital_openapi_spec(&ctx)); + let mut api_router: OpenApiRouter = + OpenApiRouter::with_openapi(H::inital_openapi_spec(&ctx)); for router in self.collect() { tracing::info!("{}", router.to_string()); @@ -230,8 +231,8 @@ impl AppRoutes { } #[cfg(feature = "openapi")] LocoMethodRouter::Utoipa(method) => { - app = app.route(&router.uri, method.2.clone().with_state::(())); - api_router = api_router.routes(method.with_state::(())); + app = app.route(&router.uri, method.2.clone()); + api_router = api_router.routes(method.with_state(ctx.clone())); } } } diff --git a/src/controller/describe.rs b/src/controller/describe.rs index a46876df2..dc168cf35 100644 --- a/src/controller/describe.rs +++ b/src/controller/describe.rs @@ -3,6 +3,8 @@ use std::sync::OnceLock; use axum::{http, routing::MethodRouter}; use regex::Regex; +use crate::app::AppContext; + static DESCRIBE_METHOD_ACTION: OnceLock = OnceLock::new(); fn get_describe_method_action() -> &'static Regex { @@ -14,7 +16,7 @@ fn get_describe_method_action() -> &'static Regex { /// Currently axum not exposed the action type of the router. for hold extra /// information about routers we need to convert the `method` to string and /// capture the details -pub fn method_action(method: &MethodRouter) -> Vec { +pub fn method_action(method: &MethodRouter) -> Vec { let method_str = format!("{method:?}"); get_describe_method_action() diff --git a/src/controller/routes.rs b/src/controller/routes.rs index 02496589c..743e89593 100644 --- a/src/controller/routes.rs +++ b/src/controller/routes.rs @@ -1,6 +1,10 @@ use std::{convert::Infallible, fmt}; -use axum::{extract::Request, response::IntoResponse, routing::Route}; +use axum::{ + extract::Request, + response::IntoResponse, + routing::{MethodRouter, Route}, +}; use tower::{Layer, Service}; #[cfg(feature = "openapi")] use utoipa_axum::router::{UtoipaMethodRouter, UtoipaMethodRouterExt}; @@ -17,9 +21,9 @@ pub struct Routes { #[derive(Clone)] pub enum LocoMethodRouter { - Axum(axum::routing::MethodRouter), + Axum(MethodRouter), #[cfg(feature = "openapi")] - Utoipa(UtoipaMethodRouter), + Utoipa(UtoipaMethodRouter), } #[derive(Clone, Debug)] @@ -203,15 +207,15 @@ impl LocoMethodRouter { } } -impl From> for LocoMethodRouter { - fn from(router: axum::routing::MethodRouter) -> Self { +impl From> for LocoMethodRouter { + fn from(router: MethodRouter) -> Self { LocoMethodRouter::Axum(router) } } #[cfg(feature = "openapi")] -impl From for LocoMethodRouter { - fn from(router: UtoipaMethodRouter) -> Self { +impl From> for LocoMethodRouter { + fn from(router: UtoipaMethodRouter) -> Self { LocoMethodRouter::Utoipa(router) } } From c1e953e44888b8e5898fc08e46743de06a89c74b Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Mon, 18 Nov 2024 21:52:48 -0800 Subject: [PATCH 13/44] missing cfg for tests cfg for tests --- tests/controller/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/controller/mod.rs b/tests/controller/mod.rs index d98d4fe52..93c6a08c1 100644 --- a/tests/controller/mod.rs +++ b/tests/controller/mod.rs @@ -1,2 +1,3 @@ mod middlewares; +#[cfg(feature = "openapi")] mod openapi; From 74f78cc615e47bbc36201e70c240e6533326b9b5 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Tue, 19 Nov 2024 05:18:32 -0800 Subject: [PATCH 14/44] clippy --- src/controller/app_routes.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index d041db24e..3f9caa242 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -2,20 +2,12 @@ //! configuring routes in an Axum application. It allows you to define route //! prefixes, add routes, and configure middlewares for the application. -use std::{borrow::Borrow, fmt, sync::OnceLock}; +use std::{fmt, sync::OnceLock}; -use axum::{handler::Handler, Router as AXRouter}; +use axum::Router as AXRouter; use regex::Regex; #[cfg(feature = "openapi")] -use utoipa::{ - openapi::security::{ApiKey, ApiKeyValue, HttpAuthScheme, HttpBuilder, SecurityScheme}, - Modify, OpenApi, -}; -#[cfg(feature = "openapi")] -use utoipa_axum::{ - router::{OpenApiRouter, UtoipaMethodRouterExt}, - routes, -}; +use utoipa_axum::router::{OpenApiRouter, UtoipaMethodRouterExt}; #[cfg(feature = "openapi")] use utoipa_redoc::{Redoc, Servable}; #[cfg(feature = "openapi")] From ac0f1ce33034a0195f0be96183536c36bb486d31 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 20 Nov 2024 09:44:05 -0800 Subject: [PATCH 15/44] openapi.josn and openapi.yaml endpoints for all types --- Cargo.toml | 2 +- examples/demo/config/OpenAPI.yaml | 18 +++- src/config.rs | 52 ++++++++--- src/controller/app_routes.rs | 88 ++++++++++++++++--- src/tests_cfg/config.rs | 19 ++-- tests/controller/openapi.rs | 19 ++-- ...pec_[api-docs__openapi.json]@openapi.snap} | 2 +- ...spec_[api-docs__openapi.yaml]@openapi.snap | 9 ++ ...pi_spec_[redoc__openapi.json]@openapi.snap | 9 ++ ...pi_spec_[redoc__openapi.yaml]@openapi.snap | 9 ++ ...i_spec_[scalar__openapi.json]@openapi.snap | 9 ++ ...i_spec_[scalar__openapi.yaml]@openapi.snap | 9 ++ 12 files changed, 203 insertions(+), 42 deletions(-) rename tests/controller/snapshots/{openapi_json@openapi.snap => openapi_spec_[api-docs__openapi.json]@openapi.snap} (93%) create mode 100644 tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap create mode 100644 tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap create mode 100644 tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap create mode 100644 tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap create mode 100644 tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap diff --git a/Cargo.toml b/Cargo.toml index 27d1e9e53..f5999f560 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,7 +131,7 @@ uuid = { version = "1.10.0", features = ["v4", "fast-rng"] } socketioxide = { version = "0.14.0", features = ["state"], optional = true } # OpenAPI -utoipa = { version = "5.0.0", optional = true } +utoipa = { version = "5.0.0", features = ["yaml"], optional = true } utoipa-axum = { version = "0.1.0", optional = true } utoipa-swagger-ui = { version = "8.0", features = [ "axum", diff --git a/examples/demo/config/OpenAPI.yaml b/examples/demo/config/OpenAPI.yaml index d8ee881bd..80c7c8bd2 100644 --- a/examples/demo/config/OpenAPI.yaml +++ b/examples/demo/config/OpenAPI.yaml @@ -63,11 +63,21 @@ server: # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds # max_age: 3600 openapi: - redoc_url: "/redoc" - scalar_url: "/scalar" + redoc: + !Redoc + url: /redoc + spec_json_url: /redoc/openapi.json + spec_yaml_url: /redoc/openapi.yaml + scalar: + !Scalar + url: /scalar + spec_json_url: /scalar/openapi.json + spec_yaml_url: /scalar/openapi.yaml swagger: - swagger_url: "/swagger-ui" - openapi_url: "/api-docs/openapi.json" + !Swagger + url: /swagger-ui + spec_json_url: /api-docs/openapi.json + spec_yaml_url: /api-docs/openapi.yaml # Worker Configuration workers: diff --git a/src/config.rs b/src/config.rs index 5dc2139d5..3440378f3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -434,22 +434,52 @@ impl Server { #[cfg(feature = "openapi")] #[derive(Debug, Clone, Deserialize, Serialize)] pub struct OpenAPI { - /// URL for where to host the redoc OpenAPI doc, example: /redoc - pub redoc_url: String, - /// URL for where to host the swagger OpenAPI doc, example: /scalar - pub scalar_url: String, + /// Redoc configuration + pub redoc: OpenAPIType, + /// Scalar configuration + pub scalar: OpenAPIType, /// Swagger configuration - pub swagger: Swagger, + pub swagger: OpenAPIType, } -/// OpenAPI Swagger configuration #[cfg(feature = "openapi")] #[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Swagger { - /// URL for where to host the swagger OpenAPI doc, example: /swagger-ui - pub swagger_url: String, - /// URL for openapi.json, for example: /api-docs/openapi.json - pub openapi_url: String, +pub enum OpenAPIType { + Redoc { + /// URL for where to host the redoc OpenAPI doc, example: /redoc + url: String, + /// URL for openapi.json, for example: /openapi.json + spec_json_url: Option, + /// URL for openapi.yaml, for example: /openapi.yaml + spec_yaml_url: Option, + }, + Scalar { + /// URL for where to host the swagger OpenAPI doc, example: /scalar + url: String, + /// URL for openapi.json, for example: /openapi.json + spec_json_url: Option, + /// URL for openapi.yaml, for example: /openapi.yaml + spec_yaml_url: Option, + }, + Swagger { + /// URL for where to host the swagger OpenAPI doc, example: /swagger-ui + url: String, + /// URL for openapi.json, for example: /api-docs/openapi.json + spec_json_url: String, + /// URL for openapi.yaml, for example: /openapi.yaml + spec_yaml_url: Option, + }, +} + +#[cfg(feature = "openapi")] +impl OpenAPIType { + pub fn url(&self) -> &String { + match self { + OpenAPIType::Redoc { url, .. } + | OpenAPIType::Scalar { url, .. } + | OpenAPIType::Swagger { url, .. } => url, + } + } } /// Background worker configuration diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 3f9caa242..3e88142d6 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -4,9 +4,13 @@ use std::{fmt, sync::OnceLock}; +#[cfg(feature = "openapi")] +use axum::routing::get; use axum::Router as AXRouter; use regex::Regex; #[cfg(feature = "openapi")] +use utoipa::openapi::OpenApi; +#[cfg(feature = "openapi")] use utoipa_axum::router::{OpenApiRouter, UtoipaMethodRouterExt}; #[cfg(feature = "openapi")] use utoipa_redoc::{Redoc, Servable}; @@ -18,8 +22,11 @@ use utoipa_swagger_ui::SwaggerUi; #[cfg(feature = "channels")] use super::channels::AppChannels; use super::routes::LocoMethodRouter; +#[cfg(feature = "openapi")] +use crate::controller::{format, Response}; use crate::{ app::{AppContext, Hooks}, + config::OpenAPIType, controller::{middleware::MiddlewareLayer, routes::Routes}, Result, }; @@ -30,6 +37,19 @@ fn get_normalize_url() -> &'static Regex { NORMALIZE_URL.get_or_init(|| Regex::new(r"/+").unwrap()) } +#[cfg(feature = "openapi")] +static OPENAPI_SPEC: OnceLock = OnceLock::new(); + +#[cfg(feature = "openapi")] +fn set_openapi_spec(api: OpenApi) -> &'static OpenApi { + OPENAPI_SPEC.get_or_init(|| api) +} + +#[cfg(feature = "openapi")] +fn get_openapi_spec() -> &'static OpenApi { + OPENAPI_SPEC.get().unwrap() +} + /// Represents the routes of the application. #[derive(Clone)] pub struct AppRoutes { @@ -231,31 +251,46 @@ impl AppRoutes { #[cfg(feature = "openapi")] let (_, api) = api_router.split_for_parts(); + #[cfg(feature = "openapi")] + set_openapi_spec(api); #[cfg(feature = "openapi")] { - app = app.merge(Redoc::with_url( - ctx.config.server.openapi.redoc_url.clone(), - api.clone(), - )) + if let OpenAPIType::Redoc { + url, + spec_json_url, + spec_yaml_url, + } = ctx.config.server.openapi.redoc.clone() + { + app = app.merge(Redoc::with_url(url, get_openapi_spec().clone())); + app = add_openapi_endpoints(app, spec_json_url, spec_yaml_url); + } } #[cfg(feature = "openapi")] { - app = app.merge(Scalar::with_url( - ctx.config.server.openapi.scalar_url.clone(), - api.clone(), - )) + if let OpenAPIType::Scalar { + url, + spec_json_url, + spec_yaml_url, + } = ctx.config.server.openapi.scalar.clone() + { + app = app.merge(Scalar::with_url(url, get_openapi_spec().clone())); + app = add_openapi_endpoints(app, spec_json_url, spec_yaml_url); + } } #[cfg(feature = "openapi")] { - app = app.merge( - SwaggerUi::new(ctx.config.server.openapi.swagger.swagger_url.clone()).url( - ctx.config.server.openapi.swagger.openapi_url.clone(), - api.clone(), - ), - ) + if let OpenAPIType::Swagger { + url, + spec_json_url, + spec_yaml_url, + } = ctx.config.server.openapi.swagger.clone() + { + app = app.merge(SwaggerUi::new(url).url(spec_json_url, get_openapi_spec().clone())); + app = add_openapi_endpoints(app, None, spec_yaml_url); + } } #[cfg(feature = "channels")] @@ -302,6 +337,31 @@ impl AppRoutes { } } +#[cfg(feature = "openapi")] +async fn openapi_spec_json() -> Result { + format::json(get_openapi_spec()) +} + +#[cfg(feature = "openapi")] +async fn openapi_spec_yaml() -> Result { + format::text(&get_openapi_spec().to_yaml()?) +} + +#[cfg(feature = "openapi")] +fn add_openapi_endpoints( + mut app: AXRouter, + json_url: Option, + yaml_url: Option, +) -> AXRouter { + if let Some(json_url) = json_url { + app = app.route(&json_url, get(openapi_spec_json)); + } + if let Some(yaml_url) = yaml_url { + app = app.route(&yaml_url, get(openapi_spec_yaml)); + } + app +} + #[cfg(test)] mod tests { diff --git a/src/tests_cfg/config.rs b/src/tests_cfg/config.rs index 8216a4b34..871c6aea1 100644 --- a/src/tests_cfg/config.rs +++ b/src/tests_cfg/config.rs @@ -25,11 +25,20 @@ pub fn test_config() -> Config { middlewares: middleware::Config::default(), #[cfg(feature = "openapi")] openapi: config::OpenAPI { - redoc_url: "/redoc".to_string(), - scalar_url: "/scalar".to_string(), - swagger: config::Swagger { - swagger_url: "/swagger-ui".to_string(), - openapi_url: "/api-docs/openapi.json".to_string(), + redoc: config::OpenAPIType::Redoc { + url: "/redoc".to_string(), + spec_json_url: Some("/redoc/openapi.json".to_string()), + spec_yaml_url: Some("/redoc/openapi.yaml".to_string()), + }, + scalar: config::OpenAPIType::Scalar { + url: "/scalar".to_string(), + spec_json_url: Some("/scalar/openapi.json".to_string()), + spec_yaml_url: Some("/scalar/openapi.yaml".to_string()), + }, + swagger: config::OpenAPIType::Swagger { + url: "/swagger-ui".to_string(), + spec_json_url: "/api-docs/openapi.json".to_string(), + spec_yaml_url: Some("/api-docs/openapi.yaml".to_string()), }, }, }, diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs index 8a4c9ca62..7d7b6da44 100644 --- a/tests/controller/openapi.rs +++ b/tests/controller/openapi.rs @@ -26,9 +26,9 @@ async fn openapi(#[case] mut test_name: &str) { let ctx: AppContext = tests_cfg::app::get_app_context().await; match test_name { - "/redoc" => assert_eq!(ctx.config.server.openapi.redoc_url, test_name), - "/scalar" => assert_eq!(ctx.config.server.openapi.scalar_url, test_name), - _ => assert_eq!(ctx.config.server.openapi.swagger.swagger_url, test_name), + "/redoc" => assert_eq!(ctx.config.server.openapi.redoc.url(), test_name), + "/scalar" => assert_eq!(ctx.config.server.openapi.scalar.url(), test_name), + _ => assert_eq!(ctx.config.server.openapi.swagger.url(), test_name), } let handle = infra_cfg::server::start_from_ctx(ctx).await; @@ -62,9 +62,16 @@ async fn openapi(#[case] mut test_name: &str) { handle.abort(); } +#[rstest] +#[case("redoc/openapi.json")] +#[case("scalar/openapi.json")] +#[case("api-docs/openapi.json")] +#[case("redoc/openapi.yaml")] +#[case("scalar/openapi.yaml")] +#[case("api-docs/openapi.yaml")] #[tokio::test] #[serial] -async fn openapi_json() { +async fn openapi_spec(#[case] test_name: &str) { configure_insta!(); let ctx: AppContext = tests_cfg::app::get_app_context().await; @@ -74,14 +81,14 @@ async fn openapi_json() { let res = reqwest::Client::new() .request( reqwest::Method::GET, - infra_cfg::server::get_base_url() + "api-docs/openapi.json", + infra_cfg::server::get_base_url() + test_name, ) .send() .await .expect("valid response"); assert_debug_snapshot!( - "openapi_json", + format!("openapi_spec_[{test_name}]"), ( res.status().to_string(), res.url().to_string(), diff --git a/tests/controller/snapshots/openapi_json@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap similarity index 93% rename from tests/controller/snapshots/openapi_json@openapi.snap rename to tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap index dff6658c9..2e94c0cb7 100644 --- a/tests/controller/snapshots/openapi_json@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/api-docs/openapi.json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.12.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap new file mode 100644 index 000000000..d4c5b70b6 --- /dev/null +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap @@ -0,0 +1,9 @@ +--- +source: tests/controller/openapi.rs +expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +--- +( + "200 OK", + "http://localhost:5555/api-docs/openapi.yaml", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n", +) diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap new file mode 100644 index 000000000..63b9b2020 --- /dev/null +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap @@ -0,0 +1,9 @@ +--- +source: tests/controller/openapi.rs +expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +--- +( + "200 OK", + "http://localhost:5555/redoc/openapi.json", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}}}}", +) diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap new file mode 100644 index 000000000..882eb0f97 --- /dev/null +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap @@ -0,0 +1,9 @@ +--- +source: tests/controller/openapi.rs +expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +--- +( + "200 OK", + "http://localhost:5555/redoc/openapi.yaml", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n", +) diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap new file mode 100644 index 000000000..1ce12df84 --- /dev/null +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap @@ -0,0 +1,9 @@ +--- +source: tests/controller/openapi.rs +expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +--- +( + "200 OK", + "http://localhost:5555/scalar/openapi.json", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}}}}", +) diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap new file mode 100644 index 000000000..26d65b74b --- /dev/null +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap @@ -0,0 +1,9 @@ +--- +source: tests/controller/openapi.rs +expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +--- +( + "200 OK", + "http://localhost:5555/scalar/openapi.yaml", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n", +) From 2082e12660d67f2adc39569ea742cbfa11c25233 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Thu, 21 Nov 2024 05:00:18 -0800 Subject: [PATCH 16/44] SecurityAddon --- src/app.rs | 43 ++++---------- src/auth/mod.rs | 2 + src/auth/openapi.rs | 57 +++++++++++++++++++ src/tests_cfg/db.rs | 17 ++++-- tests/controller/openapi.rs | 51 ++++++++++++++++- .../openapi_security_[Cookie]@openapi.snap | 9 +++ .../openapi_security_[Query]@openapi.snap | 9 +++ ...spec_[api-docs__openapi.json]@openapi.snap | 2 +- ...spec_[api-docs__openapi.yaml]@openapi.snap | 2 +- ...pi_spec_[redoc__openapi.json]@openapi.snap | 2 +- ...pi_spec_[redoc__openapi.yaml]@openapi.snap | 2 +- ...i_spec_[scalar__openapi.json]@openapi.snap | 2 +- ...i_spec_[scalar__openapi.yaml]@openapi.snap | 2 +- 13 files changed, 156 insertions(+), 44 deletions(-) create mode 100644 src/auth/openapi.rs create mode 100644 tests/controller/snapshots/openapi_security_[Cookie]@openapi.snap create mode 100644 tests/controller/snapshots/openapi_security_[Query]@openapi.snap diff --git a/src/app.rs b/src/app.rs index 6ae7790c6..41d956fc7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -223,7 +223,7 @@ pub trait Hooks: Send { /// Modify the OpenAPI spec before the routes are added, allowing you to edit (openapi::info)[https://docs.rs/utoipa/latest/utoipa/openapi/info/struct.Info.html] /// # Examples /// ```rust ignore - /// fn inital_openapi_spec() { + /// fn inital_openapi_spec(_ctx: &AppContext) -> utoipa::openapi::OpenApi { /// #[derive(OpenApi)] /// #[openapi(info( /// title = "Loco Demo", @@ -236,39 +236,18 @@ pub trait Hooks: Send { /// /// With SecurityAddon /// ```rust ignore - /// fn inital_openapi_spec() { + /// fn inital_openapi_spec(ctx: &AppContext) -> utoipa::openapi::OpenApi { + /// set_jwt_location(ctx); + /// /// #[derive(OpenApi)] - /// #[openapi(modifiers(&SecurityAddon), info( - /// title = "Loco Demo", - /// description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." - /// ))] + /// #[openapi( + /// modifiers(&SecurityAddon), + /// info( + /// title = "Loco Demo", + /// description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." + /// ) + /// )] /// struct ApiDoc; - /// - /// // TODO set the jwt token location - /// // let auth_location = ctx.config.auth.as_ref(); - /// - /// struct SecurityAddon; - /// impl Modify for SecurityAddon { - /// fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { - /// if let Some(components) = openapi.components.as_mut() { - /// components.add_security_schemes_from_iter([ - /// ( - /// "jwt_token", - /// SecurityScheme::Http( - /// HttpBuilder::new() - /// .scheme(HttpAuthScheme::Bearer) - /// .bearer_format("JWT") - /// .build(), - /// ), - /// ), - /// ( - /// "api_key", - /// SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("apikey"))), - /// ), - /// ]); - /// } - /// } - /// } /// ApiDoc::openapi() /// } /// ``` diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 3114f8423..ffcce5a29 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,2 +1,4 @@ #[cfg(feature = "auth_jwt")] pub mod jwt; +#[cfg(feature = "openapi")] +pub mod openapi; diff --git a/src/auth/openapi.rs b/src/auth/openapi.rs new file mode 100644 index 000000000..0d4ff992d --- /dev/null +++ b/src/auth/openapi.rs @@ -0,0 +1,57 @@ +use std::sync::OnceLock; +use utoipa::{ + openapi::security::{ApiKey, ApiKeyValue, HttpAuthScheme, HttpBuilder, SecurityScheme}, + Modify, +}; + +use crate::{app::AppContext, config::JWTLocation}; + +static JWT_LOCATION: OnceLock = OnceLock::new(); + +pub fn set_jwt_location(ctx: &AppContext) -> &'static JWTLocation { + JWT_LOCATION.get_or_init(|| { + ctx.config + .auth + .as_ref() + .and_then(|auth| auth.jwt.as_ref()) + .and_then(|jwt| jwt.location.as_ref()) + .unwrap_or(&JWTLocation::Bearer) + .clone() + }) +} + +fn get_jwt_location() -> &'static JWTLocation { + JWT_LOCATION.get().unwrap() +} + +pub struct SecurityAddon; + +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + if let Some(components) = openapi.components.as_mut() { + components.add_security_schemes_from_iter([ + ( + "jwt_token", + match get_jwt_location() { + JWTLocation::Bearer => SecurityScheme::Http( + HttpBuilder::new() + .scheme(HttpAuthScheme::Bearer) + .bearer_format("JWT") + .build(), + ), + JWTLocation::Query { name } => { + SecurityScheme::ApiKey(ApiKey::Query(ApiKeyValue::new(name))) + } + JWTLocation::Cookie { name } => { + SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new(name))) + } + }, + ), + ( + "api_key", + SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("apikey"))), + ), + ]); + } + } +} diff --git a/src/tests_cfg/db.rs b/src/tests_cfg/db.rs index b63def806..fab784d8d 100644 --- a/src/tests_cfg/db.rs +++ b/src/tests_cfg/db.rs @@ -7,6 +7,8 @@ pub use sea_orm_migration::prelude::*; #[cfg(feature = "openapi")] use utoipa::OpenApi; +#[cfg(feature = "openapi")] +use crate::auth::openapi::{set_jwt_location, SecurityAddon}; #[cfg(feature = "channels")] use crate::controller::channels::AppChannels; use crate::{ @@ -131,12 +133,17 @@ impl Hooks for AppHook { } #[cfg(feature = "openapi")] - fn inital_openapi_spec(_ctx: &AppContext) -> utoipa::openapi::OpenApi { + fn inital_openapi_spec(ctx: &AppContext) -> utoipa::openapi::OpenApi { + set_jwt_location(ctx); + #[derive(OpenApi)] - #[openapi(info( - title = "Loco Demo", - description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." - ))] + #[openapi( + modifiers(&SecurityAddon), + info( + title = "Loco Demo", + description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." + ) + )] struct ApiDoc; ApiDoc::openapi() } diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs index 7d7b6da44..677b56320 100644 --- a/tests/controller/openapi.rs +++ b/tests/controller/openapi.rs @@ -1,5 +1,9 @@ use insta::assert_debug_snapshot; -use loco_rs::{prelude::*, tests_cfg}; +use loco_rs::{ + config::{Auth, JWTLocation, JWT}, + prelude::*, + tests_cfg, +}; use rstest::rstest; use serial_test::serial; @@ -98,3 +102,48 @@ async fn openapi_spec(#[case] test_name: &str) { handle.abort(); } + +#[rstest] +#[case(JWTLocation::Query { name: "JWT".to_string() })] +#[case(JWTLocation::Cookie { name: "JWT".to_string() })] +#[tokio::test] +#[serial] +async fn openapi_security(#[case] location: JWTLocation) { + configure_insta!(); + + let mut ctx: AppContext = tests_cfg::app::get_app_context().await; + ctx.config.auth = Some(Auth { + jwt: Some(JWT { + location: Some(location.clone()), + secret: "PqRwLF2rhHe8J22oBeHy".to_string(), + expiration: 604800, + }), + }); + + let handle = infra_cfg::server::start_from_ctx(ctx).await; + + let res = reqwest::Client::new() + .request( + reqwest::Method::GET, + infra_cfg::server::get_base_url() + "api-docs/openapi.json", + ) + .send() + .await + .expect("valid response"); + + let test_name = match location { + JWTLocation::Query { .. } => "Query", + JWTLocation::Cookie { .. } => "Cookie", + _ => "Bearer", + }; + assert_debug_snapshot!( + format!("openapi_security_[{test_name}]"), + ( + res.status().to_string(), + res.url().to_string(), + res.text().await.unwrap(), + ) + ); + + handle.abort(); +} diff --git a/tests/controller/snapshots/openapi_security_[Cookie]@openapi.snap b/tests/controller/snapshots/openapi_security_[Cookie]@openapi.snap new file mode 100644 index 000000000..a7da4694e --- /dev/null +++ b/tests/controller/snapshots/openapi_security_[Cookie]@openapi.snap @@ -0,0 +1,9 @@ +--- +source: tests/controller/openapi.rs +expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +--- +( + "200 OK", + "http://localhost:5555/api-docs/openapi.json", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"apiKey\",\"in\":\"cookie\",\"name\":\"JWT\"}}}}", +) diff --git a/tests/controller/snapshots/openapi_security_[Query]@openapi.snap b/tests/controller/snapshots/openapi_security_[Query]@openapi.snap new file mode 100644 index 000000000..ba7a7f6d6 --- /dev/null +++ b/tests/controller/snapshots/openapi_security_[Query]@openapi.snap @@ -0,0 +1,9 @@ +--- +source: tests/controller/openapi.rs +expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +--- +( + "200 OK", + "http://localhost:5555/api-docs/openapi.json", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"apiKey\",\"in\":\"query\",\"name\":\"JWT\"}}}}", +) diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap index 2e94c0cb7..7c8d04153 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/api-docs/openapi.json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap index d4c5b70b6..d1545ccb0 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/api-docs/openapi.yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.1\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap index 63b9b2020..887ccaf11 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/redoc/openapi.json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap index 882eb0f97..349bf7210 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/redoc/openapi.yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.1\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap index 1ce12df84..4243cbd18 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/scalar/openapi.json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap index 26d65b74b..ffe1847cd 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/scalar/openapi.yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.1\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) From ea4891941fffeb8d1e5441c9a9d57eaeb5bba261 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Sat, 23 Nov 2024 06:10:06 -0800 Subject: [PATCH 17/44] fix cfg flag for import --- src/controller/app_routes.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 3e88142d6..4520e1955 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -22,14 +22,16 @@ use utoipa_swagger_ui::SwaggerUi; #[cfg(feature = "channels")] use super::channels::AppChannels; use super::routes::LocoMethodRouter; -#[cfg(feature = "openapi")] -use crate::controller::{format, Response}; use crate::{ app::{AppContext, Hooks}, - config::OpenAPIType, controller::{middleware::MiddlewareLayer, routes::Routes}, Result, }; +#[cfg(feature = "openapi")] +use crate::{ + config::OpenAPIType, + controller::{format, Response}, +}; static NORMALIZE_URL: OnceLock = OnceLock::new(); From 022909d108066c32610b4bcf7f19001ff989b084 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Tue, 26 Nov 2024 20:37:26 -0800 Subject: [PATCH 18/44] fix: snapshots and imports --- Cargo.toml | 3 +-- src/controller/app_routes.rs | 5 ++++- .../snapshots/openapi_security_[Cookie]@openapi.snap | 2 +- .../snapshots/openapi_security_[Query]@openapi.snap | 2 +- .../openapi_spec_[api-docs__openapi.json]@openapi.snap | 2 +- .../openapi_spec_[api-docs__openapi.yaml]@openapi.snap | 2 +- .../openapi_spec_[redoc__openapi.json]@openapi.snap | 2 +- .../openapi_spec_[redoc__openapi.yaml]@openapi.snap | 2 +- .../openapi_spec_[scalar__openapi.json]@openapi.snap | 2 +- .../openapi_spec_[scalar__openapi.yaml]@openapi.snap | 2 +- 10 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8b90a4018..aff074d99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ auth_jwt = ["dep:jsonwebtoken"] cli = ["dep:clap"] testing = ["dep:axum-test"] with-db = ["dep:sea-orm", "dep:sea-orm-migration", "loco-gen/with-db"] -channels = ["dep:socketioxide"] openapi = [ "dep:utoipa", "dep:utoipa-axum", @@ -129,7 +128,7 @@ cfg-if = "1" uuid = { version = "1.10.0", features = ["v4", "fast-rng"] } # OpenAPI -utoipa = { version = "5.0.0", optional = true } +utoipa = { version = "5.0.0", features = ["yaml"], optional = true } utoipa-axum = { version = "0.1.0", optional = true } utoipa-swagger-ui = { version = "8.0", features = [ "axum", diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 1efc8104d..351d4769e 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -21,7 +21,10 @@ use utoipa_swagger_ui::SwaggerUi; use crate::{ app::{AppContext, Hooks}, - controller::{middleware::MiddlewareLayer, routes::Routes}, + controller::{ + middleware::MiddlewareLayer, + routes::{LocoMethodRouter, Routes}, + }, Result, }; #[cfg(feature = "openapi")] diff --git a/tests/controller/snapshots/openapi_security_[Cookie]@openapi.snap b/tests/controller/snapshots/openapi_security_[Cookie]@openapi.snap index a7da4694e..339e18539 100644 --- a/tests/controller/snapshots/openapi_security_[Cookie]@openapi.snap +++ b/tests/controller/snapshots/openapi_security_[Cookie]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/api-docs/openapi.json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"apiKey\",\"in\":\"cookie\",\"name\":\"JWT\"}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"apiKey\",\"in\":\"cookie\",\"name\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_security_[Query]@openapi.snap b/tests/controller/snapshots/openapi_security_[Query]@openapi.snap index ba7a7f6d6..a20ba2eaf 100644 --- a/tests/controller/snapshots/openapi_security_[Query]@openapi.snap +++ b/tests/controller/snapshots/openapi_security_[Query]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/api-docs/openapi.json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"apiKey\",\"in\":\"query\",\"name\":\"JWT\"}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"apiKey\",\"in\":\"query\",\"name\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap index 7c8d04153..7defe700e 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/api-docs/openapi.json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap index d1545ccb0..63d51e810 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/api-docs/openapi.yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.1\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap index 887ccaf11..c5f499d57 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/redoc/openapi.json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap index 349bf7210..8d4d56411 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/redoc/openapi.yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.1\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap index 4243cbd18..cc1afc65e 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/scalar/openapi.json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.1\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap index ffe1847cd..3258fc558 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap @@ -5,5 +5,5 @@ expression: "(res.status().to_string(), res.url().to_string(), res.text().await. ( "200 OK", "http://localhost:5555/scalar/openapi.yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.1\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) From 00abce3261fc5ab0dd9f3a8da2b5ae8f88ca50aa Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Thu, 28 Nov 2024 09:23:07 -0800 Subject: [PATCH 19/44] split feature openapi into feature swagger-ui redoc scalar, extract some out of app_routes --- Cargo.toml | 12 ++- src/app.rs | 6 +- src/auth/mod.rs | 6 +- src/config.rs | 24 +++++- src/controller/app_routes.rs | 112 +++++++++++-------------- src/controller/middleware/remote_ip.rs | 4 +- src/controller/mod.rs | 10 ++- src/controller/openapi.rs | 45 ++++++++++ src/controller/routes.rs | 37 ++++++-- src/environment.rs | 8 +- src/tests_cfg/config.rs | 2 +- src/tests_cfg/db.rs | 8 +- tests/controller/mod.rs | 2 +- tests/infra_cfg/server.rs | 10 +-- 14 files changed, 185 insertions(+), 101 deletions(-) create mode 100644 src/controller/openapi.rs diff --git a/Cargo.toml b/Cargo.toml index aff074d99..dac32ccbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,13 +35,11 @@ auth_jwt = ["dep:jsonwebtoken"] cli = ["dep:clap"] testing = ["dep:axum-test"] with-db = ["dep:sea-orm", "dep:sea-orm-migration", "loco-gen/with-db"] -openapi = [ - "dep:utoipa", - "dep:utoipa-axum", - "dep:utoipa-swagger-ui", - "dep:utoipa-redoc", - "dep:utoipa-scalar", -] +# OpenAPI features +all_openapi = ["openapi_swagger", "openapi_redoc", "openapi_scalar"] +openapi_swagger = ["dep:utoipa", "dep:utoipa-axum", "dep:utoipa-swagger-ui"] +openapi_redoc = ["dep:utoipa", "dep:utoipa-axum", "dep:utoipa-redoc"] +openapi_scalar = ["dep:utoipa", "dep:utoipa-axum", "dep:utoipa-scalar"] # Storage features all_storage = ["storage_aws_s3", "storage_azure", "storage_gcp"] storage_aws_s3 = ["object_store/aws"] diff --git a/src/app.rs b/src/app.rs index 8d141c3a1..89c3a318e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -245,7 +245,11 @@ pub trait Hooks: Send { /// ApiDoc::openapi() /// } /// ``` - #[cfg(feature = "openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] #[must_use] fn inital_openapi_spec(_ctx: &AppContext) -> utoipa::openapi::OpenApi; } diff --git a/src/auth/mod.rs b/src/auth/mod.rs index ffcce5a29..214d8d36b 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,4 +1,8 @@ #[cfg(feature = "auth_jwt")] pub mod jwt; -#[cfg(feature = "openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] pub mod openapi; diff --git a/src/config.rs b/src/config.rs index 3440378f3..5093d40b3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -415,7 +415,11 @@ pub struct Server { #[serde(default)] pub middlewares: middleware::Config, /// OpenAPI configuration - #[cfg(feature = "openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] pub openapi: OpenAPI, } @@ -431,7 +435,11 @@ impl Server { } /// OpenAPI configuration -#[cfg(feature = "openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] #[derive(Debug, Clone, Deserialize, Serialize)] pub struct OpenAPI { /// Redoc configuration @@ -442,7 +450,11 @@ pub struct OpenAPI { pub swagger: OpenAPIType, } -#[cfg(feature = "openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] #[derive(Debug, Clone, Deserialize, Serialize)] pub enum OpenAPIType { Redoc { @@ -471,7 +483,11 @@ pub enum OpenAPIType { }, } -#[cfg(feature = "openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] impl OpenAPIType { pub fn url(&self) -> &String { match self { diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 351d4769e..8055a22aa 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -4,20 +4,20 @@ use std::{fmt, sync::OnceLock}; -#[cfg(feature = "openapi")] -use axum::routing::get; use axum::Router as AXRouter; use regex::Regex; -#[cfg(feature = "openapi")] -use utoipa::openapi::OpenApi; -#[cfg(feature = "openapi")] -use utoipa_axum::router::{OpenApiRouter, UtoipaMethodRouterExt}; -#[cfg(feature = "openapi")] +#[cfg(feature = "openapi_redoc")] use utoipa_redoc::{Redoc, Servable}; -#[cfg(feature = "openapi")] +#[cfg(feature = "openapi_scalar")] use utoipa_scalar::{Scalar, Servable as ScalarServable}; -#[cfg(feature = "openapi")] +#[cfg(feature = "openapi_swagger")] use utoipa_swagger_ui::SwaggerUi; +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] +use utoipa_axum::router::{OpenApiRouter, UtoipaMethodRouterExt}; use crate::{ app::{AppContext, Hooks}, @@ -27,10 +27,14 @@ use crate::{ }, Result, }; -#[cfg(feature = "openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] use crate::{ config::OpenAPIType, - controller::{format, Response}, + controller::openapi, }; static NORMALIZE_URL: OnceLock = OnceLock::new(); @@ -39,19 +43,6 @@ fn get_normalize_url() -> &'static Regex { NORMALIZE_URL.get_or_init(|| Regex::new(r"/+").unwrap()) } -#[cfg(feature = "openapi")] -static OPENAPI_SPEC: OnceLock = OnceLock::new(); - -#[cfg(feature = "openapi")] -fn set_openapi_spec(api: OpenApi) -> &'static OpenApi { - OPENAPI_SPEC.get_or_init(|| api) -} - -#[cfg(feature = "openapi")] -fn get_openapi_spec() -> &'static OpenApi { - OPENAPI_SPEC.get().unwrap() -} - /// Represents the routes of the application. #[derive(Clone)] pub struct AppRoutes { @@ -222,7 +213,11 @@ impl AppRoutes { // using the router directly, and ServiceBuilder has been reported to give // issues in compile times itself (https://github.com/rust-lang/crates.io/pull/7443). // - #[cfg(feature = "openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] let mut api_router: OpenApiRouter = OpenApiRouter::with_openapi(H::inital_openapi_spec(&ctx)); @@ -232,7 +227,11 @@ impl AppRoutes { LocoMethodRouter::Axum(method) => { app = app.route(&router.uri, method); } - #[cfg(feature = "openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] LocoMethodRouter::Utoipa(method) => { app = app.route(&router.uri, method.2.clone()); api_router = api_router.routes(method.with_state(ctx.clone())); @@ -240,12 +239,20 @@ impl AppRoutes { } } - #[cfg(feature = "openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] let (_, api) = api_router.split_for_parts(); - #[cfg(feature = "openapi")] - set_openapi_spec(api); - - #[cfg(feature = "openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] + openapi::set_openapi_spec(api); + + #[cfg(feature = "openapi_redoc")] { if let OpenAPIType::Redoc { url, @@ -253,12 +260,12 @@ impl AppRoutes { spec_yaml_url, } = ctx.config.server.openapi.redoc.clone() { - app = app.merge(Redoc::with_url(url, get_openapi_spec().clone())); - app = add_openapi_endpoints(app, spec_json_url, spec_yaml_url); + app = app.merge(Redoc::with_url(url, openapi::get_openapi_spec().clone())); + app = openapi::add_openapi_endpoints(app, spec_json_url, spec_yaml_url); } } - #[cfg(feature = "openapi")] + #[cfg(feature = "openapi_scalar")] { if let OpenAPIType::Scalar { url, @@ -266,12 +273,12 @@ impl AppRoutes { spec_yaml_url, } = ctx.config.server.openapi.scalar.clone() { - app = app.merge(Scalar::with_url(url, get_openapi_spec().clone())); - app = add_openapi_endpoints(app, spec_json_url, spec_yaml_url); + app = app.merge(Scalar::with_url(url, openapi::get_openapi_spec().clone())); + app = openapi::add_openapi_endpoints(app, spec_json_url, spec_yaml_url); } } - #[cfg(feature = "openapi")] + #[cfg(feature = "openapi_swagger")] { if let OpenAPIType::Swagger { url, @@ -279,8 +286,10 @@ impl AppRoutes { spec_yaml_url, } = ctx.config.server.openapi.swagger.clone() { - app = app.merge(SwaggerUi::new(url).url(spec_json_url, get_openapi_spec().clone())); - app = add_openapi_endpoints(app, None, spec_yaml_url); + app = app.merge( + SwaggerUi::new(url).url(spec_json_url, openapi::get_openapi_spec().clone()), + ); + app = openapi::add_openapi_endpoints(app, None, spec_yaml_url); } } @@ -294,31 +303,6 @@ impl AppRoutes { } } -#[cfg(feature = "openapi")] -async fn openapi_spec_json() -> Result { - format::json(get_openapi_spec()) -} - -#[cfg(feature = "openapi")] -async fn openapi_spec_yaml() -> Result { - format::text(&get_openapi_spec().to_yaml()?) -} - -#[cfg(feature = "openapi")] -fn add_openapi_endpoints( - mut app: AXRouter, - json_url: Option, - yaml_url: Option, -) -> AXRouter { - if let Some(json_url) = json_url { - app = app.route(&json_url, get(openapi_spec_json)); - } - if let Some(yaml_url) = yaml_url { - app = app.route(&yaml_url, get(openapi_spec_yaml)); - } - app -} - #[cfg(test)] mod tests { diff --git a/src/controller/middleware/remote_ip.rs b/src/controller/middleware/remote_ip.rs index ce2415918..9419c9193 100644 --- a/src/controller/middleware/remote_ip.rs +++ b/src/controller/middleware/remote_ip.rs @@ -151,7 +151,7 @@ fn maybe_get_forwarded( let forwarded = xffs.join(","); - return forwarded + forwarded .split(',') .map(str::trim) .map(str::parse) @@ -179,7 +179,7 @@ fn maybe_get_forwarded( > The first trustworthy X-Forwarded-For IP address may belong to an untrusted intermediate > proxy rather than the actual client computer, but it is the only IP suitable for security uses. */ - .next_back(); + .next_back() } #[derive(Copy, Clone, Debug)] diff --git a/src/controller/mod.rs b/src/controller/mod.rs index 54c6f9870..417edd6fa 100644 --- a/src/controller/mod.rs +++ b/src/controller/mod.rs @@ -42,11 +42,11 @@ //! AppRoutes::with_default_routes() //! // .add_route(controllers::notes::routes()) //! } -//! +//! //! async fn boot(mode: StartMode, environment: &Environment) -> Result{ //! create_app::(mode, environment).await //! } -//! +//! //! async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> { //! Ok(()) //! } @@ -83,6 +83,12 @@ pub mod format; #[cfg(feature = "with-db")] mod health; pub mod middleware; +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] +mod openapi; mod ping; mod routes; pub mod views; diff --git a/src/controller/openapi.rs b/src/controller/openapi.rs new file mode 100644 index 000000000..53a21f1f9 --- /dev/null +++ b/src/controller/openapi.rs @@ -0,0 +1,45 @@ +use std::sync::OnceLock; + +use axum::{routing::get, Router as AXRouter}; +use utoipa::openapi::OpenApi; + +use crate::{ + app::AppContext, + controller::{ + format, + Response, + }, + Result, +}; + +static OPENAPI_SPEC: OnceLock = OnceLock::new(); + +pub fn set_openapi_spec(api: OpenApi) -> &'static OpenApi { + OPENAPI_SPEC.get_or_init(|| api) +} + +pub fn get_openapi_spec() -> &'static OpenApi { + OPENAPI_SPEC.get().unwrap() +} + +pub async fn openapi_spec_json() -> Result { + format::json(get_openapi_spec()) +} + +pub async fn openapi_spec_yaml() -> Result { + format::text(&get_openapi_spec().to_yaml()?) +} + +pub fn add_openapi_endpoints( + mut app: AXRouter, + json_url: Option, + yaml_url: Option, +) -> AXRouter { + if let Some(json_url) = json_url { + app = app.route(&json_url, get(openapi_spec_json)); + } + if let Some(yaml_url) = yaml_url { + app = app.route(&yaml_url, get(openapi_spec_yaml)); + } + app +} diff --git a/src/controller/routes.rs b/src/controller/routes.rs index 743e89593..d4aef0ae4 100644 --- a/src/controller/routes.rs +++ b/src/controller/routes.rs @@ -6,7 +6,11 @@ use axum::{ routing::{MethodRouter, Route}, }; use tower::{Layer, Service}; -#[cfg(feature = "openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] use utoipa_axum::router::{UtoipaMethodRouter, UtoipaMethodRouterExt}; use super::describe; @@ -22,7 +26,11 @@ pub struct Routes { #[derive(Clone)] pub enum LocoMethodRouter { Axum(MethodRouter), - #[cfg(feature = "openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] Utoipa(UtoipaMethodRouter), } @@ -61,7 +69,6 @@ impl Routes { /// format::json(Health { ok: true }) /// } /// Routes::at("status").add("/_ping", get(ping)); - /// /// ```` #[must_use] pub fn at(prefix: &str) -> Self { @@ -96,7 +103,11 @@ impl Routes { let method = method.into(); let actions = match &method { LocoMethodRouter::Axum(m) => describe::method_action(m), - #[cfg(feature = "openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] LocoMethodRouter::Utoipa(m) => describe::method_action(&m.2), }; @@ -181,7 +192,11 @@ impl fmt::Debug for LocoMethodRouter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Axum(router) => write!(f, "{:?}", router), - #[cfg(feature = "openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] Self::Utoipa(router) => { // Get the axum::routing::MethodRouter from the UtoipaMethodRouter wrapper write!(f, "{:?}", router.2) @@ -201,7 +216,11 @@ impl LocoMethodRouter { { match self { LocoMethodRouter::Axum(router) => LocoMethodRouter::Axum(router.layer(layer)), - #[cfg(feature = "openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] LocoMethodRouter::Utoipa(router) => LocoMethodRouter::Utoipa(router.layer(layer)), } } @@ -213,7 +232,11 @@ impl From> for LocoMethodRouter { } } -#[cfg(feature = "openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] impl From> for LocoMethodRouter { fn from(router: UtoipaMethodRouter) -> Self { LocoMethodRouter::Utoipa(router) diff --git a/src/environment.rs b/src/environment.rs index 08c8c8dec..246989acf 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -131,14 +131,18 @@ mod tests { } #[test] - #[cfg(not(feature = "openapi"))] + #[cfg(not(feature = "all_openapi"))] fn test_from_folder() { let config = Environment::Development.load_from_folder(Path::new("examples/demo/config")); assert!(config.is_ok()); } #[test] - #[cfg(feature = "openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] fn test_from_folder_openapi() { let config = Environment::Any("OpenAPI".to_string()) .load_from_folder(Path::new("examples/demo/config")); diff --git a/src/tests_cfg/config.rs b/src/tests_cfg/config.rs index 871c6aea1..4b8db803a 100644 --- a/src/tests_cfg/config.rs +++ b/src/tests_cfg/config.rs @@ -23,7 +23,7 @@ pub fn test_config() -> Config { host: "localhost".to_string(), ident: None, middlewares: middleware::Config::default(), - #[cfg(feature = "openapi")] + #[cfg(feature = "all_openapi")] openapi: config::OpenAPI { redoc: config::OpenAPIType::Redoc { url: "/redoc".to_string(), diff --git a/src/tests_cfg/db.rs b/src/tests_cfg/db.rs index 86f5f72f3..ae6d10500 100644 --- a/src/tests_cfg/db.rs +++ b/src/tests_cfg/db.rs @@ -3,10 +3,10 @@ use std::path::Path; use async_trait::async_trait; use sea_orm::DatabaseConnection; pub use sea_orm_migration::prelude::*; - -#[cfg(feature = "openapi")] +#[cfg(feature = "all_openapi")] use utoipa::OpenApi; -#[cfg(feature = "openapi")] + +#[cfg(feature = "all_openapi")] use crate::auth::openapi::{set_jwt_location, SecurityAddon}; use crate::{ app::{AppContext, Hooks, Initializer}, @@ -123,7 +123,7 @@ impl Hooks for AppHook { Ok(()) } - #[cfg(feature = "openapi")] + #[cfg(feature = "all_openapi")] fn inital_openapi_spec(ctx: &AppContext) -> utoipa::openapi::OpenApi { set_jwt_location(ctx); diff --git a/tests/controller/mod.rs b/tests/controller/mod.rs index 93c6a08c1..c46b8bf4d 100644 --- a/tests/controller/mod.rs +++ b/tests/controller/mod.rs @@ -1,3 +1,3 @@ mod middlewares; -#[cfg(feature = "openapi")] +#[cfg(feature = "all_openapi")] mod openapi; diff --git a/tests/infra_cfg/server.rs b/tests/infra_cfg/server.rs index 6709dd006..f1ebda077 100644 --- a/tests/infra_cfg/server.rs +++ b/tests/infra_cfg/server.rs @@ -7,7 +7,7 @@ //! hardcoded ports and bindings. use loco_rs::{boot, controller::AppRoutes, prelude::*, tests_cfg::db::AppHook}; -#[cfg(feature = "openapi")] +#[cfg(feature = "all_openapi")] use {serde::Serialize, utoipa::ToSchema, utoipa_axum::routes}; /// The port on which the test server will run. @@ -31,14 +31,14 @@ async fn post_action(_body: axum::body::Bytes) -> Result { format::render().text("text response") } -#[cfg(feature = "openapi")] +#[cfg(feature = "all_openapi")] #[derive(Serialize, Debug, ToSchema)] pub struct Album { title: String, rating: u32, } -#[cfg(feature = "openapi")] +#[cfg(feature = "all_openapi")] #[utoipa::path( get, path = "/album", @@ -81,11 +81,11 @@ pub async fn start_from_boot(boot_result: boot::BootResult) -> tokio::task::Join pub async fn start_from_ctx(ctx: AppContext) -> tokio::task::JoinHandle<()> { let app_router = AppRoutes::empty() .add_route( - #[cfg(not(feature = "openapi"))] + #[cfg(not(feature = "all_openapi"))] Routes::new() .add("/", get(get_action)) .add("/", post(post_action)), - #[cfg(feature = "openapi")] + #[cfg(feature = "all_openapi")] Routes::new() .add("/", get(get_action)) .add("/", post(post_action)) From 2e7e750b91c0c956b17b4b315684dbbd65fe939f Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Sun, 1 Dec 2024 16:53:46 -0800 Subject: [PATCH 20/44] move OpenAPIType.url --- src/config.rs | 15 --------------- tests/controller/openapi.rs | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/config.rs b/src/config.rs index 5093d40b3..66ef53e9e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -483,21 +483,6 @@ pub enum OpenAPIType { }, } -#[cfg(any( - feature = "openapi_swagger", - feature = "openapi_redoc", - feature = "openapi_scalar" -))] -impl OpenAPIType { - pub fn url(&self) -> &String { - match self { - OpenAPIType::Redoc { url, .. } - | OpenAPIType::Scalar { url, .. } - | OpenAPIType::Swagger { url, .. } => url, - } - } -} - /// Background worker configuration /// Example (development): /// ```yaml diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs index 677b56320..c6475964c 100644 --- a/tests/controller/openapi.rs +++ b/tests/controller/openapi.rs @@ -1,6 +1,6 @@ use insta::assert_debug_snapshot; use loco_rs::{ - config::{Auth, JWTLocation, JWT}, + config::{Auth, JWTLocation, OpenAPIType, JWT}, prelude::*, tests_cfg, }; @@ -18,6 +18,20 @@ macro_rules! configure_insta { }; } +trait OpenAPITrait { + fn url(&self) -> &String; +} + +impl OpenAPITrait for OpenAPIType { + fn url(&self) -> &String { + match self { + OpenAPIType::Redoc { url, .. } + | OpenAPIType::Scalar { url, .. } + | OpenAPIType::Swagger { url, .. } => url, + } + } +} + #[rstest] #[case("/redoc")] #[case("/scalar")] From e27be7fa7802a663e7c8174477acbc045fd87c21 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Sun, 1 Dec 2024 18:24:47 -0800 Subject: [PATCH 21/44] drop test for JWT_LOCATION.get_or_init, not possible with cargo test --- src/auth/openapi.rs | 27 ++++++---- src/tests_cfg/db.rs | 4 +- tests/controller/openapi.rs | 51 +------------------ .../openapi_security_[Cookie]@openapi.snap | 9 ---- .../openapi_security_[Query]@openapi.snap | 9 ---- 5 files changed, 20 insertions(+), 80 deletions(-) delete mode 100644 tests/controller/snapshots/openapi_security_[Cookie]@openapi.snap delete mode 100644 tests/controller/snapshots/openapi_security_[Query]@openapi.snap diff --git a/src/auth/openapi.rs b/src/auth/openapi.rs index 0d4ff992d..5b5b931ab 100644 --- a/src/auth/openapi.rs +++ b/src/auth/openapi.rs @@ -1,4 +1,5 @@ use std::sync::OnceLock; + use utoipa::{ openapi::security::{ApiKey, ApiKeyValue, HttpAuthScheme, HttpBuilder, SecurityScheme}, Modify, @@ -8,16 +9,22 @@ use crate::{app::AppContext, config::JWTLocation}; static JWT_LOCATION: OnceLock = OnceLock::new(); -pub fn set_jwt_location(ctx: &AppContext) -> &'static JWTLocation { - JWT_LOCATION.get_or_init(|| { - ctx.config - .auth - .as_ref() - .and_then(|auth| auth.jwt.as_ref()) - .and_then(|jwt| jwt.location.as_ref()) - .unwrap_or(&JWTLocation::Bearer) - .clone() - }) +pub fn get_jwt_location_from_ctx(ctx: &AppContext) -> JWTLocation { + ctx.config + .auth + .as_ref() + .and_then(|auth| auth.jwt.as_ref()) + .and_then(|jwt| jwt.location.as_ref()) + .unwrap_or(&JWTLocation::Bearer) + .clone() +} + +pub fn set_jwt_location_ctx(ctx: &AppContext) -> &'static JWTLocation { + set_jwt_location(get_jwt_location_from_ctx(ctx)) +} + +pub fn set_jwt_location(jwt_location: JWTLocation) -> &'static JWTLocation { + JWT_LOCATION.get_or_init(|| jwt_location) } fn get_jwt_location() -> &'static JWTLocation { diff --git a/src/tests_cfg/db.rs b/src/tests_cfg/db.rs index ae6d10500..d77db55c2 100644 --- a/src/tests_cfg/db.rs +++ b/src/tests_cfg/db.rs @@ -7,7 +7,7 @@ pub use sea_orm_migration::prelude::*; use utoipa::OpenApi; #[cfg(feature = "all_openapi")] -use crate::auth::openapi::{set_jwt_location, SecurityAddon}; +use crate::auth::openapi::{set_jwt_location_ctx, SecurityAddon}; use crate::{ app::{AppContext, Hooks, Initializer}, bgworker::Queue, @@ -125,7 +125,7 @@ impl Hooks for AppHook { #[cfg(feature = "all_openapi")] fn inital_openapi_spec(ctx: &AppContext) -> utoipa::openapi::OpenApi { - set_jwt_location(ctx); + set_jwt_location_ctx(ctx); #[derive(OpenApi)] #[openapi( diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs index c6475964c..77e3ca44e 100644 --- a/tests/controller/openapi.rs +++ b/tests/controller/openapi.rs @@ -1,9 +1,5 @@ use insta::assert_debug_snapshot; -use loco_rs::{ - config::{Auth, JWTLocation, OpenAPIType, JWT}, - prelude::*, - tests_cfg, -}; +use loco_rs::{config::OpenAPIType, prelude::*, tests_cfg}; use rstest::rstest; use serial_test::serial; @@ -116,48 +112,3 @@ async fn openapi_spec(#[case] test_name: &str) { handle.abort(); } - -#[rstest] -#[case(JWTLocation::Query { name: "JWT".to_string() })] -#[case(JWTLocation::Cookie { name: "JWT".to_string() })] -#[tokio::test] -#[serial] -async fn openapi_security(#[case] location: JWTLocation) { - configure_insta!(); - - let mut ctx: AppContext = tests_cfg::app::get_app_context().await; - ctx.config.auth = Some(Auth { - jwt: Some(JWT { - location: Some(location.clone()), - secret: "PqRwLF2rhHe8J22oBeHy".to_string(), - expiration: 604800, - }), - }); - - let handle = infra_cfg::server::start_from_ctx(ctx).await; - - let res = reqwest::Client::new() - .request( - reqwest::Method::GET, - infra_cfg::server::get_base_url() + "api-docs/openapi.json", - ) - .send() - .await - .expect("valid response"); - - let test_name = match location { - JWTLocation::Query { .. } => "Query", - JWTLocation::Cookie { .. } => "Cookie", - _ => "Bearer", - }; - assert_debug_snapshot!( - format!("openapi_security_[{test_name}]"), - ( - res.status().to_string(), - res.url().to_string(), - res.text().await.unwrap(), - ) - ); - - handle.abort(); -} diff --git a/tests/controller/snapshots/openapi_security_[Cookie]@openapi.snap b/tests/controller/snapshots/openapi_security_[Cookie]@openapi.snap deleted file mode 100644 index 339e18539..000000000 --- a/tests/controller/snapshots/openapi_security_[Cookie]@openapi.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" ---- -( - "200 OK", - "http://localhost:5555/api-docs/openapi.json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"apiKey\",\"in\":\"cookie\",\"name\":\"JWT\"}}}}", -) diff --git a/tests/controller/snapshots/openapi_security_[Query]@openapi.snap b/tests/controller/snapshots/openapi_security_[Query]@openapi.snap deleted file mode 100644 index a20ba2eaf..000000000 --- a/tests/controller/snapshots/openapi_security_[Query]@openapi.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" ---- -( - "200 OK", - "http://localhost:5555/api-docs/openapi.json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"apiKey\",\"in\":\"query\",\"name\":\"JWT\"}}}}", -) From b848348811faa804e41404e9f56fc3de4ae5399d Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Sun, 1 Dec 2024 18:50:44 -0800 Subject: [PATCH 22/44] rstest feature flagged cases --- src/controller/app_routes.rs | 28 +++++++++++----------------- src/environment.rs | 6 +++++- src/tests_cfg/config.rs | 6 +++++- src/tests_cfg/db.rs | 18 +++++++++++++++--- tests/controller/mod.rs | 6 +++++- tests/controller/openapi.rs | 35 ++++++++++++++++++++++++++--------- tests/infra_cfg/server.rs | 30 +++++++++++++++++++++++++----- 7 files changed, 92 insertions(+), 37 deletions(-) diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 8055a22aa..85badb46f 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -6,18 +6,18 @@ use std::{fmt, sync::OnceLock}; use axum::Router as AXRouter; use regex::Regex; -#[cfg(feature = "openapi_redoc")] -use utoipa_redoc::{Redoc, Servable}; -#[cfg(feature = "openapi_scalar")] -use utoipa_scalar::{Scalar, Servable as ScalarServable}; -#[cfg(feature = "openapi_swagger")] -use utoipa_swagger_ui::SwaggerUi; #[cfg(any( feature = "openapi_swagger", feature = "openapi_redoc", feature = "openapi_scalar" ))] use utoipa_axum::router::{OpenApiRouter, UtoipaMethodRouterExt}; +#[cfg(feature = "openapi_redoc")] +use utoipa_redoc::{Redoc, Servable}; +#[cfg(feature = "openapi_scalar")] +use utoipa_scalar::{Scalar, Servable as ScalarServable}; +#[cfg(feature = "openapi_swagger")] +use utoipa_swagger_ui::SwaggerUi; use crate::{ app::{AppContext, Hooks}, @@ -32,10 +32,7 @@ use crate::{ feature = "openapi_redoc", feature = "openapi_scalar" ))] -use crate::{ - config::OpenAPIType, - controller::openapi, -}; +use crate::{config::OpenAPIType, controller::openapi}; static NORMALIZE_URL: OnceLock = OnceLock::new(); @@ -244,13 +241,10 @@ impl AppRoutes { feature = "openapi_redoc", feature = "openapi_scalar" ))] - let (_, api) = api_router.split_for_parts(); - #[cfg(any( - feature = "openapi_swagger", - feature = "openapi_redoc", - feature = "openapi_scalar" - ))] - openapi::set_openapi_spec(api); + { + let (_, api) = api_router.split_for_parts(); + openapi::set_openapi_spec(api); + } #[cfg(feature = "openapi_redoc")] { diff --git a/src/environment.rs b/src/environment.rs index 246989acf..2aa1ee146 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -131,7 +131,11 @@ mod tests { } #[test] - #[cfg(not(feature = "all_openapi"))] + #[cfg(not(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + )))] fn test_from_folder() { let config = Environment::Development.load_from_folder(Path::new("examples/demo/config")); assert!(config.is_ok()); diff --git a/src/tests_cfg/config.rs b/src/tests_cfg/config.rs index 4b8db803a..67e1e8acd 100644 --- a/src/tests_cfg/config.rs +++ b/src/tests_cfg/config.rs @@ -23,7 +23,11 @@ pub fn test_config() -> Config { host: "localhost".to_string(), ident: None, middlewares: middleware::Config::default(), - #[cfg(feature = "all_openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] openapi: config::OpenAPI { redoc: config::OpenAPIType::Redoc { url: "/redoc".to_string(), diff --git a/src/tests_cfg/db.rs b/src/tests_cfg/db.rs index d77db55c2..9c79fc62a 100644 --- a/src/tests_cfg/db.rs +++ b/src/tests_cfg/db.rs @@ -3,10 +3,18 @@ use std::path::Path; use async_trait::async_trait; use sea_orm::DatabaseConnection; pub use sea_orm_migration::prelude::*; -#[cfg(feature = "all_openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] use utoipa::OpenApi; -#[cfg(feature = "all_openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] use crate::auth::openapi::{set_jwt_location_ctx, SecurityAddon}; use crate::{ app::{AppContext, Hooks, Initializer}, @@ -123,7 +131,11 @@ impl Hooks for AppHook { Ok(()) } - #[cfg(feature = "all_openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] fn inital_openapi_spec(ctx: &AppContext) -> utoipa::openapi::OpenApi { set_jwt_location_ctx(ctx); diff --git a/tests/controller/mod.rs b/tests/controller/mod.rs index c46b8bf4d..baedd334e 100644 --- a/tests/controller/mod.rs +++ b/tests/controller/mod.rs @@ -1,3 +1,7 @@ mod middlewares; -#[cfg(feature = "all_openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] mod openapi; diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs index 77e3ca44e..21bbb7746 100644 --- a/tests/controller/openapi.rs +++ b/tests/controller/openapi.rs @@ -29,12 +29,16 @@ impl OpenAPITrait for OpenAPIType { } #[rstest] -#[case("/redoc")] -#[case("/scalar")] -#[case("/swagger-ui")] +#[cfg_attr(feature = "openapi_swagger", case("/swagger-ui"))] +#[cfg_attr(feature = "openapi_redoc", case("/redoc"))] +#[cfg_attr(feature = "openapi_scalar", case("/scalar"))] +#[case("")] #[tokio::test] #[serial] async fn openapi(#[case] mut test_name: &str) { + if test_name.is_empty() { + return; + } configure_insta!(); let ctx: AppContext = tests_cfg::app::get_app_context().await; @@ -77,15 +81,28 @@ async fn openapi(#[case] mut test_name: &str) { } #[rstest] -#[case("redoc/openapi.json")] -#[case("scalar/openapi.json")] -#[case("api-docs/openapi.json")] -#[case("redoc/openapi.yaml")] -#[case("scalar/openapi.yaml")] -#[case("api-docs/openapi.yaml")] +#[cfg_attr( + feature = "openapi_swagger", + case("api-docs/openapi.json"), + case("api-docs/openapi.yaml") +)] +#[cfg_attr( + feature = "openapi_redoc", + case("redoc/openapi.json"), + case("redoc/openapi.yaml") +)] +#[cfg_attr( + feature = "openapi_scalar", + case("scalar/openapi.json"), + case("scalar/openapi.yaml") +)] +#[case("")] #[tokio::test] #[serial] async fn openapi_spec(#[case] test_name: &str) { + if test_name.is_empty() { + return; + } configure_insta!(); let ctx: AppContext = tests_cfg::app::get_app_context().await; diff --git a/tests/infra_cfg/server.rs b/tests/infra_cfg/server.rs index f1ebda077..1d09a3498 100644 --- a/tests/infra_cfg/server.rs +++ b/tests/infra_cfg/server.rs @@ -7,7 +7,11 @@ //! hardcoded ports and bindings. use loco_rs::{boot, controller::AppRoutes, prelude::*, tests_cfg::db::AppHook}; -#[cfg(feature = "all_openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] use {serde::Serialize, utoipa::ToSchema, utoipa_axum::routes}; /// The port on which the test server will run. @@ -31,14 +35,22 @@ async fn post_action(_body: axum::body::Bytes) -> Result { format::render().text("text response") } -#[cfg(feature = "all_openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] #[derive(Serialize, Debug, ToSchema)] pub struct Album { title: String, rating: u32, } -#[cfg(feature = "all_openapi")] +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] #[utoipa::path( get, path = "/album", @@ -81,11 +93,19 @@ pub async fn start_from_boot(boot_result: boot::BootResult) -> tokio::task::Join pub async fn start_from_ctx(ctx: AppContext) -> tokio::task::JoinHandle<()> { let app_router = AppRoutes::empty() .add_route( - #[cfg(not(feature = "all_openapi"))] + #[cfg(not(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + )))] Routes::new() .add("/", get(get_action)) .add("/", post(post_action)), - #[cfg(feature = "all_openapi")] + #[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" + ))] Routes::new() .add("/", get(get_action)) .add("/", post(post_action)) From 7a76f3a5da8d0d380caebbe8d36fbf771070bacb Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Tue, 3 Dec 2024 15:14:35 -0800 Subject: [PATCH 23/44] some docs --- src/auth/openapi.rs | 1 + src/config.rs | 25 ++++++++++++++++++++++--- src/controller/app_routes.rs | 2 ++ src/controller/openapi.rs | 8 ++++---- src/controller/routes.rs | 31 ++++++++++++++++++++++++++++++- 5 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/auth/openapi.rs b/src/auth/openapi.rs index 5b5b931ab..4923d9150 100644 --- a/src/auth/openapi.rs +++ b/src/auth/openapi.rs @@ -33,6 +33,7 @@ fn get_jwt_location() -> &'static JWTLocation { pub struct SecurityAddon; +/// Adds security to the OpenAPI doc, using the JWT location in the config impl Modify for SecurityAddon { fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { if let Some(components) = openapi.components.as_mut() { diff --git a/src/config.rs b/src/config.rs index 66ef53e9e..7e21ec800 100644 --- a/src/config.rs +++ b/src/config.rs @@ -443,10 +443,29 @@ impl Server { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct OpenAPI { /// Redoc configuration + /// Example: + /// ```yaml + /// redoc: + /// !Redoc + /// url: /redoc + /// ``` pub redoc: OpenAPIType, /// Scalar configuration + /// Example: + /// ```yaml + /// scalar: + /// !Scalar + /// url: /scalar + /// ``` pub scalar: OpenAPIType, /// Swagger configuration + /// Example: + /// ```yaml + /// swagger: + /// !Swagger + /// url: /swagger + /// spec_json_url: /openapi.json + /// ``` pub swagger: OpenAPIType, } @@ -458,7 +477,7 @@ pub struct OpenAPI { #[derive(Debug, Clone, Deserialize, Serialize)] pub enum OpenAPIType { Redoc { - /// URL for where to host the redoc OpenAPI doc, example: /redoc + /// URL for where to host the redoc OpenAPI spec, example: /redoc url: String, /// URL for openapi.json, for example: /openapi.json spec_json_url: Option, @@ -466,7 +485,7 @@ pub enum OpenAPIType { spec_yaml_url: Option, }, Scalar { - /// URL for where to host the swagger OpenAPI doc, example: /scalar + /// URL for where to host the swagger OpenAPI spec, example: /scalar url: String, /// URL for openapi.json, for example: /openapi.json spec_json_url: Option, @@ -474,7 +493,7 @@ pub enum OpenAPIType { spec_yaml_url: Option, }, Swagger { - /// URL for where to host the swagger OpenAPI doc, example: /swagger-ui + /// URL for where to host the swagger OpenAPI spec, example: /swagger-ui url: String, /// URL for openapi.json, for example: /api-docs/openapi.json spec_json_url: String, diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 85badb46f..fd3819df5 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -242,10 +242,12 @@ impl AppRoutes { feature = "openapi_scalar" ))] { + // Collect the OpenAPI spec let (_, api) = api_router.split_for_parts(); openapi::set_openapi_spec(api); } + // Serve the OpenAPI spec using the enabled OpenAPI visualizers #[cfg(feature = "openapi_redoc")] { if let OpenAPIType::Redoc { diff --git a/src/controller/openapi.rs b/src/controller/openapi.rs index 53a21f1f9..38123c714 100644 --- a/src/controller/openapi.rs +++ b/src/controller/openapi.rs @@ -5,10 +5,7 @@ use utoipa::openapi::OpenApi; use crate::{ app::AppContext, - controller::{ - format, - Response, - }, + controller::{format, Response}, Result, }; @@ -22,14 +19,17 @@ pub fn get_openapi_spec() -> &'static OpenApi { OPENAPI_SPEC.get().unwrap() } +/// Axum handler that returns the OpenAPI spec as JSON pub async fn openapi_spec_json() -> Result { format::json(get_openapi_spec()) } +/// Axum handler that returns the OpenAPI spec as YAML pub async fn openapi_spec_yaml() -> Result { format::text(&get_openapi_spec().to_yaml()?) } +/// Adds the OpenAPI endpoints the app router pub fn add_openapi_endpoints( mut app: AXRouter, json_url: Option, diff --git a/src/controller/routes.rs b/src/controller/routes.rs index d4aef0ae4..6f5bcad32 100644 --- a/src/controller/routes.rs +++ b/src/controller/routes.rs @@ -97,7 +97,36 @@ impl Routes { /// format::json(Health { ok: true }) /// } /// Routes::new().add("/_ping", get(ping)); - /// ```` + /// ``` + /// + /// ## Adding a endpoint, and add it to the OpenAPI documentation + /// ```rust ignore + /// use loco_rs::prelude::*; + /// use serde::Serialize; + /// use utoipa::ToSchema; + /// use utoipa_axum::routes; + /// + /// #[derive(Serialize, ToSchema)] + /// struct Health { + /// pub ok: bool, + /// } + /// + /// /// Ping + /// /// + /// /// This endpoint is used to check the health of the service. + /// #[utoipa::path( + /// get, + /// tag = "Health", + /// path = "/_ping", + /// responses( + /// (status = 200, body = Health), + /// ), + /// )] + /// async fn ping() -> Result { + /// format::json(Health { ok: true }) + /// } + /// Routes::new().add("/_ping", routes!(ping)); + /// ``` #[must_use] pub fn add(mut self, uri: &str, method: impl Into) -> Self { let method = method.into(); From 162ae444c633047cf7d6cd7dc02f1c31984815d7 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Tue, 3 Dec 2024 15:25:07 -0800 Subject: [PATCH 24/44] clippy --- src/controller/routes.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controller/routes.rs b/src/controller/routes.rs index 6f5bcad32..557038b45 100644 --- a/src/controller/routes.rs +++ b/src/controller/routes.rs @@ -99,7 +99,7 @@ impl Routes { /// Routes::new().add("/_ping", get(ping)); /// ``` /// - /// ## Adding a endpoint, and add it to the OpenAPI documentation + /// ## Adding a endpoint, and add it to the `OpenAPI` documentation /// ```rust ignore /// use loco_rs::prelude::*; /// use serde::Serialize; @@ -220,7 +220,7 @@ impl Routes { impl fmt::Debug for LocoMethodRouter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Axum(router) => write!(f, "{:?}", router), + Self::Axum(router) => write!(f, "{router:?}"), #[cfg(any( feature = "openapi_swagger", feature = "openapi_redoc", @@ -244,7 +244,7 @@ impl LocoMethodRouter { >::Future: Send + 'static, { match self { - LocoMethodRouter::Axum(router) => LocoMethodRouter::Axum(router.layer(layer)), + Self::Axum(router) => Self::Axum(router.layer(layer)), #[cfg(any( feature = "openapi_swagger", feature = "openapi_redoc", @@ -257,7 +257,7 @@ impl LocoMethodRouter { impl From> for LocoMethodRouter { fn from(router: MethodRouter) -> Self { - LocoMethodRouter::Axum(router) + Self::Axum(router) } } From e1a64a6d9e50e743dad88a5ae89e7feac96e98a0 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Fri, 13 Dec 2024 14:38:27 -0800 Subject: [PATCH 25/44] docs: docs-site OpenAPI --- docs-site/content/docs/the-app/controller.md | 113 +++++++++++++++++++ src/app.rs | 2 +- 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/docs-site/content/docs/the-app/controller.md b/docs-site/content/docs/the-app/controller.md index c4f4b6233..80b133e24 100644 --- a/docs-site/content/docs/the-app/controller.md +++ b/docs-site/content/docs/the-app/controller.md @@ -759,6 +759,119 @@ impl Hooks for App { } ``` +# OpenAPI Integration Setup +The Loco OpenAPI integration is generated using [`Utopia`](https://github.com/juhaku/utoipa) + +## `Cargo.toml` features flages +Edit your `Cargo.toml` file and add one or multiple of the following features flages: +- `swagger-ui` +- `redoc` +- `scalar` +- `all_openapi` + +```toml +loco-rs = { version = "0.13", features = ["scalar"] } +``` + +## Configuration +Add the corresponding OpenAPI visualizer to the config file +```yaml +#... +server: + ... + openapi: + redoc: + !Redoc + url: /redoc + # spec_json_url: /redoc/openapi.json + # spec_yaml_url: /redoc/openapi.yaml + scalar: + !Scalar + url: /scalar + # spec_json_url: /scalar/openapi.json + # spec_yaml_url: /scalar/openapi.yaml + swagger: + !Swagger + url: /swagger-ui + spec_json_url: /api-docs/openapi.json # spec_json_url is required for swagger-ui + # spec_yaml_url: /api-docs/openapi.yaml +``` +## Inital OpenAPI Spec +Modifies the OpenAPI spec before the routes are added, allowing you to edit [openapi::info](https://docs.rs/utoipa/latest/utoipa/openapi/info/struct.Info.html) + +```rust +// src/app.rs +use utoipa::OpenApi; +use loco_rs::auth::openapi::{set_jwt_location_ctx, SecurityAddon}; + +impl Hooks for App { + #... + fn inital_openapi_spec(ctx: &AppContext) -> utoipa::openapi::OpenApi { + set_jwt_location_ctx(ctx); + + #[derive(OpenApi)] + #[openapi( + modifiers(&SecurityAddon), + info( + title = "Loco Demo", + description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." + ) + )] + struct ApiDoc; + ApiDoc::openapi() + } +``` + +## Generating the OpenAPI spec for a route +Only routes that are annotated with `utoipa::path` will be included in the OpenAPI spec. + +```rust +#[utoipa::path( + get, + path = "/album", + responses( + (status = 200, description = "Album found", body = Album), + ), +)] +async fn get_action_openapi() -> Result { + format::json(Album { + title: "VH II".to_string(), + rating: 10, + }) +} +``` + +Make sure to add `#[derive(ToSchema)]` on any struct that included in `utoipa::path`. +```rust +use utoipa::ToSchema; + +#[derive(Serialize, Debug, ToSchema)] +pub struct Album { + title: String, + rating: u32, +} +``` + +## Adding routes to the OpenAPI spec visualizer +Swap the `axum::routing::MethodFilter` to `routes!` +### Before +```rust +Routes::new() + .add("/album", get(get_action_openapi)), +``` +### After +```rust +use utoipa_axum::routes; + +Routes::new() + .add("/album", routes!(get_action_openapi)), +``` +### Note: do not add multiple routes inside the `routes!` macro +```rust +Routes::new() + .add("/album", routes!(get_action_1_do_not_do_this, get_action_2_do_not_do_this)), +``` + # Pagination In many scenarios, when querying data and returning responses to users, pagination is crucial. In `Loco`, we provide a straightforward method to paginate your data and maintain a consistent pagination response schema for your API responses. diff --git a/src/app.rs b/src/app.rs index 89c3a318e..ba5e1c7a0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -214,7 +214,7 @@ pub trait Hooks: Send { /// actions before the application stops completely. async fn on_shutdown(_ctx: &AppContext) {} - /// Modify the OpenAPI spec before the routes are added, allowing you to edit (openapi::info)[https://docs.rs/utoipa/latest/utoipa/openapi/info/struct.Info.html] + /// Modifies the OpenAPI spec before the routes are added, allowing you to edit [openapi::info](https://docs.rs/utoipa/latest/utoipa/openapi/info/struct.Info.html) /// # Examples /// ```rust ignore /// fn inital_openapi_spec(_ctx: &AppContext) -> utoipa::openapi::OpenApi { From 8872a10c1c8c1ef937b1baef25646660664f5ce2 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Fri, 13 Dec 2024 17:10:58 -0800 Subject: [PATCH 26/44] docs: SecurityAddon --- docs-site/content/docs/the-app/controller.md | 23 +++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/docs-site/content/docs/the-app/controller.md b/docs-site/content/docs/the-app/controller.md index 80b133e24..72f2d01af 100644 --- a/docs-site/content/docs/the-app/controller.md +++ b/docs-site/content/docs/the-app/controller.md @@ -797,7 +797,7 @@ server: # spec_yaml_url: /api-docs/openapi.yaml ``` ## Inital OpenAPI Spec -Modifies the OpenAPI spec before the routes are added, allowing you to edit [openapi::info](https://docs.rs/utoipa/latest/utoipa/openapi/info/struct.Info.html) +Modifies the OpenAPI spec before the routes are added, allowing you to edit [`openapi::info`](https://docs.rs/utoipa/latest/utoipa/openapi/info/struct.Info.html) ```rust // src/app.rs @@ -823,7 +823,7 @@ impl Hooks for App { ``` ## Generating the OpenAPI spec for a route -Only routes that are annotated with `utoipa::path` will be included in the OpenAPI spec. +Only routes that are annotated with [`utoipa::path`](https://docs.rs/utoipa/latest/utoipa/attr.path.html) will be included in the OpenAPI spec. ```rust #[utoipa::path( @@ -841,7 +841,7 @@ async fn get_action_openapi() -> Result { } ``` -Make sure to add `#[derive(ToSchema)]` on any struct that included in `utoipa::path`. +Make sure to add `#[derive(ToSchema)]` on any struct that included in [`utoipa::path`](https://docs.rs/utoipa/latest/utoipa/attr.path.html). ```rust use utoipa::ToSchema; @@ -852,6 +852,23 @@ pub struct Album { } ``` +If `modifiers(&SecurityAddon)` is set in `inital_openapi_spec`, you can document the per route security in `utoipa::path`: +- `security(("jwt_token" = []))` +- `security(("api_key" = []))` +- or leave blank to remove security from the route `security(())` + +Example: +```rust +#[utoipa::path( + get, + path = "/album", + security(("jwt_token" = [])), + responses( + (status = 200, description = "Album found", body = Album), + ), +)] +``` + ## Adding routes to the OpenAPI spec visualizer Swap the `axum::routing::MethodFilter` to `routes!` ### Before From e352e882bb2b0f2a5d858c166a750761abfc7396 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Sun, 15 Dec 2024 19:35:58 -0800 Subject: [PATCH 27/44] feat: format::yaml --- src/controller/format.rs | 30 +++++++++++++++++++ src/controller/openapi.rs | 2 +- ...__format__tests__yaml_response_format.snap | 14 +++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/controller/snapshots/loco_rs__controller__format__tests__yaml_response_format.snap diff --git a/src/controller/format.rs b/src/controller/format.rs index 5d10900fe..e08e7d415 100644 --- a/src/controller/format.rs +++ b/src/controller/format.rs @@ -140,6 +140,27 @@ pub fn html(content: &str) -> Result { Ok(Html(content.to_string()).into_response()) } +/// Returns a YAML response +/// +/// # Example: +/// +/// ```rust +/// pub async fn openapi_spec_yaml() -> Result { +/// format::yaml(&get_openapi_spec().to_yaml()?) +/// } +/// ``` +/// +/// # Errors +/// +/// Currently this function doesn't return any error. this is for feature +/// functionality +pub fn yaml(content: &str) -> Result { + Ok(Builder::new() + .header(header::CONTENT_TYPE, "application/yaml") + .body(Body::from(content.to_string()))? + .into_response()) +} + /// Returns an redirect response /// /// # Example: @@ -441,6 +462,15 @@ mod tests { assert_eq!(response_body_to_string(response).await, response_content); } + #[tokio::test] + async fn yaml_response_format() { + let response_content: &str = "openapi: 3.1.0\ninfo:\n title: Loco Demo\n "; + let response = yaml(response_content).unwrap(); + + assert_debug_snapshot!(response); + assert_eq!(response_body_to_string(response).await, response_content); + } + #[tokio::test] async fn redirect_response() { let response = redirect("https://loco.rs").unwrap(); diff --git a/src/controller/openapi.rs b/src/controller/openapi.rs index 38123c714..cea8bcab1 100644 --- a/src/controller/openapi.rs +++ b/src/controller/openapi.rs @@ -26,7 +26,7 @@ pub async fn openapi_spec_json() -> Result { /// Axum handler that returns the OpenAPI spec as YAML pub async fn openapi_spec_yaml() -> Result { - format::text(&get_openapi_spec().to_yaml()?) + format::yaml(&get_openapi_spec().to_yaml()?) } /// Adds the OpenAPI endpoints the app router diff --git a/src/controller/snapshots/loco_rs__controller__format__tests__yaml_response_format.snap b/src/controller/snapshots/loco_rs__controller__format__tests__yaml_response_format.snap new file mode 100644 index 000000000..49dc44b2e --- /dev/null +++ b/src/controller/snapshots/loco_rs__controller__format__tests__yaml_response_format.snap @@ -0,0 +1,14 @@ +--- +source: src/controller/format.rs +expression: response +--- +Response { + status: 200, + version: HTTP/1.1, + headers: { + "content-type": "application/yaml", + }, + body: Body( + UnsyncBoxBody, + ), +} From dc97291c73c1396a830a3c420470e0e878d414a7 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Sun, 15 Dec 2024 19:46:06 -0800 Subject: [PATCH 28/44] tests: add headers content-type to snapshots --- tests/controller/openapi.rs | 1 + .../openapi_spec_[api-docs__openapi.json]@openapi.snap | 3 ++- .../openapi_spec_[api-docs__openapi.yaml]@openapi.snap | 3 ++- .../snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap | 3 ++- .../snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap | 3 ++- .../snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap | 3 ++- .../snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap | 3 ++- 7 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs index 21bbb7746..900532368 100644 --- a/tests/controller/openapi.rs +++ b/tests/controller/openapi.rs @@ -123,6 +123,7 @@ async fn openapi_spec(#[case] test_name: &str) { ( res.status().to_string(), res.url().to_string(), + res.headers().get("content-type").unwrap().to_owned(), res.text().await.unwrap(), ) ); diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap index 7defe700e..c3494ef85 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap @@ -1,9 +1,10 @@ --- source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().get(\"content-type\").unwrap().to_owned(),\nres.text().await.unwrap(),)" --- ( "200 OK", "http://localhost:5555/api-docs/openapi.json", + "application/json", "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap index 63d51e810..bf129dbbf 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap @@ -1,9 +1,10 @@ --- source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().get(\"content-type\").unwrap().to_owned(),\nres.text().await.unwrap(),)" --- ( "200 OK", "http://localhost:5555/api-docs/openapi.yaml", + "application/yaml", "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap index c5f499d57..57f842b7b 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap @@ -1,9 +1,10 @@ --- source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().get(\"content-type\").unwrap().to_owned(),\nres.text().await.unwrap(),)" --- ( "200 OK", "http://localhost:5555/redoc/openapi.json", + "application/json", "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap index 8d4d56411..a621a70e5 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap @@ -1,9 +1,10 @@ --- source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().get(\"content-type\").unwrap().to_owned(),\nres.text().await.unwrap(),)" --- ( "200 OK", "http://localhost:5555/redoc/openapi.yaml", + "application/yaml", "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap index cc1afc65e..75cc6eb5c 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap @@ -1,9 +1,10 @@ --- source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().get(\"content-type\").unwrap().to_owned(),\nres.text().await.unwrap(),)" --- ( "200 OK", "http://localhost:5555/scalar/openapi.json", + "application/json", "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap index 3258fc558..2e42cf28a 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap @@ -1,9 +1,10 @@ --- source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(), res.text().await.unwrap(),)" +expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().get(\"content-type\").unwrap().to_owned(),\nres.text().await.unwrap(),)" --- ( "200 OK", "http://localhost:5555/scalar/openapi.yaml", + "application/yaml", "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) From 93c1123cd9e2b5f4be55e91d9660004c9811ce90 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 8 Jan 2025 00:32:55 -0800 Subject: [PATCH 29/44] mark layer as sync --- src/controller/routes.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controller/routes.rs b/src/controller/routes.rs index 910972c7d..cbd3ca939 100644 --- a/src/controller/routes.rs +++ b/src/controller/routes.rs @@ -237,8 +237,8 @@ impl fmt::Debug for LocoMethodRouter { impl LocoMethodRouter { pub fn layer(self, layer: L) -> Self where - L: Layer + Clone + Send + 'static, - L::Service: Service + Clone + Send + 'static, + L: Layer + Clone + Send + Sync + 'static, + L::Service: Service + Clone + Send + Sync + 'static, >::Response: IntoResponse + 'static, >::Error: Into + 'static, >::Future: Send + 'static, From edccb0ca09477977ef74d558fcdb7a485e9bd5c5 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 8 Jan 2025 00:40:34 -0800 Subject: [PATCH 30/44] update snapshots --- .../openapi_spec_[api-docs__openapi.json]@openapi.snap | 2 +- .../openapi_spec_[api-docs__openapi.yaml]@openapi.snap | 2 +- .../snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap | 2 +- .../snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap | 2 +- .../snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap | 2 +- .../snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap index c3494ef85..65af8e27a 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap @@ -6,5 +6,5 @@ expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().ge "200 OK", "http://localhost:5555/api-docs/openapi.json", "application/json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\",\"identifier\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap index bf129dbbf..03ec81d78 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap @@ -6,5 +6,5 @@ expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().ge "200 OK", "http://localhost:5555/api-docs/openapi.yaml", "application/yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n identifier: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap index 57f842b7b..4c668a8c0 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap @@ -6,5 +6,5 @@ expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().ge "200 OK", "http://localhost:5555/redoc/openapi.json", "application/json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\",\"identifier\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap index a621a70e5..565d82b12 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap @@ -6,5 +6,5 @@ expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().ge "200 OK", "http://localhost:5555/redoc/openapi.yaml", "application/yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n identifier: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap index 75cc6eb5c..ad18f6104 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap @@ -6,5 +6,5 @@ expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().ge "200 OK", "http://localhost:5555/scalar/openapi.json", "application/json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\",\"identifier\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap index 2e42cf28a..e9baac7c6 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap @@ -6,5 +6,5 @@ expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().ge "200 OK", "http://localhost:5555/scalar/openapi.yaml", "application/yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n identifier: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) From a23dd5585d16b6157ab6da449f201a6c01bedfe9 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 8 Jan 2025 01:52:10 -0800 Subject: [PATCH 31/44] block doc test for openapi_spec_yaml --- src/controller/format.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controller/format.rs b/src/controller/format.rs index e08e7d415..a8f85ef30 100644 --- a/src/controller/format.rs +++ b/src/controller/format.rs @@ -144,7 +144,9 @@ pub fn html(content: &str) -> Result { /// /// # Example: /// -/// ```rust +/// ```rust, ignore +/// use loco_rs::prelude::*; +/// /// pub async fn openapi_spec_yaml() -> Result { /// format::yaml(&get_openapi_spec().to_yaml()?) /// } From 00e72b9c37bcc6d08d2893e069880be4606b8a31 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 8 Jan 2025 01:56:29 -0800 Subject: [PATCH 32/44] cargo loco generate controller --openapi --- loco-gen/src/controller.rs | 1 + loco-gen/src/lib.rs | 1 + loco-gen/src/scaffold.rs | 5 + .../templates/controller/openapi/controller.t | 41 ++++++ .../src/templates/controller/openapi/test.t | 39 ++++++ .../templates/scaffold/openapi/controller.t | 127 ++++++++++++++++++ .../src/templates/scaffold/openapi/test.t | 26 ++++ loco-gen/tests/templates/controller.rs | 3 +- loco-gen/tests/templates/scaffold.rs | 2 + src/cli.rs | 10 +- 10 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 loco-gen/src/templates/controller/openapi/controller.t create mode 100644 loco-gen/src/templates/controller/openapi/test.t create mode 100644 loco-gen/src/templates/scaffold/openapi/controller.t create mode 100644 loco-gen/src/templates/scaffold/openapi/test.t diff --git a/loco-gen/src/controller.rs b/loco-gen/src/controller.rs index 189ad7fb6..05b472262 100644 --- a/loco-gen/src/controller.rs +++ b/loco-gen/src/controller.rs @@ -36,5 +36,6 @@ pub fn generate( } Ok(gen_result) } + gen::ScaffoldKind::OpenApi => gen::render_template(rrgen, Path::new("controller/openapi"), &vars), } } diff --git a/loco-gen/src/lib.rs b/loco-gen/src/lib.rs index fc26b8f67..6debd6c36 100644 --- a/loco-gen/src/lib.rs +++ b/loco-gen/src/lib.rs @@ -130,6 +130,7 @@ pub enum ScaffoldKind { Api, Html, Htmx, + OpenApi, } #[derive(clap::ValueEnum, Debug, Clone)] diff --git a/loco-gen/src/scaffold.rs b/loco-gen/src/scaffold.rs index 4e03e18cc..25d3c5b44 100644 --- a/loco-gen/src/scaffold.rs +++ b/loco-gen/src/scaffold.rs @@ -91,6 +91,11 @@ pub fn generate( gen_result.rrgen.extend(res.rrgen); gen_result.local_templates.extend(res.local_templates); } + ScaffoldKind::OpenApi => { + let res = render_template(rrgen, Path::new("scaffold/openapi"), &vars)?; + gen_result.rrgen.extend(res.rrgen); + gen_result.local_templates.extend(res.local_templates); + } } Ok(gen_result) } diff --git a/loco-gen/src/templates/controller/openapi/controller.t b/loco-gen/src/templates/controller/openapi/controller.t new file mode 100644 index 000000000..56eee13ee --- /dev/null +++ b/loco-gen/src/templates/controller/openapi/controller.t @@ -0,0 +1,41 @@ +{% set file_name = name | snake_case -%} +{% set module_name = file_name | pascal_case -%} +to: src/controllers/{{ file_name }}.rs +skip_exists: true +message: "Controller `{{module_name}}` was added successfully." +injections: +- into: src/controllers/mod.rs + append: true + content: "pub mod {{ file_name }};" +- into: src/app.rs + after: "AppRoutes::" + content: " .add_route(controllers::{{ file_name }}::routes())" +--- +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::unnecessary_struct_initialization)] +#![allow(clippy::unused_async)] +use loco_rs::prelude::*; +use axum::debug_handler; +use utoipa_axum::routes; + +#[debug_handler] +pub async fn index(State(_ctx): State) -> Result { + format::empty() +} + +{% for action in actions -%} +#[debug_handler] +pub async fn {{action}}(State(_ctx): State) -> Result { + format::empty() +} + +{% endfor -%} + +pub fn routes() -> Routes { + Routes::new() + .prefix("api/{{file_name | plural}}/") + .add("/", routes!(index)) + {%- for action in actions %} + .add("{{action}}", routes!({{action}})) + {%- endfor %} +} diff --git a/loco-gen/src/templates/controller/openapi/test.t b/loco-gen/src/templates/controller/openapi/test.t new file mode 100644 index 000000000..9611bfb79 --- /dev/null +++ b/loco-gen/src/templates/controller/openapi/test.t @@ -0,0 +1,39 @@ +{% set file_name = name | snake_case -%} +{% set module_name = file_name | pascal_case -%} +to: tests/requests/{{ file_name }}.rs +skip_exists: true +message: "Tests for controller `{{module_name}}` was added successfully. Run `cargo test`." +injections: +- into: tests/requests/mod.rs + append: true + content: "pub mod {{ file_name }};" +--- +use {{pkg_name}}::app::App; +use loco_rs::testing::prelude::*; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn can_get_{{ name | plural | snake_case }}() { + request::(|request, _ctx| async move { + let res = request.get("/api/{{ name | plural | snake_case }}/").await; + assert_eq!(res.status_code(), 200); + + // you can assert content like this: + // assert_eq!(res.text(), "content"); + }) + .await; +} + +{% for action in actions -%} +#[tokio::test] +#[serial] +async fn can_get_{{action}}() { + request::(|request, _ctx| async move { + let res = request.get("/{{ name | plural | snake_case }}/{{action}}").await; + assert_eq!(res.status_code(), 200); + }) + .await; +} + +{% endfor -%} diff --git a/loco-gen/src/templates/scaffold/openapi/controller.t b/loco-gen/src/templates/scaffold/openapi/controller.t new file mode 100644 index 000000000..91cd56f32 --- /dev/null +++ b/loco-gen/src/templates/scaffold/openapi/controller.t @@ -0,0 +1,127 @@ +{% set file_name = name | snake_case -%} +{% set module_name = file_name | pascal_case -%} +to: src/controllers/{{ file_name }}.rs +skip_exists: true +message: "Controller `{{module_name}}` was added successfully." +injections: +- into: src/controllers/mod.rs + append: true + content: "pub mod {{ file_name }};" +- into: src/app.rs + after: "AppRoutes::" + content: " .add_route(controllers::{{ file_name }}::routes())" +--- +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::unnecessary_struct_initialization)] +#![allow(clippy::unused_async)] +use loco_rs::prelude::*; +use serde::{Deserialize, Serialize}; +use axum::debug_handler; +use utoipa_axum::routes; + +use crate::models::_entities::{{file_name | plural}}::{ActiveModel, Entity, Model}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Params { + {% for column in columns -%} + pub {{column.0}}: {{column.1}}, + {% endfor -%} +} + +impl Params { + fn update(&self, item: &mut ActiveModel) { + {% for column in columns -%} + item.{{column.0}} = Set(self.{{column.0}}.clone()); + {% endfor -%} + } +} + +async fn load_item(ctx: &AppContext, id: i32) -> Result { + let item = Entity::find_by_id(id).one(&ctx.db).await?; + item.ok_or_else(|| Error::NotFound) +} + +#[debug_handler] +#[utoipa::path( + get, + path = "/api/{{file_name | plural}}/", + responses( + (status = 200), + ), +)] +pub async fn list(State(ctx): State) -> Result { + format::json(Entity::find().all(&ctx.db).await?) +} + +#[debug_handler] +#[utoipa::path( + post, + path = "/api/{{file_name | plural}}/", + responses( + (status = 200), + ), +)] +pub async fn add(State(ctx): State, Json(params): Json) -> Result { + let mut item = ActiveModel { + ..Default::default() + }; + params.update(&mut item); + let item = item.insert(&ctx.db).await?; + format::json(item) +} + +#[debug_handler] +#[utoipa::path( + put, + path = "/api/{{file_name | plural}}/{id}", + responses( + (status = 200), + ), +)] +pub async fn update( + Path(id): Path, + State(ctx): State, + Json(params): Json, +) -> Result { + let item = load_item(&ctx, id).await?; + let mut item = item.into_active_model(); + params.update(&mut item); + let item = item.update(&ctx.db).await?; + format::json(item) +} + +#[debug_handler] +#[utoipa::path( + delete, + path = "/api/{{file_name | plural}}/{id}", + responses( + (status = 200), + ), +)] +pub async fn remove(Path(id): Path, State(ctx): State) -> Result { + load_item(&ctx, id).await?.delete(&ctx.db).await?; + format::empty() +} + +#[debug_handler] +#[utoipa::path( + get, + path = "/api/{{file_name | plural}}/{id}", + responses( + (status = 200), + ), +)] +pub async fn get_one(Path(id): Path, State(ctx): State) -> Result { + format::json(load_item(&ctx, id).await?) +} + +pub fn routes() -> Routes { + Routes::new() + .prefix("api/{{file_name | plural}}/") + .add("/", routes!(list)) + .add("/", routes!(add)) + .add("{id}", routes!(get_one)) + .add("{id}", routes!(remove)) + .add("{id}", routes!(update)) + .add("{id}", patch(update)) +} diff --git a/loco-gen/src/templates/scaffold/openapi/test.t b/loco-gen/src/templates/scaffold/openapi/test.t new file mode 100644 index 000000000..f20cfa0c6 --- /dev/null +++ b/loco-gen/src/templates/scaffold/openapi/test.t @@ -0,0 +1,26 @@ +{% set file_name = name | snake_case -%} +{% set module_name = file_name | pascal_case -%} +to: tests/requests/{{ file_name }}.rs +skip_exists: true +message: "Tests for controller `{{module_name}}` was added successfully. Run `cargo test`." +injections: +- into: tests/requests/mod.rs + append: true + content: "pub mod {{ file_name }};" +--- +use {{pkg_name}}::app::App; +use loco_rs::testing::prelude::*; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn can_get_{{ name | plural | snake_case }}() { + request::(|request, _ctx| async move { + let res = request.get("/api/{{ name | plural | snake_case }}/").await; + assert_eq!(res.status_code(), 200); + + // you can assert content like this: + // assert_eq!(res.text(), "content"); + }) + .await; +} diff --git a/loco-gen/tests/templates/controller.rs b/loco-gen/tests/templates/controller.rs index 9924d89a6..734058e1e 100644 --- a/loco-gen/tests/templates/controller.rs +++ b/loco-gen/tests/templates/controller.rs @@ -9,6 +9,7 @@ use std::fs; #[case(ScaffoldKind::Api)] #[case(ScaffoldKind::Html)] #[case(ScaffoldKind::Htmx)] +#[case(ScaffoldKind::OpenApi)] #[test] fn can_generate(#[case] kind: ScaffoldKind) { let actions = vec!["GET".to_string(), "POST".to_string()]; @@ -59,7 +60,7 @@ fn can_generate(#[case] kind: ScaffoldKind) { .expect("app.rs injection failed") ); - if matches!(kind, ScaffoldKind::Api) { + if matches!(kind, ScaffoldKind::Api | ScaffoldKind::OpenApi) { let test_controllers_path = tree_fs.root.join("tests").join("requests"); assert_snapshot!( "generate[tests_controller_mod_rs]", diff --git a/loco-gen/tests/templates/scaffold.rs b/loco-gen/tests/templates/scaffold.rs index b713b9c75..54c97ea0c 100644 --- a/loco-gen/tests/templates/scaffold.rs +++ b/loco-gen/tests/templates/scaffold.rs @@ -9,6 +9,7 @@ use std::fs; #[case(ScaffoldKind::Api)] #[case(ScaffoldKind::Html)] #[case(ScaffoldKind::Htmx)] +#[case(ScaffoldKind::OpenApi)] #[test] fn can_generate(#[case] kind: ScaffoldKind) { std::env::set_var("SKIP_MIGRATION", ""); @@ -131,6 +132,7 @@ fn can_generate(#[case] kind: ScaffoldKind) { ); } } + ScaffoldKind::OpenApi => (), } } diff --git a/src/cli.rs b/src/cli.rs index 63b68a755..d56c6c95b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -306,6 +306,9 @@ After running the migration, follow these steps to complete the process: /// Use API controller actions #[clap(long, group = "scaffold_kind_group")] api: bool, + + #[clap(long, group = "scaffold_kind_group")] + openapi: bool, }, /// Generate a Task based on the given name Task { @@ -380,7 +383,7 @@ impl ComponentArg { ScaffoldKind::Api } else { return Err(crate::Error::string( - "Error: One of `kind`, `htmx`, `html`, or `api` must be specified.", + "Error: One of `kind`, `htmx`, `html`, `api`, or `openapi` must be specified.", )); }; @@ -393,6 +396,7 @@ impl ComponentArg { htmx, html, api, + openapi, } => { let kind = if let Some(kind) = kind { kind @@ -402,9 +406,11 @@ impl ComponentArg { ScaffoldKind::Html } else if api { ScaffoldKind::Api + } else if openapi { + ScaffoldKind::OpenApi } else { return Err(crate::Error::string( - "Error: One of `kind`, `htmx`, `html`, or `api` must be specified.", + "Error: One of `kind`, `htmx`, `html`, `api`, or `openapi` must be specified.", )); }; From 42e978912325ba60869b110c7b410f2b7f9ba40c Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 15 Jan 2025 04:20:21 -0800 Subject: [PATCH 33/44] update features --- Cargo.toml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cdba8471e..3f9be0d6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,14 +136,11 @@ cfg-if = "1" uuid = { version = "1.10.0", features = ["v4", "fast-rng"] } # OpenAPI -utoipa = { version = "5.0.0", features = ["yaml"], optional = true } -utoipa-axum = { version = "0.1.0", optional = true } -utoipa-swagger-ui = { version = "8.0", features = [ - "axum", - "vendored", -], optional = true } -utoipa-redoc = { version = "5.0.0", features = ["axum"], optional = true } -utoipa-scalar = { version = "0.2.0", features = ["axum"], optional = true } +utoipa = { workspace = true, optional = true } +utoipa-axum = { workspace = true, optional = true } +utoipa-swagger-ui = { workspace = true, optional = true } +utoipa-redoc = { workspace = true, optional = true } +utoipa-scalar = { workspace = true, optional = true } # File Upload opendal = { version = "0.50.2", default-features = false, features = [ @@ -195,6 +192,13 @@ tower-http = { version = "0.6.1", features = [ "compression-full", ] } +# OpenAPI +utoipa = { version = "5.0.0", features = ["yaml"] } +utoipa-axum = { version = "0.1.0" } +utoipa-swagger-ui = { version = "8.0", features = ["axum", "vendored"] } +utoipa-redoc = { version = "5.0.0", features = ["axum"] } +utoipa-scalar = { version = "0.2.0", features = ["axum"] } + [dependencies.sea-orm-migration] optional = true version = "1.0.0" From 152de4bbb477f060da8ce5f5121986cccab376ca Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 15 Jan 2025 04:20:34 -0800 Subject: [PATCH 34/44] prelude and docs update --- docs-site/content/docs/the-app/controller.md | 28 +++++++++----------- src/prelude.rs | 13 +++++++++ 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/docs-site/content/docs/the-app/controller.md b/docs-site/content/docs/the-app/controller.md index 668d72793..d8143b176 100644 --- a/docs-site/content/docs/the-app/controller.md +++ b/docs-site/content/docs/the-app/controller.md @@ -113,7 +113,7 @@ Note that by-design _sharing state between controllers and workers have no meani ### Shared state in tasks -Tasks don't really have a value for shared state, as they have a similar life as any exec'd binary. The process fires up, boots, creates all resources needed (connects to db, etc.), performs the task logic, and then the +Tasks don't really have a value for shared state, as they have a similar life as any exec'd binary. The process fires up, boots, creates all resources needed (connects to db, etc.), performs the task logic, and then the ## Routes in Controllers @@ -416,7 +416,7 @@ By default, Loco uses Bearer authentication for JWT. However, you can customize auth: # JWT authentication jwt: - location: + location: from: Cookie name: token ... @@ -427,7 +427,7 @@ By default, Loco uses Bearer authentication for JWT. However, you can customize auth: # JWT authentication jwt: - location: + location: from: Query name: token ... @@ -700,7 +700,7 @@ middlewares: ``` ## CORS -This middleware enables Cross-Origin Resource Sharing (CORS) by allowing configurable origins, methods, and headers in HTTP requests. +This middleware enables Cross-Origin Resource Sharing (CORS) by allowing configurable origins, methods, and headers in HTTP requests. It can be tailored to fit various application requirements, supporting permissive CORS or specific rules as defined in the middleware configuration. ```yaml @@ -765,17 +765,20 @@ impl Hooks for App { ``` # OpenAPI Integration Setup -The Loco OpenAPI integration is generated using [`Utopia`](https://github.com/juhaku/utoipa) +The Loco OpenAPI integration is generated using [`Utoipa`](https://github.com/juhaku/utoipa) ## `Cargo.toml` features flages Edit your `Cargo.toml` file and add one or multiple of the following features flages: -- `swagger-ui` -- `redoc` -- `scalar` +- `openapi_swagger` +- `openapi_redoc` +- `openapi_scalar` - `all_openapi` ```toml -loco-rs = { version = "0.13", features = ["scalar"] } +[workspace.dependencies] +loco-rs = { version = "0.14", features = [ + "openapi_scalar", +] } ``` ## Configuration @@ -806,7 +809,6 @@ Modifies the OpenAPI spec before the routes are added, allowing you to edit [`op ```rust // src/app.rs -use utoipa::OpenApi; use loco_rs::auth::openapi::{set_jwt_location_ctx, SecurityAddon}; impl Hooks for App { @@ -848,8 +850,6 @@ async fn get_action_openapi() -> Result { Make sure to add `#[derive(ToSchema)]` on any struct that included in [`utoipa::path`](https://docs.rs/utoipa/latest/utoipa/attr.path.html). ```rust -use utoipa::ToSchema; - #[derive(Serialize, Debug, ToSchema)] pub struct Album { title: String, @@ -883,8 +883,6 @@ Routes::new() ``` ### After ```rust -use utoipa_axum::routes; - Routes::new() .add("/album", routes!(get_action_openapi)), ``` @@ -990,7 +988,7 @@ impl PaginationResponse { ``` -# Testing +# Testing When testing controllers, the goal is to call the router's controller endpoint and verify the HTTP response, including the status code, response content, headers, and more. To initialize a test request, use `use loco_rs::testing::prelude::*;`, which prepares your app routers, providing the request instance and the application context. diff --git a/src/prelude.rs b/src/prelude.rs index 47353fd17..c9ce33edc 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -48,5 +48,18 @@ pub use crate::{ pub mod model { pub use crate::model::query; } +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] +pub use utoipa::{path, OpenApi, ToSchema}; +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] +pub use utoipa_axum::routes; + #[cfg(feature = "testing")] pub use crate::testing::prelude::*; From 4958d36f181debc0a32b14d63ef76344e2feb415 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 15 Jan 2025 05:40:43 -0800 Subject: [PATCH 35/44] more prelude changes --- src/prelude.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/prelude.rs b/src/prelude.rs index 33ba3a6a9..e18a3104c 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -54,7 +54,13 @@ pub mod model { feature = "openapi_redoc", feature = "openapi_scalar" ))] -pub use utoipa::{path, OpenApi, ToSchema}; +pub use utoipa; +#[cfg(any( + feature = "openapi_swagger", + feature = "openapi_redoc", + feature = "openapi_scalar" +))] +pub use utoipa::{openapi::*, path, schema, OpenApi, ToSchema}; #[cfg(any( feature = "openapi_swagger", feature = "openapi_redoc", From f833e6bcdee1ef675e2e2c1317b470b341257409 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 15 Jan 2025 05:56:16 -0800 Subject: [PATCH 36/44] make unused openapi optional in config file --- src/config.rs | 6 +++--- src/controller/app_routes.rs | 12 ++++++------ src/tests_cfg/config.rs | 12 ++++++------ tests/controller/openapi.rs | 35 ++++++++++++++++++++++++++++++++--- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/config.rs b/src/config.rs index 182e11b6a..ed2236b11 100644 --- a/src/config.rs +++ b/src/config.rs @@ -449,7 +449,7 @@ pub struct OpenAPI { /// !Redoc /// url: /redoc /// ``` - pub redoc: OpenAPIType, + pub redoc: Option, /// Scalar configuration /// Example: /// ```yaml @@ -457,7 +457,7 @@ pub struct OpenAPI { /// !Scalar /// url: /scalar /// ``` - pub scalar: OpenAPIType, + pub scalar: Option, /// Swagger configuration /// Example: /// ```yaml @@ -466,7 +466,7 @@ pub struct OpenAPI { /// url: /swagger /// spec_json_url: /openapi.json /// ``` - pub swagger: OpenAPIType, + pub swagger: Option, } #[cfg(any( diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index fd3819df5..62e6cae10 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -250,11 +250,11 @@ impl AppRoutes { // Serve the OpenAPI spec using the enabled OpenAPI visualizers #[cfg(feature = "openapi_redoc")] { - if let OpenAPIType::Redoc { + if let Some(OpenAPIType::Redoc { url, spec_json_url, spec_yaml_url, - } = ctx.config.server.openapi.redoc.clone() + }) = ctx.config.server.openapi.redoc.clone() { app = app.merge(Redoc::with_url(url, openapi::get_openapi_spec().clone())); app = openapi::add_openapi_endpoints(app, spec_json_url, spec_yaml_url); @@ -263,11 +263,11 @@ impl AppRoutes { #[cfg(feature = "openapi_scalar")] { - if let OpenAPIType::Scalar { + if let Some(OpenAPIType::Scalar { url, spec_json_url, spec_yaml_url, - } = ctx.config.server.openapi.scalar.clone() + }) = ctx.config.server.openapi.scalar.clone() { app = app.merge(Scalar::with_url(url, openapi::get_openapi_spec().clone())); app = openapi::add_openapi_endpoints(app, spec_json_url, spec_yaml_url); @@ -276,11 +276,11 @@ impl AppRoutes { #[cfg(feature = "openapi_swagger")] { - if let OpenAPIType::Swagger { + if let Some(OpenAPIType::Swagger { url, spec_json_url, spec_yaml_url, - } = ctx.config.server.openapi.swagger.clone() + }) = ctx.config.server.openapi.swagger.clone() { app = app.merge( SwaggerUi::new(url).url(spec_json_url, openapi::get_openapi_spec().clone()), diff --git a/src/tests_cfg/config.rs b/src/tests_cfg/config.rs index 67e1e8acd..45c33c23d 100644 --- a/src/tests_cfg/config.rs +++ b/src/tests_cfg/config.rs @@ -29,21 +29,21 @@ pub fn test_config() -> Config { feature = "openapi_scalar" ))] openapi: config::OpenAPI { - redoc: config::OpenAPIType::Redoc { + redoc: Some(config::OpenAPIType::Redoc { url: "/redoc".to_string(), spec_json_url: Some("/redoc/openapi.json".to_string()), spec_yaml_url: Some("/redoc/openapi.yaml".to_string()), - }, - scalar: config::OpenAPIType::Scalar { + }), + scalar: Some(config::OpenAPIType::Scalar { url: "/scalar".to_string(), spec_json_url: Some("/scalar/openapi.json".to_string()), spec_yaml_url: Some("/scalar/openapi.yaml".to_string()), - }, - swagger: config::OpenAPIType::Swagger { + }), + swagger: Some(config::OpenAPIType::Swagger { url: "/swagger-ui".to_string(), spec_json_url: "/api-docs/openapi.json".to_string(), spec_yaml_url: Some("/api-docs/openapi.yaml".to_string()), - }, + }), }, }, #[cfg(feature = "with-db")] diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs index 900532368..98310395a 100644 --- a/tests/controller/openapi.rs +++ b/tests/controller/openapi.rs @@ -44,9 +44,38 @@ async fn openapi(#[case] mut test_name: &str) { let ctx: AppContext = tests_cfg::app::get_app_context().await; match test_name { - "/redoc" => assert_eq!(ctx.config.server.openapi.redoc.url(), test_name), - "/scalar" => assert_eq!(ctx.config.server.openapi.scalar.url(), test_name), - _ => assert_eq!(ctx.config.server.openapi.swagger.url(), test_name), + "/redoc" => { + assert_eq!( + ctx.config + .server + .openapi + .redoc + .clone() + .expect("redoc url is missing in test config") + .url(), + test_name + ) + } + "/scalar" => assert_eq!( + ctx.config + .server + .openapi + .scalar + .clone() + .expect("scalar url is missing in test config") + .url(), + test_name + ), + _ => assert_eq!( + ctx.config + .server + .openapi + .swagger + .clone() + .expect("swagger url is missing in test config") + .url(), + test_name + ), } let handle = infra_cfg::server::start_from_ctx(ctx).await; From 25698375736adb18293a5a2d3f25c38e08f59fb4 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 15 Jan 2025 06:55:46 -0800 Subject: [PATCH 37/44] snapshots --- .../openapi_spec_[api-docs__openapi.json]@openapi.snap | 2 +- .../openapi_spec_[api-docs__openapi.yaml]@openapi.snap | 2 +- .../snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap | 2 +- .../snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap | 2 +- .../snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap | 2 +- .../snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap index 65af8e27a..2bf96f04c 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap @@ -6,5 +6,5 @@ expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().ge "200 OK", "http://localhost:5555/api-docs/openapi.json", "application/json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\",\"identifier\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\",\"identifier\":\"Apache-2.0\"},\"version\":\"0.14.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap index 03ec81d78..bcc2b0ddf 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap @@ -6,5 +6,5 @@ expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().ge "200 OK", "http://localhost:5555/api-docs/openapi.yaml", "application/yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n identifier: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n identifier: Apache-2.0\n version: 0.14.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap index 4c668a8c0..18cfc3005 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap @@ -6,5 +6,5 @@ expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().ge "200 OK", "http://localhost:5555/redoc/openapi.json", "application/json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\",\"identifier\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\",\"identifier\":\"Apache-2.0\"},\"version\":\"0.14.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap index 565d82b12..1a6f6bda1 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap @@ -6,5 +6,5 @@ expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().ge "200 OK", "http://localhost:5555/redoc/openapi.yaml", "application/yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n identifier: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n identifier: Apache-2.0\n version: 0.14.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap index ad18f6104..c7eed8864 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap @@ -6,5 +6,5 @@ expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().ge "200 OK", "http://localhost:5555/scalar/openapi.json", "application/json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\",\"identifier\":\"Apache-2.0\"},\"version\":\"0.13.2\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\",\"identifier\":\"Apache-2.0\"},\"version\":\"0.14.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", ) diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap index e9baac7c6..4ef056953 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap @@ -6,5 +6,5 @@ expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().ge "200 OK", "http://localhost:5555/scalar/openapi.yaml", "application/yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n identifier: Apache-2.0\n version: 0.13.2\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", + "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n identifier: Apache-2.0\n version: 0.14.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", ) From b492d682eb366a98b8b4423c67dc8c836356df3d Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Wed, 15 Jan 2025 06:56:30 -0800 Subject: [PATCH 38/44] fix panic for set_jwt_location_ctx --- src/auth/openapi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/openapi.rs b/src/auth/openapi.rs index 4923d9150..f31f51efb 100644 --- a/src/auth/openapi.rs +++ b/src/auth/openapi.rs @@ -28,7 +28,7 @@ pub fn set_jwt_location(jwt_location: JWTLocation) -> &'static JWTLocation { } fn get_jwt_location() -> &'static JWTLocation { - JWT_LOCATION.get().unwrap() + JWT_LOCATION.get().unwrap_or(&JWTLocation::Bearer) } pub struct SecurityAddon; From 970e6c6f7a3c5c07ec45c4e38809704607118d14 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Thu, 16 Jan 2025 09:22:52 -0800 Subject: [PATCH 39/44] remove demo app test --- examples/demo/config/OpenAPI.yaml | 170 ------------------------------ src/environment.rs | 12 --- 2 files changed, 182 deletions(-) delete mode 100644 examples/demo/config/OpenAPI.yaml diff --git a/examples/demo/config/OpenAPI.yaml b/examples/demo/config/OpenAPI.yaml deleted file mode 100644 index 80c7c8bd2..000000000 --- a/examples/demo/config/OpenAPI.yaml +++ /dev/null @@ -1,170 +0,0 @@ -# Loco configuration file documentation - -# Application logging configuration -logger: - # Enable or disable logging. - enable: false - # Log level, options: trace, debug, info, warn or error. - level: error - # Define the logging format. options: compact, pretty or json - format: compact - # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries - # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters. - # override_filter: trace - -# Web server configuration -server: - # Port on which the server will listen. the server binding is 0.0.0.0:{PORT} - port: 5150 - # The UI hostname or IP address that mailers will point to. - host: http://localhost - # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block - middlewares: - # Allows to limit the payload size request. payload that bigger than this file will blocked the request. - limit_payload: - # Enable/Disable the middleware. - enable: true - # the limit size. can be b,kb,kib,mb,mib,gb,gib - body_limit: 5mb - # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. - logger: - # Enable/Disable the middleware. - enable: true - # when your code is panicked, the request still returns 500 status code. - catch_panic: - # Enable/Disable the middleware. - enable: true - # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. - timeout_request: - # Enable/Disable the middleware. - enable: true - # Duration time in milliseconds. - timeout: 5000 - static_assets: - enable: true - must_exist: true - precompressed: true - folder: - path: assets - fallback: index.html - compression: - enable: true - cors: - enable: true - # Set the value of the [`Access-Control-Allow-Origin`][mdn] header - # allow_origins: - # - https://loco.rs - # Set the value of the [`Access-Control-Allow-Headers`][mdn] header - # allow_headers: - # - Content-Type - # Set the value of the [`Access-Control-Allow-Methods`][mdn] header - # allow_methods: - # - POST - # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds - # max_age: 3600 - openapi: - redoc: - !Redoc - url: /redoc - spec_json_url: /redoc/openapi.json - spec_yaml_url: /redoc/openapi.yaml - scalar: - !Scalar - url: /scalar - spec_json_url: /scalar/openapi.json - spec_yaml_url: /scalar/openapi.yaml - swagger: - !Swagger - url: /swagger-ui - spec_json_url: /api-docs/openapi.json - spec_yaml_url: /api-docs/openapi.yaml - -# Worker Configuration -workers: - # specifies the worker mode. Options: - # - BackgroundQueue - Workers operate asynchronously in the background, processing queued. - # - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed. - # - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities. - mode: ForegroundBlocking - -# Mailer Configuration. -mailer: - # SMTP mailer configuration. - smtp: - # Enable/Disable smtp mailer. - enable: true - # SMTP server host. e.x localhost, smtp.gmail.com - host: localhost - # SMTP server port - port: 1025 - # Use secure connection (SSL/TLS). - secure: false - # auth: - # user: - # password: - stub: true - -# Initializers Configuration -# initializers: -# oauth2: -# authorization_code: # Authorization code grant type -# - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config. -# ... other fields - -# Database Configuration -database: - # Database connection URI - uri: {{get_env(name="DATABASE_URL", default="postgres://loco:loco@localhost:5432/loco_app")}} - # When enabled, the sql query will be logged. - enable_logging: false - # Set the timeout duration when acquiring a connection. - connect_timeout: 500 - # Set the idle duration before closing a connection. - idle_timeout: 500 - # Minimum number of connections for a pool. - min_connections: 1 - # Maximum number of connections for a pool. - max_connections: 1 - # Run migration up when application loaded - auto_migrate: true - # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode - dangerously_truncate: true - # Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode - dangerously_recreate: true - -# Queue Configuration -queue: - kind: Redis - # Redis connection URI - uri: {{get_env(name="REDIS_URL", default="redis://127.0.0.1")}} - # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode - dangerously_flush: false - -# Authentication Configuration -auth: - # JWT authentication - jwt: - # Secret key for token generation and verification - secret: PqRwLF2rhHe8J22oBeHy - # Token expiration time in seconds - expiration: 604800 # 7 days - -scheduler: - output: stdout - jobs: - write_content: - shell: true - run: "echo loco >> ./scheduler.txt" - schedule: run every 1 second - output: silent - tags: ['base', 'infra'] - - run_task: - run: "foo" - schedule: "at 10:00 am" - - list_if_users: - run: "user_report" - shell: true - schedule: "* 2 * * * *" - tags: ['base', 'users'] \ No newline at end of file diff --git a/src/environment.rs b/src/environment.rs index 74a847c1f..d799d94c0 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -141,16 +141,4 @@ mod tests { let config = Environment::Development.load_from_folder(Path::new("examples/demo/config")); assert!(config.is_ok()); } - - #[test] - #[cfg(any( - feature = "openapi_swagger", - feature = "openapi_redoc", - feature = "openapi_scalar" - ))] - fn test_from_folder_openapi() { - let config = Environment::Any("OpenAPI".to_string()) - .load_from_folder(Path::new("examples/demo/config")); - assert!(config.is_ok()); - } } From e0c1f892121e83ab8cf5985b5a6133ce180155f9 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Thu, 16 Jan 2025 09:42:57 -0800 Subject: [PATCH 40/44] bump version of utoipa --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5cc288aa4..e716e95fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -195,10 +195,10 @@ tower-http = { version = "0.6.1", features = [ # OpenAPI utoipa = { version = "5.0.0", features = ["yaml"] } -utoipa-axum = { version = "0.1.0" } -utoipa-swagger-ui = { version = "8.0", features = ["axum", "vendored"] } -utoipa-redoc = { version = "5.0.0", features = ["axum"] } -utoipa-scalar = { version = "0.2.0", features = ["axum"] } +utoipa-axum = { version = "0.2.0" } +utoipa-swagger-ui = { version = "9.0", features = ["axum", "vendored"] } +utoipa-redoc = { version = "6.0.0", features = ["axum"] } +utoipa-scalar = { version = "0.3.0", features = ["axum"] } [dependencies.sea-orm-migration] optional = true From ba43fcf8d8851bc56cf5a3065bb1a46b2a5d0c3c Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Tue, 21 Jan 2025 09:18:42 -0800 Subject: [PATCH 41/44] swap assert_debug_snapshot to json or yaml snapshot --- Cargo.toml | 7 +- tests/controller/openapi.rs | 37 ++++++--- ...spec_[api-docs__openapi.json]@openapi.snap | 79 +++++++++++++++++-- ...spec_[api-docs__openapi.yaml]@openapi.snap | 54 +++++++++++-- ...pi_spec_[redoc__openapi.json]@openapi.snap | 79 +++++++++++++++++-- ...pi_spec_[redoc__openapi.yaml]@openapi.snap | 54 +++++++++++-- ...i_spec_[scalar__openapi.json]@openapi.snap | 79 +++++++++++++++++-- ...i_spec_[scalar__openapi.yaml]@openapi.snap | 54 +++++++++++-- 8 files changed, 390 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e716e95fc..1430ef5d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -218,7 +218,12 @@ features = ["testing"] [dev-dependencies] loco-rs = { path = ".", features = ["testing"] } rstest = "0.21.0" -insta = { version = "1.34.0", features = ["redactions", "yaml", "filters"] } +insta = { version = "1.34.0", features = [ + "redactions", + "yaml", + "json", + "filters", +] } tree-fs = { version = "0.2.1" } reqwest = { version = "0.12.7" } serial_test = "3.1.1" diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs index 98310395a..3482997dd 100644 --- a/tests/controller/openapi.rs +++ b/tests/controller/openapi.rs @@ -1,4 +1,4 @@ -use insta::assert_debug_snapshot; +use insta::{assert_debug_snapshot, assert_json_snapshot, assert_yaml_snapshot}; use loco_rs::{config::OpenAPIType, prelude::*, tests_cfg}; use rstest::rstest; use serial_test::serial; @@ -147,15 +147,32 @@ async fn openapi_spec(#[case] test_name: &str) { .await .expect("valid response"); - assert_debug_snapshot!( - format!("openapi_spec_[{test_name}]"), - ( - res.status().to_string(), - res.url().to_string(), - res.headers().get("content-type").unwrap().to_owned(), - res.text().await.unwrap(), - ) - ); + let status = res.status(); + assert_eq!(status, 200); + + let content_type = res.headers().get("content-type").unwrap().to_str().unwrap(); + + match content_type { + "application/json" => { + assert_json_snapshot!( + format!("openapi_spec_[{test_name}]"), + ( + res.url().to_string(), + res.json::().await.unwrap() + ) + ) + } + "application/yaml" => { + assert_yaml_snapshot!( + format!("openapi_spec_[{test_name}]"), + ( + res.url().to_string(), + serde_yaml::from_str::(&res.text().await.unwrap()).unwrap() + ) + ) + } + _ => panic!("Invalid content type"), + } handle.abort(); } diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap index 2bf96f04c..03ce64049 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap @@ -1,10 +1,75 @@ --- source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().get(\"content-type\").unwrap().to_owned(),\nres.text().await.unwrap(),)" +expression: "(res.url().to_string(), res.json::().await.unwrap())" --- -( - "200 OK", - "http://localhost:5555/api-docs/openapi.json", - "application/json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\",\"identifier\":\"Apache-2.0\"},\"version\":\"0.14.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", -) +[ + "http://localhost:5555/api-docs/openapi.json", + { + "components": { + "schemas": { + "Album": { + "properties": { + "rating": { + "format": "int32", + "minimum": 0, + "type": "integer" + }, + "title": { + "type": "string" + } + }, + "required": [ + "title", + "rating" + ], + "type": "object" + } + }, + "securitySchemes": { + "api_key": { + "in": "header", + "name": "apikey", + "type": "apiKey" + }, + "jwt_token": { + "bearerFormat": "JWT", + "scheme": "bearer", + "type": "http" + } + } + }, + "info": { + "contact": { + "email": "dotan@rng0.io", + "name": "Dotan Nahum" + }, + "description": "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.", + "license": { + "identifier": "Apache-2.0", + "name": "Apache-2.0" + }, + "title": "Loco Demo", + "version": "0.14.0" + }, + "openapi": "3.1.0", + "paths": { + "/album": { + "get": { + "operationId": "get_action_openapi", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Album" + } + } + }, + "description": "Album found" + } + } + } + } + } + } +] diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap index bcc2b0ddf..249bbac7d 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap @@ -1,10 +1,50 @@ --- source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().get(\"content-type\").unwrap().to_owned(),\nres.text().await.unwrap(),)" +expression: "(res.url().to_string(),\nserde_yaml::from_str::(&res.text().await.unwrap()).unwrap())" --- -( - "200 OK", - "http://localhost:5555/api-docs/openapi.yaml", - "application/yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n identifier: Apache-2.0\n version: 0.14.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", -) +- "http://localhost:5555/api-docs/openapi.yaml" +- openapi: 3.1.0 + info: + title: Loco Demo + description: "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." + contact: + name: Dotan Nahum + email: dotan@rng0.io + license: + name: Apache-2.0 + identifier: Apache-2.0 + version: 0.14.0 + paths: + /album: + get: + operationId: get_action_openapi + responses: + "200": + description: Album found + content: + application/json: + schema: + $ref: "#/components/schemas/Album" + components: + schemas: + Album: + type: object + required: + - title + - rating + properties: + rating: + type: integer + format: int32 + minimum: 0 + title: + type: string + securitySchemes: + api_key: + type: apiKey + in: header + name: apikey + jwt_token: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap index 18cfc3005..d5ca8aa8a 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap @@ -1,10 +1,75 @@ --- source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().get(\"content-type\").unwrap().to_owned(),\nres.text().await.unwrap(),)" +expression: "(res.url().to_string(), res.json::().await.unwrap())" --- -( - "200 OK", - "http://localhost:5555/redoc/openapi.json", - "application/json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\",\"identifier\":\"Apache-2.0\"},\"version\":\"0.14.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", -) +[ + "http://localhost:5555/redoc/openapi.json", + { + "components": { + "schemas": { + "Album": { + "properties": { + "rating": { + "format": "int32", + "minimum": 0, + "type": "integer" + }, + "title": { + "type": "string" + } + }, + "required": [ + "title", + "rating" + ], + "type": "object" + } + }, + "securitySchemes": { + "api_key": { + "in": "header", + "name": "apikey", + "type": "apiKey" + }, + "jwt_token": { + "bearerFormat": "JWT", + "scheme": "bearer", + "type": "http" + } + } + }, + "info": { + "contact": { + "email": "dotan@rng0.io", + "name": "Dotan Nahum" + }, + "description": "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.", + "license": { + "identifier": "Apache-2.0", + "name": "Apache-2.0" + }, + "title": "Loco Demo", + "version": "0.14.0" + }, + "openapi": "3.1.0", + "paths": { + "/album": { + "get": { + "operationId": "get_action_openapi", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Album" + } + } + }, + "description": "Album found" + } + } + } + } + } + } +] diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap index 1a6f6bda1..f2a3ec10a 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap @@ -1,10 +1,50 @@ --- source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().get(\"content-type\").unwrap().to_owned(),\nres.text().await.unwrap(),)" +expression: "(res.url().to_string(),\nserde_yaml::from_str::(&res.text().await.unwrap()).unwrap())" --- -( - "200 OK", - "http://localhost:5555/redoc/openapi.yaml", - "application/yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n identifier: Apache-2.0\n version: 0.14.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", -) +- "http://localhost:5555/redoc/openapi.yaml" +- openapi: 3.1.0 + info: + title: Loco Demo + description: "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." + contact: + name: Dotan Nahum + email: dotan@rng0.io + license: + name: Apache-2.0 + identifier: Apache-2.0 + version: 0.14.0 + paths: + /album: + get: + operationId: get_action_openapi + responses: + "200": + description: Album found + content: + application/json: + schema: + $ref: "#/components/schemas/Album" + components: + schemas: + Album: + type: object + required: + - title + - rating + properties: + rating: + type: integer + format: int32 + minimum: 0 + title: + type: string + securitySchemes: + api_key: + type: apiKey + in: header + name: apikey + jwt_token: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap index c7eed8864..cf60c8453 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap @@ -1,10 +1,75 @@ --- source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().get(\"content-type\").unwrap().to_owned(),\nres.text().await.unwrap(),)" +expression: "(res.url().to_string(), res.json::().await.unwrap())" --- -( - "200 OK", - "http://localhost:5555/scalar/openapi.json", - "application/json", - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"Loco Demo\",\"description\":\"This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\",\"contact\":{\"name\":\"Dotan Nahum\",\"email\":\"dotan@rng0.io\"},\"license\":{\"name\":\"Apache-2.0\",\"identifier\":\"Apache-2.0\"},\"version\":\"0.14.0\"},\"paths\":{\"/album\":{\"get\":{\"operationId\":\"get_action_openapi\",\"responses\":{\"200\":{\"description\":\"Album found\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Album\"}}}}}}}},\"components\":{\"schemas\":{\"Album\":{\"type\":\"object\",\"required\":[\"title\",\"rating\"],\"properties\":{\"rating\":{\"type\":\"integer\",\"format\":\"int32\",\"minimum\":0},\"title\":{\"type\":\"string\"}}}},\"securitySchemes\":{\"api_key\":{\"type\":\"apiKey\",\"in\":\"header\",\"name\":\"apikey\"},\"jwt_token\":{\"type\":\"http\",\"scheme\":\"bearer\",\"bearerFormat\":\"JWT\"}}}}", -) +[ + "http://localhost:5555/scalar/openapi.json", + { + "components": { + "schemas": { + "Album": { + "properties": { + "rating": { + "format": "int32", + "minimum": 0, + "type": "integer" + }, + "title": { + "type": "string" + } + }, + "required": [ + "title", + "rating" + ], + "type": "object" + } + }, + "securitySchemes": { + "api_key": { + "in": "header", + "name": "apikey", + "type": "apiKey" + }, + "jwt_token": { + "bearerFormat": "JWT", + "scheme": "bearer", + "type": "http" + } + } + }, + "info": { + "contact": { + "email": "dotan@rng0.io", + "name": "Dotan Nahum" + }, + "description": "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.", + "license": { + "identifier": "Apache-2.0", + "name": "Apache-2.0" + }, + "title": "Loco Demo", + "version": "0.14.0" + }, + "openapi": "3.1.0", + "paths": { + "/album": { + "get": { + "operationId": "get_action_openapi", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Album" + } + } + }, + "description": "Album found" + } + } + } + } + } + } +] diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap index 4ef056953..bc81a2207 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap @@ -1,10 +1,50 @@ --- source: tests/controller/openapi.rs -expression: "(res.status().to_string(), res.url().to_string(),\nres.headers().get(\"content-type\").unwrap().to_owned(),\nres.text().await.unwrap(),)" +expression: "(res.url().to_string(),\nserde_yaml::from_str::(&res.text().await.unwrap()).unwrap())" --- -( - "200 OK", - "http://localhost:5555/scalar/openapi.yaml", - "application/yaml", - "openapi: 3.1.0\ninfo:\n title: Loco Demo\n description: This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.\n contact:\n name: Dotan Nahum\n email: dotan@rng0.io\n license:\n name: Apache-2.0\n identifier: Apache-2.0\n version: 0.14.0\npaths:\n /album:\n get:\n operationId: get_action_openapi\n responses:\n '200':\n description: Album found\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/Album'\ncomponents:\n schemas:\n Album:\n type: object\n required:\n - title\n - rating\n properties:\n rating:\n type: integer\n format: int32\n minimum: 0\n title:\n type: string\n securitySchemes:\n api_key:\n type: apiKey\n in: header\n name: apikey\n jwt_token:\n type: http\n scheme: bearer\n bearerFormat: JWT\n", -) +- "http://localhost:5555/scalar/openapi.yaml" +- openapi: 3.1.0 + info: + title: Loco Demo + description: "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." + contact: + name: Dotan Nahum + email: dotan@rng0.io + license: + name: Apache-2.0 + identifier: Apache-2.0 + version: 0.14.0 + paths: + /album: + get: + operationId: get_action_openapi + responses: + "200": + description: Album found + content: + application/json: + schema: + $ref: "#/components/schemas/Album" + components: + schemas: + Album: + type: object + required: + - title + - rating + properties: + rating: + type: integer + format: int32 + minimum: 0 + title: + type: string + securitySchemes: + api_key: + type: apiKey + in: header + name: apikey + jwt_token: + type: http + scheme: bearer + bearerFormat: JWT From facb0ae35d0a4be6b7d9dae4d1a208b4a5f16e76 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Tue, 21 Jan 2025 09:19:22 -0800 Subject: [PATCH 42/44] cargo format --- loco-gen/src/controller.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/loco-gen/src/controller.rs b/loco-gen/src/controller.rs index 05b472262..2782c975d 100644 --- a/loco-gen/src/controller.rs +++ b/loco-gen/src/controller.rs @@ -1,8 +1,10 @@ -use super::{AppInfo, GenerateResults, Result}; -use crate as gen; +use std::path::Path; + use rrgen::RRgen; use serde_json::json; -use std::path::Path; + +use super::{AppInfo, GenerateResults, Result}; +use crate as gen; pub fn generate( rrgen: &RRgen, @@ -36,6 +38,8 @@ pub fn generate( } Ok(gen_result) } - gen::ScaffoldKind::OpenApi => gen::render_template(rrgen, Path::new("controller/openapi"), &vars), + gen::ScaffoldKind::OpenApi => { + gen::render_template(rrgen, Path::new("controller/openapi"), &vars) + } } } From 06da6e9347994f449b9ddc56fa42d26679772d8c Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Tue, 21 Jan 2025 09:39:47 -0800 Subject: [PATCH 43/44] hide loco version from snapshot --- tests/controller/openapi.rs | 41 +++--- ...spec_[api-docs__openapi.json]@openapi.snap | 117 +++++++++--------- ...spec_[api-docs__openapi.yaml]@openapi.snap | 93 +++++++------- ...pi_spec_[redoc__openapi.json]@openapi.snap | 117 +++++++++--------- ...pi_spec_[redoc__openapi.yaml]@openapi.snap | 93 +++++++------- ...i_spec_[scalar__openapi.json]@openapi.snap | 117 +++++++++--------- ...i_spec_[scalar__openapi.yaml]@openapi.snap | 93 +++++++------- 7 files changed, 336 insertions(+), 335 deletions(-) diff --git a/tests/controller/openapi.rs b/tests/controller/openapi.rs index 3482997dd..6d0bc0eb2 100644 --- a/tests/controller/openapi.rs +++ b/tests/controller/openapi.rs @@ -154,22 +154,35 @@ async fn openapi_spec(#[case] test_name: &str) { match content_type { "application/json" => { - assert_json_snapshot!( - format!("openapi_spec_[{test_name}]"), - ( - res.url().to_string(), - res.json::().await.unwrap() - ) - ) + let mut json_value = res.json::().await.unwrap(); + if let Some(info) = json_value + .as_object_mut() + .and_then(|obj| obj.get_mut("info")) + { + if let Some(obj) = info.as_object_mut() { + obj.insert( + "version".to_string(), + serde_json::Value::String("*.*.*".to_string()), + ); + } + } + assert_json_snapshot!(format!("openapi_spec_[{test_name}]"), json_value) } "application/yaml" => { - assert_yaml_snapshot!( - format!("openapi_spec_[{test_name}]"), - ( - res.url().to_string(), - serde_yaml::from_str::(&res.text().await.unwrap()).unwrap() - ) - ) + let yaml_text = res.text().await.unwrap(); + let mut yaml_value = serde_yaml::from_str::(&yaml_text).unwrap(); + if let Some(info) = yaml_value + .as_mapping_mut() + .and_then(|map| map.get_mut("info")) + { + if let Some(map) = info.as_mapping_mut() { + map.insert( + serde_yaml::Value::String("version".to_string()), + serde_yaml::Value::String("*.*.*".to_string()), + ); + } + } + assert_yaml_snapshot!(format!("openapi_spec_[{test_name}]"), yaml_value) } _ => panic!("Invalid content type"), } diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap index 03ce64049..865872420 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.json]@openapi.snap @@ -1,75 +1,72 @@ --- source: tests/controller/openapi.rs -expression: "(res.url().to_string(), res.json::().await.unwrap())" +expression: json_value --- -[ - "http://localhost:5555/api-docs/openapi.json", - { - "components": { - "schemas": { - "Album": { - "properties": { - "rating": { - "format": "int32", - "minimum": 0, - "type": "integer" - }, - "title": { - "type": "string" - } +{ + "components": { + "schemas": { + "Album": { + "properties": { + "rating": { + "format": "int32", + "minimum": 0, + "type": "integer" }, - "required": [ - "title", - "rating" - ], - "type": "object" - } - }, - "securitySchemes": { - "api_key": { - "in": "header", - "name": "apikey", - "type": "apiKey" + "title": { + "type": "string" + } }, - "jwt_token": { - "bearerFormat": "JWT", - "scheme": "bearer", - "type": "http" - } + "required": [ + "title", + "rating" + ], + "type": "object" } }, - "info": { - "contact": { - "email": "dotan@rng0.io", - "name": "Dotan Nahum" + "securitySchemes": { + "api_key": { + "in": "header", + "name": "apikey", + "type": "apiKey" }, - "description": "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.", - "license": { - "identifier": "Apache-2.0", - "name": "Apache-2.0" - }, - "title": "Loco Demo", - "version": "0.14.0" + "jwt_token": { + "bearerFormat": "JWT", + "scheme": "bearer", + "type": "http" + } + } + }, + "info": { + "contact": { + "email": "dotan@rng0.io", + "name": "Dotan Nahum" }, - "openapi": "3.1.0", - "paths": { - "/album": { - "get": { - "operationId": "get_action_openapi", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Album" - } + "description": "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.", + "license": { + "identifier": "Apache-2.0", + "name": "Apache-2.0" + }, + "title": "Loco Demo", + "version": "*.*.*" + }, + "openapi": "3.1.0", + "paths": { + "/album": { + "get": { + "operationId": "get_action_openapi", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Album" } - }, - "description": "Album found" - } + } + }, + "description": "Album found" } } } } } -] +} diff --git a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap index 249bbac7d..d680f9b1d 100644 --- a/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[api-docs__openapi.yaml]@openapi.snap @@ -1,50 +1,49 @@ --- source: tests/controller/openapi.rs -expression: "(res.url().to_string(),\nserde_yaml::from_str::(&res.text().await.unwrap()).unwrap())" +expression: yaml_value --- -- "http://localhost:5555/api-docs/openapi.yaml" -- openapi: 3.1.0 - info: - title: Loco Demo - description: "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." - contact: - name: Dotan Nahum - email: dotan@rng0.io - license: - name: Apache-2.0 - identifier: Apache-2.0 - version: 0.14.0 - paths: - /album: - get: - operationId: get_action_openapi - responses: - "200": - description: Album found - content: - application/json: - schema: - $ref: "#/components/schemas/Album" - components: - schemas: - Album: - type: object - required: - - title - - rating - properties: - rating: - type: integer - format: int32 - minimum: 0 - title: - type: string - securitySchemes: - api_key: - type: apiKey - in: header - name: apikey - jwt_token: - type: http - scheme: bearer - bearerFormat: JWT +openapi: 3.1.0 +info: + title: Loco Demo + description: "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." + contact: + name: Dotan Nahum + email: dotan@rng0.io + license: + name: Apache-2.0 + identifier: Apache-2.0 + version: "*.*.*" +paths: + /album: + get: + operationId: get_action_openapi + responses: + "200": + description: Album found + content: + application/json: + schema: + $ref: "#/components/schemas/Album" +components: + schemas: + Album: + type: object + required: + - title + - rating + properties: + rating: + type: integer + format: int32 + minimum: 0 + title: + type: string + securitySchemes: + api_key: + type: apiKey + in: header + name: apikey + jwt_token: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap index d5ca8aa8a..865872420 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.json]@openapi.snap @@ -1,75 +1,72 @@ --- source: tests/controller/openapi.rs -expression: "(res.url().to_string(), res.json::().await.unwrap())" +expression: json_value --- -[ - "http://localhost:5555/redoc/openapi.json", - { - "components": { - "schemas": { - "Album": { - "properties": { - "rating": { - "format": "int32", - "minimum": 0, - "type": "integer" - }, - "title": { - "type": "string" - } +{ + "components": { + "schemas": { + "Album": { + "properties": { + "rating": { + "format": "int32", + "minimum": 0, + "type": "integer" }, - "required": [ - "title", - "rating" - ], - "type": "object" - } - }, - "securitySchemes": { - "api_key": { - "in": "header", - "name": "apikey", - "type": "apiKey" + "title": { + "type": "string" + } }, - "jwt_token": { - "bearerFormat": "JWT", - "scheme": "bearer", - "type": "http" - } + "required": [ + "title", + "rating" + ], + "type": "object" } }, - "info": { - "contact": { - "email": "dotan@rng0.io", - "name": "Dotan Nahum" + "securitySchemes": { + "api_key": { + "in": "header", + "name": "apikey", + "type": "apiKey" }, - "description": "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.", - "license": { - "identifier": "Apache-2.0", - "name": "Apache-2.0" - }, - "title": "Loco Demo", - "version": "0.14.0" + "jwt_token": { + "bearerFormat": "JWT", + "scheme": "bearer", + "type": "http" + } + } + }, + "info": { + "contact": { + "email": "dotan@rng0.io", + "name": "Dotan Nahum" }, - "openapi": "3.1.0", - "paths": { - "/album": { - "get": { - "operationId": "get_action_openapi", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Album" - } + "description": "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.", + "license": { + "identifier": "Apache-2.0", + "name": "Apache-2.0" + }, + "title": "Loco Demo", + "version": "*.*.*" + }, + "openapi": "3.1.0", + "paths": { + "/album": { + "get": { + "operationId": "get_action_openapi", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Album" } - }, - "description": "Album found" - } + } + }, + "description": "Album found" } } } } } -] +} diff --git a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap index f2a3ec10a..d680f9b1d 100644 --- a/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[redoc__openapi.yaml]@openapi.snap @@ -1,50 +1,49 @@ --- source: tests/controller/openapi.rs -expression: "(res.url().to_string(),\nserde_yaml::from_str::(&res.text().await.unwrap()).unwrap())" +expression: yaml_value --- -- "http://localhost:5555/redoc/openapi.yaml" -- openapi: 3.1.0 - info: - title: Loco Demo - description: "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." - contact: - name: Dotan Nahum - email: dotan@rng0.io - license: - name: Apache-2.0 - identifier: Apache-2.0 - version: 0.14.0 - paths: - /album: - get: - operationId: get_action_openapi - responses: - "200": - description: Album found - content: - application/json: - schema: - $ref: "#/components/schemas/Album" - components: - schemas: - Album: - type: object - required: - - title - - rating - properties: - rating: - type: integer - format: int32 - minimum: 0 - title: - type: string - securitySchemes: - api_key: - type: apiKey - in: header - name: apikey - jwt_token: - type: http - scheme: bearer - bearerFormat: JWT +openapi: 3.1.0 +info: + title: Loco Demo + description: "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." + contact: + name: Dotan Nahum + email: dotan@rng0.io + license: + name: Apache-2.0 + identifier: Apache-2.0 + version: "*.*.*" +paths: + /album: + get: + operationId: get_action_openapi + responses: + "200": + description: Album found + content: + application/json: + schema: + $ref: "#/components/schemas/Album" +components: + schemas: + Album: + type: object + required: + - title + - rating + properties: + rating: + type: integer + format: int32 + minimum: 0 + title: + type: string + securitySchemes: + api_key: + type: apiKey + in: header + name: apikey + jwt_token: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap index cf60c8453..865872420 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.json]@openapi.snap @@ -1,75 +1,72 @@ --- source: tests/controller/openapi.rs -expression: "(res.url().to_string(), res.json::().await.unwrap())" +expression: json_value --- -[ - "http://localhost:5555/scalar/openapi.json", - { - "components": { - "schemas": { - "Album": { - "properties": { - "rating": { - "format": "int32", - "minimum": 0, - "type": "integer" - }, - "title": { - "type": "string" - } +{ + "components": { + "schemas": { + "Album": { + "properties": { + "rating": { + "format": "int32", + "minimum": 0, + "type": "integer" }, - "required": [ - "title", - "rating" - ], - "type": "object" - } - }, - "securitySchemes": { - "api_key": { - "in": "header", - "name": "apikey", - "type": "apiKey" + "title": { + "type": "string" + } }, - "jwt_token": { - "bearerFormat": "JWT", - "scheme": "bearer", - "type": "http" - } + "required": [ + "title", + "rating" + ], + "type": "object" } }, - "info": { - "contact": { - "email": "dotan@rng0.io", - "name": "Dotan Nahum" + "securitySchemes": { + "api_key": { + "in": "header", + "name": "apikey", + "type": "apiKey" }, - "description": "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.", - "license": { - "identifier": "Apache-2.0", - "name": "Apache-2.0" - }, - "title": "Loco Demo", - "version": "0.14.0" + "jwt_token": { + "bearerFormat": "JWT", + "scheme": "bearer", + "type": "http" + } + } + }, + "info": { + "contact": { + "email": "dotan@rng0.io", + "name": "Dotan Nahum" }, - "openapi": "3.1.0", - "paths": { - "/album": { - "get": { - "operationId": "get_action_openapi", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Album" - } + "description": "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project.", + "license": { + "identifier": "Apache-2.0", + "name": "Apache-2.0" + }, + "title": "Loco Demo", + "version": "*.*.*" + }, + "openapi": "3.1.0", + "paths": { + "/album": { + "get": { + "operationId": "get_action_openapi", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Album" } - }, - "description": "Album found" - } + } + }, + "description": "Album found" } } } } } -] +} diff --git a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap index bc81a2207..d680f9b1d 100644 --- a/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap +++ b/tests/controller/snapshots/openapi_spec_[scalar__openapi.yaml]@openapi.snap @@ -1,50 +1,49 @@ --- source: tests/controller/openapi.rs -expression: "(res.url().to_string(),\nserde_yaml::from_str::(&res.text().await.unwrap()).unwrap())" +expression: yaml_value --- -- "http://localhost:5555/scalar/openapi.yaml" -- openapi: 3.1.0 - info: - title: Loco Demo - description: "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." - contact: - name: Dotan Nahum - email: dotan@rng0.io - license: - name: Apache-2.0 - identifier: Apache-2.0 - version: 0.14.0 - paths: - /album: - get: - operationId: get_action_openapi - responses: - "200": - description: Album found - content: - application/json: - schema: - $ref: "#/components/schemas/Album" - components: - schemas: - Album: - type: object - required: - - title - - rating - properties: - rating: - type: integer - format: int32 - minimum: 0 - title: - type: string - securitySchemes: - api_key: - type: apiKey - in: header - name: apikey - jwt_token: - type: http - scheme: bearer - bearerFormat: JWT +openapi: 3.1.0 +info: + title: Loco Demo + description: "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project." + contact: + name: Dotan Nahum + email: dotan@rng0.io + license: + name: Apache-2.0 + identifier: Apache-2.0 + version: "*.*.*" +paths: + /album: + get: + operationId: get_action_openapi + responses: + "200": + description: Album found + content: + application/json: + schema: + $ref: "#/components/schemas/Album" +components: + schemas: + Album: + type: object + required: + - title + - rating + properties: + rating: + type: integer + format: int32 + minimum: 0 + title: + type: string + securitySchemes: + api_key: + type: apiKey + in: header + name: apikey + jwt_token: + type: http + scheme: bearer + bearerFormat: JWT From 819b45ef54a32dbaf313b297863031aeaafb7733 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Tue, 21 Jan 2025 12:52:07 -0800 Subject: [PATCH 44/44] allow and defualt to None jwt_location --- src/auth/openapi.rs | 62 +++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/auth/openapi.rs b/src/auth/openapi.rs index f31f51efb..bf8c0c40d 100644 --- a/src/auth/openapi.rs +++ b/src/auth/openapi.rs @@ -7,7 +7,7 @@ use utoipa::{ use crate::{app::AppContext, config::JWTLocation}; -static JWT_LOCATION: OnceLock = OnceLock::new(); +static JWT_LOCATION: OnceLock> = OnceLock::new(); pub fn get_jwt_location_from_ctx(ctx: &AppContext) -> JWTLocation { ctx.config @@ -19,16 +19,16 @@ pub fn get_jwt_location_from_ctx(ctx: &AppContext) -> JWTLocation { .clone() } -pub fn set_jwt_location_ctx(ctx: &AppContext) -> &'static JWTLocation { +pub fn set_jwt_location_ctx(ctx: &AppContext) -> &'static Option { set_jwt_location(get_jwt_location_from_ctx(ctx)) } -pub fn set_jwt_location(jwt_location: JWTLocation) -> &'static JWTLocation { - JWT_LOCATION.get_or_init(|| jwt_location) +pub fn set_jwt_location(jwt_location: JWTLocation) -> &'static Option { + JWT_LOCATION.get_or_init(|| Some(jwt_location)) } -fn get_jwt_location() -> &'static JWTLocation { - JWT_LOCATION.get().unwrap_or(&JWTLocation::Bearer) +fn get_jwt_location() -> &'static Option { + JWT_LOCATION.get().unwrap_or(&None) } pub struct SecurityAddon; @@ -36,30 +36,32 @@ pub struct SecurityAddon; /// Adds security to the OpenAPI doc, using the JWT location in the config impl Modify for SecurityAddon { fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { - if let Some(components) = openapi.components.as_mut() { - components.add_security_schemes_from_iter([ - ( - "jwt_token", - match get_jwt_location() { - JWTLocation::Bearer => SecurityScheme::Http( - HttpBuilder::new() - .scheme(HttpAuthScheme::Bearer) - .bearer_format("JWT") - .build(), - ), - JWTLocation::Query { name } => { - SecurityScheme::ApiKey(ApiKey::Query(ApiKeyValue::new(name))) - } - JWTLocation::Cookie { name } => { - SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new(name))) - } - }, - ), - ( - "api_key", - SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("apikey"))), - ), - ]); + if let Some(jwt_location) = get_jwt_location() { + if let Some(components) = openapi.components.as_mut() { + components.add_security_schemes_from_iter([ + ( + "jwt_token", + match jwt_location { + JWTLocation::Bearer => SecurityScheme::Http( + HttpBuilder::new() + .scheme(HttpAuthScheme::Bearer) + .bearer_format("JWT") + .build(), + ), + JWTLocation::Query { name } => { + SecurityScheme::ApiKey(ApiKey::Query(ApiKeyValue::new(name))) + } + JWTLocation::Cookie { name } => { + SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new(name))) + } + }, + ), + ( + "api_key", + SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("apikey"))), + ), + ]); + } } } }