├── example
├── src
│ ├── tasks
│ │ └── mod.rs
│ ├── initializers
│ │ └── mod.rs
│ ├── views
│ │ ├── mod.rs
│ │ └── auth.rs
│ ├── mailers
│ │ ├── mod.rs
│ │ ├── auth
│ │ │ ├── welcome
│ │ │ │ ├── subject.t
│ │ │ │ ├── text.t
│ │ │ │ └── html.t
│ │ │ ├── magic_link
│ │ │ │ ├── subject.t
│ │ │ │ ├── text.t
│ │ │ │ └── html.t
│ │ │ └── forgot
│ │ │ │ ├── subject.t
│ │ │ │ ├── text.t
│ │ │ │ └── html.t
│ │ └── auth.rs
│ ├── workers
│ │ ├── mod.rs
│ │ └── downloader.rs
│ ├── models
│ │ ├── mod.rs
│ │ ├── _entities
│ │ │ ├── mod.rs
│ │ │ ├── prelude.rs
│ │ │ └── users.rs
│ │ └── users.rs
│ ├── controllers
│ │ ├── mod.rs
│ │ ├── album.rs
│ │ └── auth.rs
│ ├── lib.rs
│ ├── bin
│ │ └── main.rs
│ ├── fixtures
│ │ └── users.yaml
│ └── app.rs
├── tests
│ ├── tasks
│ │ └── mod.rs
│ ├── workers
│ │ └── mod.rs
│ ├── models
│ │ ├── mod.rs
│ │ ├── snapshots
│ │ │ ├── can_find_by_email@users-2.snap
│ │ │ ├── can_find_by_pid@users-2.snap
│ │ │ ├── handle_create_with_password_with_duplicate@users.snap
│ │ │ ├── can_validate_model@users.snap
│ │ │ ├── can_create_with_password@users.snap
│ │ │ ├── can_find_by_pid@users.snap
│ │ │ └── can_find_by_email@users.snap
│ │ └── users.rs
│ ├── mod.rs
│ └── requests
│ │ ├── mod.rs
│ │ ├── snapshots
│ │ ├── can_reset_password@auth_request.snap
│ │ ├── can_login_without_verify@auth_request.snap
│ │ ├── can_auth_with_magic_link@auth_request.snap
│ │ ├── can_get_current_user@auth_request.snap
│ │ ├── login_with_valid_password@auth_request.snap
│ │ ├── login_with_invalid_password@auth_request.snap
│ │ └── can_register@auth_request.snap
│ │ ├── album.rs
│ │ ├── prepare_data.rs
│ │ └── auth.rs
├── .rustfmt.toml
├── .cargo
│ └── config.toml
├── .gitignore
├── migration
│ ├── src
│ │ ├── lib.rs
│ │ └── m20220101_000001_users.rs
│ └── Cargo.toml
├── examples
│ └── playground.rs
├── Cargo.toml
└── config
│ ├── test.yaml
│ └── development.yaml
├── tests
├── ui
│ ├── mod.rs
│ ├── snapshots
│ │ ├── r#mod__ui__reqeust__[__swagger__].snap
│ │ ├── r#mod__ui__reqeust__[__redoc__openapi.yaml].snap
│ │ ├── r#mod__ui__reqeust__[__scalar__openapi.yaml].snap
│ │ ├── r#mod__ui__reqeust__[__swagger__openapi.yaml].snap
│ │ ├── r#mod__ui__reqeust__[__scalar].snap
│ │ ├── r#mod__ui__reqeust__[__redoc].snap
│ │ ├── r#mod__ui__reqeust__[__redoc__openapi.json].snap
│ │ ├── r#mod__ui__reqeust__[__scalar__openapi.json].snap
│ │ └── r#mod__ui__reqeust__[__swagger__openapi.json].snap
│ └── reqeust.rs
└── mod.rs
├── .gitignore
├── src
├── prelude.rs
├── utils.rs
├── openapi.rs
├── lib.rs
├── auth.rs
└── config.rs
├── Cargo.toml
├── .github
└── workflows
│ ├── example.yaml
│ └── ci.yaml
└── README.md
/example/src/tasks/mod.rs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/example/tests/tasks/mod.rs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/example/tests/workers/mod.rs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/example/src/initializers/mod.rs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tests/ui/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod reqeust;
2 |
--------------------------------------------------------------------------------
/example/src/views/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod auth;
2 |
--------------------------------------------------------------------------------
/example/tests/models/mod.rs:
--------------------------------------------------------------------------------
1 | mod users;
2 |
--------------------------------------------------------------------------------
/example/src/mailers/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod auth;
2 |
--------------------------------------------------------------------------------
/example/src/workers/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod downloader;
2 |
--------------------------------------------------------------------------------
/tests/mod.rs:
--------------------------------------------------------------------------------
1 | #[cfg(feature = "full")]
2 | mod ui;
3 |
--------------------------------------------------------------------------------
/example/src/mailers/auth/welcome/subject.t:
--------------------------------------------------------------------------------
1 | Welcome {{name}}
2 |
--------------------------------------------------------------------------------
/example/src/mailers/auth/magic_link/subject.t:
--------------------------------------------------------------------------------
1 | Magic link example
2 |
--------------------------------------------------------------------------------
/example/src/models/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod _entities;
2 | pub mod users;
3 |
--------------------------------------------------------------------------------
/example/src/controllers/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod auth;
2 |
3 | pub mod album;
4 |
--------------------------------------------------------------------------------
/example/src/mailers/auth/forgot/subject.t:
--------------------------------------------------------------------------------
1 | Your reset password link
2 |
--------------------------------------------------------------------------------
/example/.rustfmt.toml:
--------------------------------------------------------------------------------
1 | max_width = 100
2 | use_small_heuristics = "Default"
3 |
--------------------------------------------------------------------------------
/example/tests/mod.rs:
--------------------------------------------------------------------------------
1 | mod models;
2 | mod requests;
3 | mod tasks;
4 | mod workers;
5 |
--------------------------------------------------------------------------------
/example/tests/requests/mod.rs:
--------------------------------------------------------------------------------
1 | mod auth;
2 | mod prepare_data;
3 |
4 | pub mod album;
5 |
--------------------------------------------------------------------------------
/example/src/mailers/auth/magic_link/text.t:
--------------------------------------------------------------------------------
1 | Magic link with this link:
2 | {{host}}/api/auth/magic-link/{{token}}
--------------------------------------------------------------------------------
/example/src/mailers/auth/forgot/text.t:
--------------------------------------------------------------------------------
1 | Reset your password with this link:
2 |
3 | http://localhost/reset#{{resetToken}}
4 |
--------------------------------------------------------------------------------
/example/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [alias]
2 | loco = "run --"
3 | loco-tool = "run --"
4 |
5 | playground = "run --example playground"
6 |
--------------------------------------------------------------------------------
/example/src/models/_entities/mod.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
2 |
3 | pub mod prelude;
4 | pub mod users;
5 |
--------------------------------------------------------------------------------
/example/src/models/_entities/prelude.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
2 | pub use super::users::Entity as Users;
3 |
--------------------------------------------------------------------------------
/example/src/mailers/auth/welcome/text.t:
--------------------------------------------------------------------------------
1 | Welcome {{name}}, you can now log in.
2 | Verify your account with the link below:
3 |
4 | {{domain}}/api/auth/verify/{{verifyToken}}
5 |
--------------------------------------------------------------------------------
/example/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod app;
2 | pub mod controllers;
3 | pub mod initializers;
4 | pub mod mailers;
5 | pub mod models;
6 | pub mod tasks;
7 | pub mod views;
8 | pub mod workers;
9 |
--------------------------------------------------------------------------------
/example/tests/models/snapshots/can_find_by_email@users-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/models/users.rs
3 | expression: non_existing_user_results
4 | ---
5 | Err(
6 | EntityNotFound,
7 | )
8 |
--------------------------------------------------------------------------------
/example/tests/models/snapshots/can_find_by_pid@users-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/models/users.rs
3 | expression: non_existing_user_results
4 | ---
5 | Err(
6 | EntityNotFound,
7 | )
8 |
--------------------------------------------------------------------------------
/example/tests/requests/snapshots/can_reset_password@auth_request.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/requests/auth.rs
3 | expression: "(reset_response.status_code(), reset_response.text())"
4 | ---
5 | "null"
--------------------------------------------------------------------------------
/example/tests/models/snapshots/handle_create_with_password_with_duplicate@users.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/models/users.rs
3 | expression: new_user
4 | ---
5 | Err(
6 | EntityAlreadyExists,
7 | )
8 |
--------------------------------------------------------------------------------
/example/src/mailers/auth/magic_link/html.t:
--------------------------------------------------------------------------------
1 | ;
2 |
3 | Magic link example:
4 |
5 | Verify Your Account
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/src/bin/main.rs:
--------------------------------------------------------------------------------
1 | use loco_openapi_example::app::App;
2 | use loco_rs::cli;
3 | use migration::Migrator;
4 |
5 | #[tokio::main]
6 | async fn main() -> loco_rs::Result<()> {
7 | cli::main::().await
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | debug/
4 | target/
5 |
6 | # include cargo lock
7 | Cargo.lock
8 | example/loco-openapi_test.sqlite-shm
9 | example/loco-openapi_test.sqlite-wal
10 |
--------------------------------------------------------------------------------
/example/tests/requests/snapshots/can_login_without_verify@auth_request.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/requests/auth.rs
3 | expression: login_response.text()
4 | ---
5 | "{\"token\":\"TOKEN\",\"pid\":\"PID\",\"name\":\"loco\",\"is_verified\":false}"
6 |
--------------------------------------------------------------------------------
/src/prelude.rs:
--------------------------------------------------------------------------------
1 | pub use super::auth::{set_jwt_location, SecurityAddon};
2 | pub use super::openapi::openapi;
3 | pub use utoipa;
4 | pub use utoipa::{path, schema, OpenApi, ToSchema};
5 | pub use utoipa_axum::{router::OpenApiRouter, routes};
6 |
--------------------------------------------------------------------------------
/example/tests/requests/snapshots/can_auth_with_magic_link@auth_request.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/requests/auth.rs
3 | expression: magic_link_response.text()
4 | ---
5 | "{\"token\":\"TOKEN\",\"pid\":\"PID\",\"name\":\"user1\",\"is_verified\":false}"
6 |
--------------------------------------------------------------------------------
/example/tests/requests/snapshots/can_get_current_user@auth_request.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/requests/auth.rs
3 | expression: "(response.status_code(), response.text())"
4 | ---
5 | (
6 | 200,
7 | "{\"pid\":\"PID\",\"name\":\"loco\",\"email\":\"test@loco.com\"}",
8 | )
9 |
--------------------------------------------------------------------------------
/example/tests/requests/snapshots/login_with_valid_password@auth_request.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/requests/auth.rs
3 | expression: "(response.status_code(), response.text())"
4 | ---
5 | (
6 | 200,
7 | "{\"token\":\"TOKEN\",\"pid\":\"PID\",\"name\":\"loco\",\"is_verified\":true}",
8 | )
9 |
--------------------------------------------------------------------------------
/example/tests/requests/snapshots/login_with_invalid_password@auth_request.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/requests/auth.rs
3 | expression: "(response.status_code(), response.text())"
4 | ---
5 | (
6 | 401,
7 | "{\"error\":\"unauthorized\",\"description\":\"You do not have permission to access this resource\"}",
8 | )
9 |
--------------------------------------------------------------------------------
/example/tests/models/snapshots/can_validate_model@users.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/models/users.rs
3 | expression: res
4 | snapshot_kind: text
5 | ---
6 | Err(
7 | Custom(
8 | "{\"email\":[{\"code\":\"email\",\"message\":\"invalid email\"}],\"name\":[{\"code\":\"length\",\"message\":\"Name must be at least 2 characters long.\"}]}",
9 | ),
10 | )
11 |
--------------------------------------------------------------------------------
/example/src/mailers/auth/forgot/html.t:
--------------------------------------------------------------------------------
1 | ;
2 |
3 |
4 | Hey {{name}},
5 | Forgot your password? No worries! You can reset it by clicking the link below:
6 | Reset Your Password
7 | If you didn't request a password reset, please ignore this email.
8 | Best regards,
The Loco Team
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/src/mailers/auth/welcome/html.t:
--------------------------------------------------------------------------------
1 | ;
2 |
3 |
4 | Dear {{name}},
5 | Welcome to Loco! You can now log in to your account.
6 | Before you get started, please verify your account by clicking the link below:
7 |
8 | Verify Your Account
9 |
10 | Best regards,
The Loco Team
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | **/config/local.yaml
2 | **/config/*.local.yaml
3 | **/config/production.yaml
4 |
5 | # Generated by Cargo
6 | # will have compiled files and executables
7 | debug/
8 | target/
9 |
10 | Cargo.lock
11 |
12 | # These are backup files generated by rustfmt
13 | **/*.rs.bk
14 |
15 | # MSVC Windows builds of rustc generate these, which store debugging information
16 | *.pdb
17 |
18 | *.sqlite
--------------------------------------------------------------------------------
/example/tests/requests/album.rs:
--------------------------------------------------------------------------------
1 | use loco_openapi_example::app::App;
2 | use loco_rs::testing::prelude::*;
3 | use serial_test::serial;
4 |
5 | #[tokio::test]
6 | #[serial]
7 | async fn can_get_albums() {
8 | request::(|request, _ctx| async move {
9 | let res = request.get("/api/albums/").await;
10 | assert_eq!(res.status_code(), 200);
11 |
12 | // you can assert content like this:
13 | // assert_eq!(res.text(), "content");
14 | })
15 | .await;
16 | }
17 |
--------------------------------------------------------------------------------
/example/migration/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![allow(elided_lifetimes_in_paths)]
2 | #![allow(clippy::wildcard_imports)]
3 | pub use sea_orm_migration::prelude::*;
4 | mod m20220101_000001_users;
5 |
6 | pub struct Migrator;
7 |
8 | #[async_trait::async_trait]
9 | impl MigratorTrait for Migrator {
10 | fn migrations() -> Vec> {
11 | vec![
12 | Box::new(m20220101_000001_users::Migration),
13 | // inject-above (do not remove this comment)
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/example/src/workers/downloader.rs:
--------------------------------------------------------------------------------
1 | use loco_rs::prelude::*;
2 | use serde::{Deserialize, Serialize};
3 |
4 | pub struct DownloadWorker {
5 | pub ctx: AppContext,
6 | }
7 |
8 | #[derive(Deserialize, Debug, Serialize)]
9 | pub struct DownloadWorkerArgs {
10 | pub user_guid: String,
11 | }
12 |
13 | #[async_trait]
14 | impl BackgroundWorker for DownloadWorker {
15 | fn build(ctx: &AppContext) -> Self {
16 | Self { ctx: ctx.clone() }
17 | }
18 | async fn perform(&self, _args: DownloadWorkerArgs) -> Result<()> {
19 | // TODO: Some actual work goes here...
20 |
21 | Ok(())
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/example/tests/models/snapshots/can_create_with_password@users.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/models/users.rs
3 | expression: res
4 | ---
5 | Ok(
6 | Model {
7 | created_at: DATE,
8 | updated_at: DATE,
9 | id: ID
10 | pid: PID,
11 | email: "test@framework.com",
12 | password: "PASSWORD",
13 | api_key: "lo-PID",
14 | name: "framework",
15 | reset_token: None,
16 | reset_sent_at: None,
17 | email_verification_token: None,
18 | email_verification_sent_at: None,
19 | email_verified_at: None,
20 | magic_link_token: None,
21 | magic_link_expiration: None,
22 | },
23 | )
24 |
--------------------------------------------------------------------------------
/example/migration/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "migration"
3 | version = "0.1.0"
4 | edition = "2021"
5 | publish = false
6 |
7 | [lib]
8 | name = "migration"
9 | path = "src/lib.rs"
10 |
11 | [dependencies]
12 | async-std = { version = "1", features = ["attributes", "tokio1"] }
13 | loco-rs = { workspace = true }
14 |
15 |
16 | [dependencies.sea-orm-migration]
17 | version = "1.1.0"
18 | features = [
19 | # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
20 | # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
21 | # e.g.
22 | "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
23 | ]
24 |
--------------------------------------------------------------------------------
/example/tests/requests/snapshots/can_register@auth_request.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/requests/auth.rs
3 | expression: saved_user
4 | ---
5 | Ok(
6 | Model {
7 | created_at: DATE,
8 | updated_at: DATE,
9 | id: ID
10 | pid: PID,
11 | email: "test@loco.com",
12 | password: "PASSWORD",
13 | api_key: "lo-PID",
14 | name: "loco",
15 | reset_token: None,
16 | reset_sent_at: None,
17 | email_verification_token: Some(
18 | "PID",
19 | ),
20 | email_verification_sent_at: Some(
21 | DATE,
22 | ),
23 | email_verified_at: None,
24 | magic_link_token: None,
25 | magic_link_expiration: None,
26 | },
27 | )
28 |
--------------------------------------------------------------------------------
/example/src/fixtures/users.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | - id: 1
3 | pid: 11111111-1111-1111-1111-111111111111
4 | email: user1@example.com
5 | password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc"
6 | api_key: lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758
7 | name: user1
8 | created_at: "2023-11-12T12:34:56.789Z"
9 | updated_at: "2023-11-12T12:34:56.789Z"
10 | - id: 2
11 | pid: 22222222-2222-2222-2222-222222222222
12 | email: user2@example.com
13 | password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc"
14 | api_key: lo-153561ca-fa84-4e1b-813a-c62526d0a77e
15 | name: user2
16 | created_at: "2023-11-12T12:34:56.789Z"
17 | updated_at: "2023-11-12T12:34:56.789Z"
18 |
--------------------------------------------------------------------------------
/example/examples/playground.rs:
--------------------------------------------------------------------------------
1 | use loco_openapi_example::app::App;
2 | #[allow(unused_imports)]
3 | use loco_rs::{cli::playground, prelude::*};
4 |
5 | #[tokio::main]
6 | async fn main() -> loco_rs::Result<()> {
7 | let _ctx = playground::().await?;
8 |
9 | // let active_model: articles::ActiveModel = ActiveModel {
10 | // title: Set(Some("how to build apps in 3 steps".to_string())),
11 | // content: Set(Some("use Loco: https://loco.rs".to_string())),
12 | // ..Default::default()
13 | // };
14 | // active_model.insert(&ctx.db).await.unwrap();
15 |
16 | // let res = articles::Entity::find().all(&ctx.db).await.unwrap();
17 | // println!("{:?}", res);
18 | println!("welcome to playground. edit me at `examples/playground.rs`");
19 |
20 | Ok(())
21 | }
22 |
--------------------------------------------------------------------------------
/example/tests/models/snapshots/can_find_by_pid@users.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/models/users.rs
3 | expression: existing_user
4 | ---
5 | Ok(
6 | Model {
7 | created_at: 2023-11-12T12:34:56.789+00:00,
8 | updated_at: 2023-11-12T12:34:56.789+00:00,
9 | id: 1,
10 | pid: 11111111-1111-1111-1111-111111111111,
11 | email: "user1@example.com",
12 | password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc",
13 | api_key: "lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758",
14 | name: "user1",
15 | reset_token: None,
16 | reset_sent_at: None,
17 | email_verification_token: None,
18 | email_verification_sent_at: None,
19 | email_verified_at: None,
20 | magic_link_token: None,
21 | magic_link_expiration: None,
22 | },
23 | )
24 |
--------------------------------------------------------------------------------
/example/tests/models/snapshots/can_find_by_email@users.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/models/users.rs
3 | expression: existing_user
4 | ---
5 | Ok(
6 | Model {
7 | created_at: 2023-11-12T12:34:56.789+00:00,
8 | updated_at: 2023-11-12T12:34:56.789+00:00,
9 | id: 1,
10 | pid: 11111111-1111-1111-1111-111111111111,
11 | email: "user1@example.com",
12 | password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc",
13 | api_key: "lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758",
14 | name: "user1",
15 | reset_token: None,
16 | reset_sent_at: None,
17 | email_verification_token: None,
18 | email_verification_sent_at: None,
19 | email_verified_at: None,
20 | magic_link_token: None,
21 | magic_link_expiration: None,
22 | },
23 | )
24 |
--------------------------------------------------------------------------------
/tests/ui/snapshots/r#mod__ui__reqeust__[__swagger__].snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/ui/reqeust.rs
3 | expression: res.text()
4 | ---
5 |
6 |
7 |
8 |
9 |
10 | Swagger UI
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/example/src/controllers/album.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::missing_errors_doc)]
2 | #![allow(clippy::unnecessary_struct_initialization)]
3 | #![allow(clippy::unused_async)]
4 | use axum::debug_handler;
5 | use loco_openapi::prelude::*;
6 | use loco_rs::prelude::*;
7 | use serde::Serialize;
8 |
9 | #[derive(Serialize, Debug, ToSchema)]
10 | pub struct Album {
11 | title: String,
12 | rating: u32,
13 | }
14 |
15 | /// Get album
16 | ///
17 | /// Returns a title and rating
18 | #[utoipa::path(
19 | get,
20 | path = "/api/album/get_album",
21 | tags = ["album"],
22 | responses(
23 | (status = 200, description = "Album found", body = Album),
24 | ),
25 | )]
26 | #[debug_handler]
27 | pub async fn get_album(State(_ctx): State) -> Result {
28 | format::json(Album {
29 | title: "VH II".to_string(),
30 | rating: 10,
31 | })
32 | }
33 |
34 | pub fn routes() -> Routes {
35 | Routes::new()
36 | .prefix("api/album/")
37 | .add("/get_album", openapi(get(get_album), routes!(get_album)))
38 | }
39 |
--------------------------------------------------------------------------------
/example/src/views/auth.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | use crate::models::_entities::users;
4 |
5 | #[derive(Debug, Deserialize, Serialize)]
6 | pub struct LoginResponse {
7 | pub token: String,
8 | pub pid: String,
9 | pub name: String,
10 | pub is_verified: bool,
11 | }
12 |
13 | impl LoginResponse {
14 | #[must_use]
15 | pub fn new(user: &users::Model, token: &String) -> Self {
16 | Self {
17 | token: token.to_string(),
18 | pid: user.pid.to_string(),
19 | name: user.name.clone(),
20 | is_verified: user.email_verified_at.is_some(),
21 | }
22 | }
23 | }
24 |
25 | #[derive(Debug, Deserialize, Serialize)]
26 | pub struct CurrentResponse {
27 | pub pid: String,
28 | pub name: String,
29 | pub email: String,
30 | }
31 |
32 | impl CurrentResponse {
33 | #[must_use]
34 | pub fn new(user: &users::Model) -> Self {
35 | Self {
36 | pid: user.pid.to_string(),
37 | name: user.name.clone(),
38 | email: user.email.clone(),
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/example/src/models/_entities/users.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
2 |
3 | use sea_orm::entity::prelude::*;
4 | use serde::{Deserialize, Serialize};
5 |
6 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
7 | #[sea_orm(table_name = "users")]
8 | pub struct Model {
9 | pub created_at: DateTimeWithTimeZone,
10 | pub updated_at: DateTimeWithTimeZone,
11 | #[sea_orm(primary_key)]
12 | pub id: i32,
13 | pub pid: Uuid,
14 | #[sea_orm(unique)]
15 | pub email: String,
16 | pub password: String,
17 | #[sea_orm(unique)]
18 | pub api_key: String,
19 | pub name: String,
20 | pub reset_token: Option,
21 | pub reset_sent_at: Option,
22 | pub email_verification_token: Option,
23 | pub email_verification_sent_at: Option,
24 | pub email_verified_at: Option,
25 | pub magic_link_token: Option,
26 | pub magic_link_expiration: Option,
27 | }
28 |
29 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
30 | pub enum Relation {}
31 |
--------------------------------------------------------------------------------
/tests/ui/snapshots/r#mod__ui__reqeust__[__redoc__openapi.yaml].snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/ui/reqeust.rs
3 | expression: yaml_value
4 | ---
5 | openapi: 3.1.0
6 | info:
7 | title: Loco Demo Test
8 | description: Test OpenAPI spec for loco-openapi
9 | license:
10 | name: MIT OR Apache-2.0
11 | identifier: MIT OR Apache-2.0
12 | version: "[version]"
13 | paths:
14 | /api/album/get_album:
15 | get:
16 | tags:
17 | - album
18 | summary: Get album
19 | description: Returns a title and rating
20 | operationId: get_album
21 | responses:
22 | "200":
23 | description: Album found
24 | content:
25 | application/json:
26 | schema:
27 | $ref: "#/components/schemas/Album"
28 | components:
29 | schemas:
30 | Album:
31 | type: object
32 | required:
33 | - title
34 | - rating
35 | properties:
36 | rating:
37 | type: integer
38 | format: int32
39 | minimum: 0
40 | title:
41 | type: string
42 | securitySchemes:
43 | api_key:
44 | type: apiKey
45 | in: header
46 | name: apikey
47 | jwt_token:
48 | type: http
49 | scheme: bearer
50 | bearerFormat: JWT
51 |
--------------------------------------------------------------------------------
/tests/ui/snapshots/r#mod__ui__reqeust__[__scalar__openapi.yaml].snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/ui/reqeust.rs
3 | expression: yaml_value
4 | ---
5 | openapi: 3.1.0
6 | info:
7 | title: Loco Demo Test
8 | description: Test OpenAPI spec for loco-openapi
9 | license:
10 | name: MIT OR Apache-2.0
11 | identifier: MIT OR Apache-2.0
12 | version: "[version]"
13 | paths:
14 | /api/album/get_album:
15 | get:
16 | tags:
17 | - album
18 | summary: Get album
19 | description: Returns a title and rating
20 | operationId: get_album
21 | responses:
22 | "200":
23 | description: Album found
24 | content:
25 | application/json:
26 | schema:
27 | $ref: "#/components/schemas/Album"
28 | components:
29 | schemas:
30 | Album:
31 | type: object
32 | required:
33 | - title
34 | - rating
35 | properties:
36 | rating:
37 | type: integer
38 | format: int32
39 | minimum: 0
40 | title:
41 | type: string
42 | securitySchemes:
43 | api_key:
44 | type: apiKey
45 | in: header
46 | name: apikey
47 | jwt_token:
48 | type: http
49 | scheme: bearer
50 | bearerFormat: JWT
51 |
--------------------------------------------------------------------------------
/tests/ui/snapshots/r#mod__ui__reqeust__[__swagger__openapi.yaml].snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/ui/reqeust.rs
3 | expression: yaml_value
4 | ---
5 | openapi: 3.1.0
6 | info:
7 | title: Loco Demo Test
8 | description: Test OpenAPI spec for loco-openapi
9 | license:
10 | name: MIT OR Apache-2.0
11 | identifier: MIT OR Apache-2.0
12 | version: "[version]"
13 | paths:
14 | /api/album/get_album:
15 | get:
16 | tags:
17 | - album
18 | summary: Get album
19 | description: Returns a title and rating
20 | operationId: get_album
21 | responses:
22 | "200":
23 | description: Album found
24 | content:
25 | application/json:
26 | schema:
27 | $ref: "#/components/schemas/Album"
28 | components:
29 | schemas:
30 | Album:
31 | type: object
32 | required:
33 | - title
34 | - rating
35 | properties:
36 | rating:
37 | type: integer
38 | format: int32
39 | minimum: 0
40 | title:
41 | type: string
42 | securitySchemes:
43 | api_key:
44 | type: apiKey
45 | in: header
46 | name: apikey
47 | jwt_token:
48 | type: http
49 | scheme: bearer
50 | bearerFormat: JWT
51 |
--------------------------------------------------------------------------------
/tests/ui/snapshots/r#mod__ui__reqeust__[__scalar].snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/ui/reqeust.rs
3 | expression: res.text()
4 | ---
5 |
6 |
7 |
8 | Scalar
9 |
10 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "loco-openapi"
3 | version = "0.1.2"
4 | edition = "2021"
5 | publish = true
6 | license = "MIT OR Apache-2.0"
7 | repository = "https://github.com/loco-rs/loco-openapi-Initializer"
8 | homepage = "https://loco.rs/"
9 | description = "OpenAPI support to Loco framework"
10 |
11 | [lib]
12 | path = "src/lib.rs"
13 |
14 | [features]
15 | default = ["full"]
16 | full = ["swagger", "redoc", "scalar"]
17 | swagger = ["dep:utoipa-swagger-ui"]
18 | redoc = ["dep:utoipa-redoc"]
19 | scalar = ["dep:utoipa-scalar"]
20 |
21 |
22 | [workspace.dependencies]
23 | loco-rs = { version = "0.16", default-features = false }
24 |
25 | [dependencies]
26 | loco-rs = { workspace = true }
27 | serde = { version = "1", features = ["derive"] }
28 | serde_json = { version = "1" }
29 | async-trait = { version = "0.1" }
30 | axum = { version = "0.8.1" }
31 |
32 | # OpenAPI
33 | utoipa = { version = "5.0.0", features = ["yaml"] }
34 | utoipa-axum = { version = "0.2.0" }
35 | utoipa-swagger-ui = { version = "9.0.0", features = [
36 | "axum",
37 | "vendored",
38 | ], optional = true }
39 | utoipa-redoc = { version = "6.0.0", features = ["axum"], optional = true }
40 | utoipa-scalar = { version = "0.3.0", features = ["axum"], optional = true }
41 |
42 | [dev-dependencies]
43 | loco-rs = { workspace = true, features = ["testing"] }
44 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
45 | insta = { version = "1.34.0", features = [
46 | "redactions",
47 | "yaml",
48 | "json",
49 | "filters",
50 | ] }
51 | rstest = { version = "0.21.0" }
52 | serde_yaml = { version = "0.9" }
53 | serial_test = "3.2.0"
54 |
--------------------------------------------------------------------------------
/example/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 |
3 | [package]
4 | name = "loco-openapi-example"
5 | version = "0.1.0"
6 | edition = "2024"
7 | publish = false
8 | default-run = "loco_openapi-cli"
9 | license = "MIT OR Apache-2.0"
10 |
11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12 |
13 | [workspace.dependencies]
14 | loco-rs = { version = "0.16" }
15 |
16 | [dependencies]
17 | loco-openapi = { path = "../", features = ["full"] }
18 | loco-rs = { workspace = true }
19 | serde = { version = "1", features = ["derive"] }
20 | serde_json = { version = "1" }
21 | tokio = { version = "1.33.0", default-features = false, features = [
22 | "rt-multi-thread",
23 | ] }
24 | async-trait = { version = "0.1.74" }
25 | axum = { version = "0.8.1" }
26 | tracing = { version = "0.1.40" }
27 | tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] }
28 | regex = { version = "1.11.1" }
29 | migration = { path = "migration" }
30 | sea-orm = { version = "1.1.0", features = [
31 | "sqlx-sqlite",
32 | "sqlx-postgres",
33 | "runtime-tokio-rustls",
34 | "macros",
35 | ] }
36 | chrono = { version = "0.4" }
37 | validator = { version = "0.20" }
38 | uuid = { version = "1.6.0", features = ["v4"] }
39 | include_dir = { version = "0.7" }
40 |
41 | [[bin]]
42 | name = "loco_openapi-cli"
43 | path = "src/bin/main.rs"
44 | required-features = []
45 |
46 | [dev-dependencies]
47 | loco-rs = { workspace = true, features = ["testing"] }
48 | serial_test = { version = "3.1.1" }
49 | rstest = { version = "0.21.0" }
50 | insta = { version = "1.34.0", features = ["redactions", "yaml", "filters"] }
51 |
--------------------------------------------------------------------------------
/tests/ui/snapshots/r#mod__ui__reqeust__[__redoc].snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/ui/reqeust.rs
3 | expression: res.text()
4 | ---
5 |
6 |
7 |
8 | Redoc
9 |
10 |
11 |
15 |
16 |
22 |
23 |
24 |
25 |
26 |
27 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/utils.rs:
--------------------------------------------------------------------------------
1 | use std::sync::OnceLock;
2 |
3 | use axum::{response::Response, routing::get, Router as AxumRouter};
4 | use utoipa::openapi::OpenApi;
5 |
6 | use loco_rs::{controller::format, Result};
7 |
8 | static OPENAPI_SPEC: OnceLock = OnceLock::new();
9 |
10 | pub fn set_openapi_spec(api: OpenApi) -> &'static OpenApi {
11 | OPENAPI_SPEC.get_or_init(|| api)
12 | }
13 |
14 | /// # Panics
15 | ///
16 | /// Will panic if `OpenAPI` spec fails to build
17 | pub fn get_openapi_spec() -> &'static OpenApi {
18 | OPENAPI_SPEC.get().unwrap()
19 | }
20 |
21 | /// Axum handler that returns the `OpenAPI` spec as JSON
22 | ///
23 | /// # Errors
24 | /// Currently this function doesn't return any error. this is for feature
25 | /// functionality
26 | pub async fn openapi_spec_json() -> Result {
27 | format::json(get_openapi_spec())
28 | }
29 |
30 | /// Axum handler that returns the `OpenAPI` spec as YAML
31 | ///
32 | /// # Errors
33 | /// Currently this function doesn't return any error. this is for feature
34 | /// functionality
35 | pub async fn openapi_spec_yaml() -> Result {
36 | let yaml = get_openapi_spec()
37 | .to_yaml()
38 | .map_err(|e| loco_rs::Error::Any(Box::new(e)))?;
39 | format::yaml(&yaml)
40 | }
41 |
42 | /// Adds the `OpenAPI` endpoints the app router
43 | pub fn add_openapi_endpoints(
44 | mut app: AxumRouter,
45 | json_url: &Option,
46 | yaml_url: &Option,
47 | ) -> AxumRouter
48 | where
49 | T: Clone + Send + Sync + 'static,
50 | {
51 | if let Some(json_url) = json_url {
52 | app = app.route(json_url, get(openapi_spec_json));
53 | }
54 | if let Some(yaml_url) = yaml_url {
55 | app = app.route(yaml_url, get(openapi_spec_yaml));
56 | }
57 | app
58 | }
59 |
--------------------------------------------------------------------------------
/example/migration/src/m20220101_000001_users.rs:
--------------------------------------------------------------------------------
1 | use loco_rs::schema::table_auto_tz;
2 | use sea_orm_migration::{prelude::*, schema::*};
3 |
4 | #[derive(DeriveMigrationName)]
5 | pub struct Migration;
6 |
7 | #[async_trait::async_trait]
8 | impl MigrationTrait for Migration {
9 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
10 | let table = table_auto_tz(Users::Table)
11 | .col(pk_auto(Users::Id))
12 | .col(uuid(Users::Pid))
13 | .col(string_uniq(Users::Email))
14 | .col(string(Users::Password))
15 | .col(string(Users::ApiKey).unique_key())
16 | .col(string(Users::Name))
17 | .col(string_null(Users::ResetToken))
18 | .col(timestamp_with_time_zone_null(Users::ResetSentAt))
19 | .col(string_null(Users::EmailVerificationToken))
20 | .col(timestamp_with_time_zone_null(
21 | Users::EmailVerificationSentAt,
22 | ))
23 | .col(timestamp_with_time_zone_null(Users::EmailVerifiedAt))
24 | .col(string_null(Users::MagicLinkToken))
25 | .col(timestamp_with_time_zone_null(Users::MagicLinkExpiration))
26 | .to_owned();
27 | manager.create_table(table).await?;
28 | Ok(())
29 | }
30 |
31 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
32 | manager
33 | .drop_table(Table::drop().table(Users::Table).to_owned())
34 | .await
35 | }
36 | }
37 |
38 | #[derive(Iden)]
39 | pub enum Users {
40 | Table,
41 | Id,
42 | Pid,
43 | Email,
44 | Name,
45 | Password,
46 | ApiKey,
47 | ResetToken,
48 | ResetSentAt,
49 | EmailVerificationToken,
50 | EmailVerificationSentAt,
51 | EmailVerifiedAt,
52 | MagicLinkToken,
53 | MagicLinkExpiration,
54 | }
55 |
--------------------------------------------------------------------------------
/tests/ui/snapshots/r#mod__ui__reqeust__[__redoc__openapi.json].snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/ui/reqeust.rs
3 | expression: json_value
4 | ---
5 | {
6 | "components": {
7 | "schemas": {
8 | "Album": {
9 | "properties": {
10 | "rating": {
11 | "format": "int32",
12 | "minimum": 0,
13 | "type": "integer"
14 | },
15 | "title": {
16 | "type": "string"
17 | }
18 | },
19 | "required": [
20 | "title",
21 | "rating"
22 | ],
23 | "type": "object"
24 | }
25 | },
26 | "securitySchemes": {
27 | "api_key": {
28 | "in": "header",
29 | "name": "apikey",
30 | "type": "apiKey"
31 | },
32 | "jwt_token": {
33 | "bearerFormat": "JWT",
34 | "scheme": "bearer",
35 | "type": "http"
36 | }
37 | }
38 | },
39 | "info": {
40 | "description": "Test OpenAPI spec for loco-openapi",
41 | "license": {
42 | "identifier": "MIT OR Apache-2.0",
43 | "name": "MIT OR Apache-2.0"
44 | },
45 | "title": "Loco Demo Test",
46 | "version": "[version]"
47 | },
48 | "openapi": "3.1.0",
49 | "paths": {
50 | "/api/album/get_album": {
51 | "get": {
52 | "description": "Returns a title and rating",
53 | "operationId": "get_album",
54 | "responses": {
55 | "200": {
56 | "content": {
57 | "application/json": {
58 | "schema": {
59 | "$ref": "#/components/schemas/Album"
60 | }
61 | }
62 | },
63 | "description": "Album found"
64 | }
65 | },
66 | "summary": "Get album",
67 | "tags": [
68 | "album"
69 | ]
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/ui/snapshots/r#mod__ui__reqeust__[__scalar__openapi.json].snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/ui/reqeust.rs
3 | expression: json_value
4 | ---
5 | {
6 | "components": {
7 | "schemas": {
8 | "Album": {
9 | "properties": {
10 | "rating": {
11 | "format": "int32",
12 | "minimum": 0,
13 | "type": "integer"
14 | },
15 | "title": {
16 | "type": "string"
17 | }
18 | },
19 | "required": [
20 | "title",
21 | "rating"
22 | ],
23 | "type": "object"
24 | }
25 | },
26 | "securitySchemes": {
27 | "api_key": {
28 | "in": "header",
29 | "name": "apikey",
30 | "type": "apiKey"
31 | },
32 | "jwt_token": {
33 | "bearerFormat": "JWT",
34 | "scheme": "bearer",
35 | "type": "http"
36 | }
37 | }
38 | },
39 | "info": {
40 | "description": "Test OpenAPI spec for loco-openapi",
41 | "license": {
42 | "identifier": "MIT OR Apache-2.0",
43 | "name": "MIT OR Apache-2.0"
44 | },
45 | "title": "Loco Demo Test",
46 | "version": "[version]"
47 | },
48 | "openapi": "3.1.0",
49 | "paths": {
50 | "/api/album/get_album": {
51 | "get": {
52 | "description": "Returns a title and rating",
53 | "operationId": "get_album",
54 | "responses": {
55 | "200": {
56 | "content": {
57 | "application/json": {
58 | "schema": {
59 | "$ref": "#/components/schemas/Album"
60 | }
61 | }
62 | },
63 | "description": "Album found"
64 | }
65 | },
66 | "summary": "Get album",
67 | "tags": [
68 | "album"
69 | ]
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/ui/snapshots/r#mod__ui__reqeust__[__swagger__openapi.json].snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: tests/ui/reqeust.rs
3 | expression: json_value
4 | ---
5 | {
6 | "components": {
7 | "schemas": {
8 | "Album": {
9 | "properties": {
10 | "rating": {
11 | "format": "int32",
12 | "minimum": 0,
13 | "type": "integer"
14 | },
15 | "title": {
16 | "type": "string"
17 | }
18 | },
19 | "required": [
20 | "title",
21 | "rating"
22 | ],
23 | "type": "object"
24 | }
25 | },
26 | "securitySchemes": {
27 | "api_key": {
28 | "in": "header",
29 | "name": "apikey",
30 | "type": "apiKey"
31 | },
32 | "jwt_token": {
33 | "bearerFormat": "JWT",
34 | "scheme": "bearer",
35 | "type": "http"
36 | }
37 | }
38 | },
39 | "info": {
40 | "description": "Test OpenAPI spec for loco-openapi",
41 | "license": {
42 | "identifier": "MIT OR Apache-2.0",
43 | "name": "MIT OR Apache-2.0"
44 | },
45 | "title": "Loco Demo Test",
46 | "version": "[version]"
47 | },
48 | "openapi": "3.1.0",
49 | "paths": {
50 | "/api/album/get_album": {
51 | "get": {
52 | "description": "Returns a title and rating",
53 | "operationId": "get_album",
54 | "responses": {
55 | "200": {
56 | "content": {
57 | "application/json": {
58 | "schema": {
59 | "$ref": "#/components/schemas/Album"
60 | }
61 | }
62 | },
63 | "description": "Album found"
64 | }
65 | },
66 | "summary": "Get album",
67 | "tags": [
68 | "album"
69 | ]
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/example/tests/requests/prepare_data.rs:
--------------------------------------------------------------------------------
1 | use axum::http::{HeaderName, HeaderValue};
2 | use loco_openapi_example::{models::users, views::auth::LoginResponse};
3 | use loco_rs::{TestServer, app::AppContext};
4 |
5 | const USER_EMAIL: &str = "test@loco.com";
6 | const USER_PASSWORD: &str = "1234";
7 |
8 | pub struct LoggedInUser {
9 | pub user: users::Model,
10 | pub token: String,
11 | }
12 |
13 | pub async fn init_user_login(request: &TestServer, ctx: &AppContext) -> LoggedInUser {
14 | let register_payload = serde_json::json!({
15 | "name": "loco",
16 | "email": USER_EMAIL,
17 | "password": USER_PASSWORD
18 | });
19 |
20 | //Creating a new user
21 | request
22 | .post("/api/auth/register")
23 | .json(®ister_payload)
24 | .await;
25 | let user = users::Model::find_by_email(&ctx.db, USER_EMAIL)
26 | .await
27 | .unwrap();
28 |
29 | let verify_payload = serde_json::json!({
30 | "token": user.email_verification_token,
31 | });
32 |
33 | request.post("/api/auth/verify").json(&verify_payload).await;
34 |
35 | let response = request
36 | .post("/api/auth/login")
37 | .json(&serde_json::json!({
38 | "email": USER_EMAIL,
39 | "password": USER_PASSWORD
40 | }))
41 | .await;
42 |
43 | let login_response: LoginResponse = serde_json::from_str(&response.text()).unwrap();
44 |
45 | LoggedInUser {
46 | user: users::Model::find_by_email(&ctx.db, USER_EMAIL)
47 | .await
48 | .unwrap(),
49 | token: login_response.token,
50 | }
51 | }
52 |
53 | pub fn auth_header(token: &str) -> (HeaderName, HeaderValue) {
54 | let auth_header_value = HeaderValue::from_str(&format!("Bearer {}", &token)).unwrap();
55 |
56 | (HeaderName::from_static("authorization"), auth_header_value)
57 | }
58 |
--------------------------------------------------------------------------------
/.github/workflows/example.yaml:
--------------------------------------------------------------------------------
1 | name: Example
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 |
8 | env:
9 | RUST_TOOLCHAIN: stable
10 | TOOLCHAIN_PROFILE: minimal
11 |
12 | jobs:
13 | rustfmt:
14 | name: Check Style
15 | runs-on: ubuntu-latest
16 |
17 | permissions:
18 | contents: read
19 |
20 | steps:
21 | - name: Checkout the code
22 | uses: actions/checkout@v4
23 | - uses: dtolnay/rust-toolchain@stable
24 | with:
25 | toolchain: ${{ env.RUST_TOOLCHAIN }}
26 | components: rustfmt
27 | - name: Run cargo fmt
28 | run: cargo fmt --all -- --check
29 | working-directory: ./example
30 |
31 | clippy:
32 | name: Run Clippy
33 | runs-on: ubuntu-latest
34 |
35 | permissions:
36 | contents: read
37 |
38 | steps:
39 | - name: Checkout the code
40 | uses: actions/checkout@v4
41 | - uses: dtolnay/rust-toolchain@stable
42 | with:
43 | toolchain: ${{ env.RUST_TOOLCHAIN }}
44 | - name: Setup Rust cache
45 | uses: Swatinem/rust-cache@v2
46 | - name: Run cargo clippy
47 | run: cargo clippy --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms -A clippy::result_large_err
48 | working-directory: ./example
49 |
50 |
51 | test:
52 | name: Run Tests
53 | runs-on: ubuntu-latest
54 |
55 | permissions:
56 | contents: read
57 |
58 | steps:
59 | - name: Checkout the code
60 | uses: actions/checkout@v4
61 | - uses: dtolnay/rust-toolchain@stable
62 | with:
63 | toolchain: ${{ env.RUST_TOOLCHAIN }}
64 | - name: Setup Rust cache
65 | uses: Swatinem/rust-cache@v2
66 | - name: Run cargo test
67 | run: cargo test --all-features --all
68 | working-directory: ./example
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 |
8 | env:
9 | RUST_TOOLCHAIN: stable
10 | TOOLCHAIN_PROFILE: minimal
11 |
12 | jobs:
13 | check:
14 | runs-on: ubuntu-latest
15 |
16 | permissions:
17 | contents: read
18 |
19 | steps:
20 | - name: Checkout the code
21 | uses: actions/checkout@v4
22 | - uses: dtolnay/rust-toolchain@stable
23 | with:
24 | toolchain: ${{ env.RUST_TOOLCHAIN }}
25 | - name: Setup Rust cache
26 | uses: Swatinem/rust-cache@v2
27 | - uses: taiki-e/install-action@v2
28 | with:
29 | tool: cargo-hack
30 | - run: cargo hack check --each-feature
31 |
32 | rustfmt:
33 | name: Check Style
34 | runs-on: ubuntu-latest
35 |
36 | permissions:
37 | contents: read
38 |
39 | steps:
40 | - name: Checkout the code
41 | uses: actions/checkout@v4
42 | - uses: dtolnay/rust-toolchain@stable
43 | with:
44 | toolchain: ${{ env.RUST_TOOLCHAIN }}
45 | components: rustfmt
46 | - name: Setup Rust cache
47 | uses: Swatinem/rust-cache@v2
48 | - uses: taiki-e/install-action@v2
49 | with:
50 | tool: cargo-hack
51 | - name: Run cargo fmt
52 | run: cargo fmt --all -- --check
53 |
54 | clippy:
55 | name: Run Clippy
56 | runs-on: ubuntu-latest
57 |
58 | permissions:
59 | contents: read
60 |
61 | steps:
62 | - name: Checkout the code
63 | uses: actions/checkout@v4
64 | - uses: dtolnay/rust-toolchain@stable
65 | with:
66 | toolchain: ${{ env.RUST_TOOLCHAIN }}
67 | - name: Setup Rust cache
68 | uses: Swatinem/rust-cache@v2
69 | - name: Run cargo clippy
70 | run: cargo clippy --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms -A clippy::result_large_err
71 |
72 | test:
73 | name: Run Tests
74 | runs-on: ubuntu-latest
75 |
76 | permissions:
77 | contents: read
78 |
79 | steps:
80 | - name: Checkout the code
81 | uses: actions/checkout@v4
82 | - uses: dtolnay/rust-toolchain@stable
83 | with:
84 | toolchain: ${{ env.RUST_TOOLCHAIN }}
85 | - name: Setup Rust cache
86 | uses: Swatinem/rust-cache@v2
87 | - uses: taiki-e/install-action@v2
88 | with:
89 | tool: cargo-hack
90 | - run: cargo hack test --each-feature
91 |
--------------------------------------------------------------------------------
/src/openapi.rs:
--------------------------------------------------------------------------------
1 | use loco_rs::app::AppContext;
2 | use std::sync::{Mutex, OnceLock};
3 | use utoipa_axum::router::{OpenApiRouter, UtoipaMethodRouter};
4 |
5 | static OPENAPI_ROUTES: OnceLock>>> = OnceLock::new();
6 |
7 | fn get_routes() -> &'static Mutex>> {
8 | OPENAPI_ROUTES.get_or_init(|| Mutex::new(Vec::new()))
9 | }
10 |
11 | // Register a route for later merging
12 | pub fn add_route(route: OpenApiRouter) {
13 | if let Ok(mut routes) = get_routes().lock() {
14 | routes.push(route);
15 | }
16 | }
17 |
18 | // Clears all registered routes in the `OPENAPI_ROUTES`
19 | // Mostly used for testing, to prevent routes added from different test runs from overlapping
20 | pub fn clear_routes() {
21 | if let Ok(mut routes) = get_routes().lock() {
22 | routes.clear();
23 | }
24 | }
25 |
26 | // Get a merged router containing all collected routes
27 | #[must_use]
28 | pub fn get_merged_router() -> OpenApiRouter {
29 | let mut result = OpenApiRouter::new();
30 |
31 | if let Ok(routes) = get_routes().lock() {
32 | for route in routes.iter() {
33 | result = result.merge(route.clone());
34 | }
35 | }
36 | result
37 | }
38 |
39 | /// Auto collect the openapi routes
40 | /// ```rust
41 | /// # use axum::debug_handler;
42 | /// use loco_openapi::prelude::*;
43 | /// # use loco_rs::prelude::*;
44 | /// # use serde::Serialize;
45 | /// # #[derive(Serialize, Debug, ToSchema)]
46 | /// # pub struct Album {
47 | /// # title: String,
48 | /// # rating: u32,
49 | /// # }
50 | /// # #[utoipa::path(
51 | /// # get,
52 | /// # path = "/api/album/get_album",
53 | /// # tags = ["album"],
54 | /// # responses(
55 | /// # (status = 200, description = "Album found", body = Album),
56 | /// # ),
57 | /// # )]
58 | /// # #[debug_handler]
59 | /// # pub async fn get_album(State(_ctx): State) -> Result {
60 | /// # format::json(Album {
61 | /// # title: "VH II".to_string(),
62 | /// # rating: 10,
63 | /// # })
64 | /// # }
65 | ///
66 | /// // Swap from:
67 | /// Routes::new()
68 | /// .add("/album", get(get_album));
69 | /// // To:
70 | /// Routes::new()
71 | /// .add("/get_album", openapi(get(get_album), routes!(get_album)));
72 | /// ```
73 | pub fn openapi(
74 | method: axum::routing::MethodRouter,
75 | method_openapi: UtoipaMethodRouter,
76 | ) -> axum::routing::MethodRouter {
77 | let router = OpenApiRouter::new().routes(method_openapi);
78 | add_route(router);
79 | method
80 | }
81 |
--------------------------------------------------------------------------------
/example/src/mailers/auth.rs:
--------------------------------------------------------------------------------
1 | // auth mailer
2 | #![allow(non_upper_case_globals)]
3 |
4 | use loco_rs::prelude::*;
5 | use serde_json::json;
6 |
7 | use crate::models::users;
8 |
9 | static welcome: Dir<'_> = include_dir!("src/mailers/auth/welcome");
10 | static forgot: Dir<'_> = include_dir!("src/mailers/auth/forgot");
11 | static magic_link: Dir<'_> = include_dir!("src/mailers/auth/magic_link");
12 | // #[derive(Mailer)] // -- disabled for faster build speed. it works. but lets
13 | // move on for now.
14 |
15 | #[allow(clippy::module_name_repetitions)]
16 | pub struct AuthMailer {}
17 | impl Mailer for AuthMailer {}
18 | impl AuthMailer {
19 | /// Sending welcome email the the given user
20 | ///
21 | /// # Errors
22 | ///
23 | /// When email sending is failed
24 | pub async fn send_welcome(ctx: &AppContext, user: &users::Model) -> Result<()> {
25 | Self::mail_template(
26 | ctx,
27 | &welcome,
28 | mailer::Args {
29 | to: user.email.to_string(),
30 | locals: json!({
31 | "name": user.name,
32 | "verifyToken": user.email_verification_token,
33 | "domain": ctx.config.server.full_url()
34 | }),
35 | ..Default::default()
36 | },
37 | )
38 | .await?;
39 |
40 | Ok(())
41 | }
42 |
43 | /// Sending forgot password email
44 | ///
45 | /// # Errors
46 | ///
47 | /// When email sending is failed
48 | pub async fn forgot_password(ctx: &AppContext, user: &users::Model) -> Result<()> {
49 | Self::mail_template(
50 | ctx,
51 | &forgot,
52 | mailer::Args {
53 | to: user.email.to_string(),
54 | locals: json!({
55 | "name": user.name,
56 | "resetToken": user.reset_token,
57 | "domain": ctx.config.server.full_url()
58 | }),
59 | ..Default::default()
60 | },
61 | )
62 | .await?;
63 |
64 | Ok(())
65 | }
66 |
67 | /// Sends a magic link authentication email to the user.
68 | ///
69 | /// # Errors
70 | ///
71 | /// When email sending is failed
72 | pub async fn send_magic_link(ctx: &AppContext, user: &users::Model) -> Result<()> {
73 | Self::mail_template(
74 | ctx,
75 | &magic_link,
76 | mailer::Args {
77 | to: user.email.to_string(),
78 | locals: json!({
79 | "name": user.name,
80 | "token": user.magic_link_token.clone().ok_or_else(|| Error::string(
81 | "the user model not contains magic link token",
82 | ))?,
83 | "host": ctx.config.server.full_url()
84 | }),
85 | ..Default::default()
86 | },
87 | )
88 | .await?;
89 |
90 | Ok(())
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/example/src/app.rs:
--------------------------------------------------------------------------------
1 | use async_trait::async_trait;
2 | use loco_openapi::prelude::*;
3 | use loco_rs::{
4 | Result,
5 | app::{AppContext, Hooks, Initializer},
6 | bgworker::{BackgroundWorker, Queue},
7 | boot::{BootResult, StartMode, create_app},
8 | config::Config,
9 | controller::AppRoutes,
10 | db::{self, truncate_table},
11 | environment::Environment,
12 | task::Tasks,
13 | };
14 | use migration::Migrator;
15 | use std::path::Path;
16 |
17 | #[allow(unused_imports)]
18 | use crate::{controllers, models::_entities::users, tasks, workers::downloader::DownloadWorker};
19 |
20 | pub struct App;
21 | #[async_trait]
22 | impl Hooks for App {
23 | fn app_name() -> &'static str {
24 | env!("CARGO_CRATE_NAME")
25 | }
26 |
27 | fn app_version() -> String {
28 | format!(
29 | "{} ({})",
30 | env!("CARGO_PKG_VERSION"),
31 | option_env!("BUILD_SHA")
32 | .or(option_env!("GITHUB_SHA"))
33 | .unwrap_or("dev")
34 | )
35 | }
36 |
37 | async fn boot(
38 | mode: StartMode,
39 | environment: &Environment,
40 | config: Config,
41 | ) -> Result {
42 | create_app::(mode, environment, config).await
43 | }
44 |
45 | async fn initializers(ctx: &AppContext) -> Result>> {
46 | let mut initializers: Vec> = vec![];
47 |
48 | if ctx.environment != Environment::Test {
49 | initializers.push(
50 | Box::new(
51 | loco_openapi::OpenapiInitializerWithSetup::new(
52 | |ctx| {
53 | #[derive(OpenApi)]
54 | #[openapi(
55 | modifiers(&SecurityAddon),
56 | info(
57 | title = "Loco Demo",
58 | description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project."
59 | )
60 | )]
61 | struct ApiDoc;
62 | set_jwt_location(ctx.into());
63 |
64 | ApiDoc::openapi()
65 | },
66 | None,
67 | ),
68 | ) as Box
69 | );
70 | }
71 |
72 | Ok(initializers)
73 | }
74 |
75 | fn routes(_ctx: &AppContext) -> AppRoutes {
76 | AppRoutes::with_default_routes() // controller routes below
77 | .add_route(controllers::album::routes())
78 | .add_route(controllers::auth::routes())
79 | }
80 | async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
81 | queue.register(DownloadWorker::build(ctx)).await?;
82 | Ok(())
83 | }
84 |
85 | #[allow(unused_variables)]
86 | fn register_tasks(tasks: &mut Tasks) {
87 | // tasks-inject (do not remove)
88 | }
89 | async fn truncate(ctx: &AppContext) -> Result<()> {
90 | truncate_table(&ctx.db, users::Entity).await?;
91 | Ok(())
92 | }
93 | async fn seed(ctx: &AppContext, base: &Path) -> Result<()> {
94 | db::seed::(&ctx.db, &base.join("users.yaml").display().to_string())
95 | .await?;
96 | Ok(())
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/example/config/test.yaml:
--------------------------------------------------------------------------------
1 | # Loco configuration file documentation
2 |
3 | # Application logging configuration
4 | logger:
5 | # Enable or disable logging.
6 | enable: false
7 | # Enable pretty backtrace (sets RUST_BACKTRACE=1)
8 | pretty_backtrace: true
9 | # Log level, options: trace, debug, info, warn or error.
10 | level: debug
11 | # Define the logging format. options: compact, pretty or json
12 | format: compact
13 | # 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
14 | # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
15 | # override_filter: trace
16 |
17 | # Web server configuration
18 | server:
19 | # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
20 | port: 5150
21 | # The UI hostname or IP address that mailers will point to.
22 | host: http://localhost
23 | # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
24 |
25 | # Worker Configuration
26 | workers:
27 | # specifies the worker mode. Options:
28 | # - BackgroundQueue - Workers operate asynchronously in the background, processing queued.
29 | # - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.
30 | # - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.
31 | mode: ForegroundBlocking
32 |
33 |
34 |
35 | # Mailer Configuration.
36 | mailer:
37 | stub: true
38 | # SMTP mailer configuration.
39 | smtp:
40 | # Enable/Disable smtp mailer.
41 | enable: true
42 | # SMTP server host. e.x localhost, smtp.gmail.com
43 | host: localhost
44 | # SMTP server port
45 | port: 1025
46 | # Use secure connection (SSL/TLS).
47 | secure: false
48 | # auth:
49 | # user:
50 | # password:
51 |
52 | # Initializers Configuration
53 | # initializers:
54 | # oauth2:
55 | # authorization_code: # Authorization code grant type
56 | # - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config.
57 | # ... other fields
58 | initializers:
59 | openapi:
60 | scalar:
61 | url: /scalar
62 | redoc:
63 | url: /redoc
64 | swagger:
65 | url: /swagger
66 | spec_json_url: /api-docs/openapi.json
67 |
68 | # Database Configuration
69 | database:
70 | # Database connection URI
71 | uri: {{ get_env(name="DATABASE_URL", default="sqlite://loco-openapi_test.sqlite?mode=rwc") }}
72 | # When enabled, the sql query will be logged.
73 | enable_logging: false
74 | # Set the timeout duration when acquiring a connection.
75 | connect_timeout: {{ get_env(name="DB_CONNECT_TIMEOUT", default="500") }}
76 | # Set the idle duration before closing a connection.
77 | idle_timeout: {{ get_env(name="DB_IDLE_TIMEOUT", default="500") }}
78 | # Minimum number of connections for a pool.
79 | min_connections: {{ get_env(name="DB_MIN_CONNECTIONS", default="1") }}
80 | # Maximum number of connections for a pool.
81 | max_connections: {{ get_env(name="DB_MAX_CONNECTIONS", default="1") }}
82 | # Run migration up when application loaded
83 | auto_migrate: true
84 | # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
85 | dangerously_truncate: true
86 | # Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
87 | dangerously_recreate: true
88 |
89 | # Authentication Configuration
90 | auth:
91 | # JWT authentication
92 | jwt:
93 | # Secret key for token generation and verification
94 | secret: sQAX9LCnjrp8YbKkVlgb
95 | # Token expiration time in seconds
96 | expiration: 604800 # 7 days
97 |
--------------------------------------------------------------------------------
/example/config/development.yaml:
--------------------------------------------------------------------------------
1 | # Loco configuration file documentation
2 |
3 | # Application logging configuration
4 | logger:
5 | # Enable or disable logging.
6 | enable: true
7 | # Enable pretty backtrace (sets RUST_BACKTRACE=1)
8 | pretty_backtrace: true
9 | # Log level, options: trace, debug, info, warn or error.
10 | level: debug
11 | # Define the logging format. options: compact, pretty or json
12 | format: compact
13 | # 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
14 | # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
15 | # override_filter: trace
16 |
17 | # Web server configuration
18 | server:
19 | # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
20 | port: 5150
21 | # Binding for the server (which interface to bind to)
22 | binding: localhost
23 | # The UI hostname or IP address that mailers will point to.
24 | host: http://localhost
25 | # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
26 | middlewares:
27 |
28 | # Worker Configuration
29 | workers:
30 | # specifies the worker mode. Options:
31 | # - BackgroundQueue - Workers operate asynchronously in the background, processing queued.
32 | # - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.
33 | # - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.
34 | mode: BackgroundAsync
35 |
36 |
37 | # Mailer Configuration.
38 | mailer:
39 | # SMTP mailer configuration.
40 | smtp:
41 | # Enable/Disable smtp mailer.
42 | enable: true
43 | # SMTP server host. e.x localhost, smtp.gmail.com
44 | host: localhost
45 | # SMTP server port
46 | port: 1025
47 | # Use secure connection (SSL/TLS).
48 | secure: false
49 | # auth:
50 | # user:
51 | # password:
52 | # Override the SMTP hello name (default is the machine's hostname)
53 | # hello_name:
54 |
55 | # Initializers Configuration
56 | # initializers:
57 | # oauth2:
58 | # authorization_code: # Authorization code grant type
59 | # - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config.
60 | # ... other fields
61 | initializers:
62 | openapi:
63 | scalar:
64 | url: /scalar
65 | redoc:
66 | url: /redoc
67 | swagger:
68 | url: /swagger
69 | spec_json_url: /api-docs/openapi.json
70 |
71 | # Database Configuration
72 | database:
73 | # Database connection URI
74 | uri: {{ get_env(name="DATABASE_URL", default="sqlite://loco-openapi_development.sqlite?mode=rwc") }}
75 | # When enabled, the sql query will be logged.
76 | enable_logging: false
77 | # Set the timeout duration when acquiring a connection.
78 | connect_timeout: {{ get_env(name="DB_CONNECT_TIMEOUT", default="500") }}
79 | # Set the idle duration before closing a connection.
80 | idle_timeout: {{ get_env(name="DB_IDLE_TIMEOUT", default="500") }}
81 | # Minimum number of connections for a pool.
82 | min_connections: {{ get_env(name="DB_MIN_CONNECTIONS", default="1") }}
83 | # Maximum number of connections for a pool.
84 | max_connections: {{ get_env(name="DB_MAX_CONNECTIONS", default="1") }}
85 | # Run migration up when application loaded
86 | auto_migrate: true
87 | # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
88 | dangerously_truncate: false
89 | # Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
90 | dangerously_recreate: false
91 |
92 | # Authentication Configuration
93 | auth:
94 | # JWT authentication
95 | jwt:
96 | # Secret key for token generation and verification
97 | secret: bGmCQYHLRCiJ5g75TukQ
98 | # Token expiration time in seconds
99 | expiration: 604800 # 7 days
100 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | use async_trait::async_trait;
2 | use axum::Router as AxumRouter;
3 | use loco_rs::prelude::*;
4 | use utoipa::openapi::OpenApi;
5 | use utoipa_axum::router::OpenApiRouter;
6 | #[cfg(feature = "redoc")]
7 | use utoipa_redoc::{Redoc, Servable};
8 | #[cfg(feature = "scalar")]
9 | use utoipa_scalar::{Scalar, Servable as ScalarServable};
10 | #[cfg(feature = "swagger")]
11 | use utoipa_swagger_ui::SwaggerUi;
12 |
13 | use crate::config::{get_openapi_config, set_openapi_config, InitializerConfig};
14 | use crate::openapi::get_merged_router;
15 | // Always used
16 | use crate::utils::set_openapi_spec;
17 | // Only used in feature blocks
18 | #[cfg(any(feature = "redoc", feature = "scalar", feature = "swagger"))]
19 | use crate::utils::{add_openapi_endpoints, get_openapi_spec};
20 |
21 | pub mod auth;
22 | pub mod config;
23 | pub mod openapi;
24 | pub mod prelude;
25 | pub mod utils;
26 |
27 | type RouterList = Option>>;
28 | type InitialSpec = dyn Fn(&AppContext) -> OpenApi + Send + Sync + 'static;
29 |
30 | /// Loco initializer for `OpenAPI` with custom initial spec setup
31 | #[derive(Default)]
32 | pub struct OpenapiInitializerWithSetup {
33 | /// Custom setup for the initial `OpenAPI` spec, if any
34 | initial_spec: Option>,
35 | /// Routes to add to the `OpenAPI` spec
36 | routes_setup: RouterList,
37 | }
38 |
39 | impl OpenapiInitializerWithSetup {
40 | #[must_use]
41 | pub fn new(initial_spec: F, routes_setup: RouterList) -> Self
42 | where
43 | F: Fn(&AppContext) -> OpenApi + Send + Sync + 'static,
44 | {
45 | Self {
46 | initial_spec: Some(Box::new(initial_spec)),
47 | routes_setup,
48 | }
49 | }
50 | }
51 |
52 | #[async_trait]
53 | impl Initializer for OpenapiInitializerWithSetup {
54 | fn name(&self) -> String {
55 | "openapi".to_string()
56 | }
57 |
58 | async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result {
59 | // Use the InitializerConfig wrapper
60 | set_openapi_config(InitializerConfig::from(&ctx.config.initializers).into())?;
61 |
62 | let mut api_router: OpenApiRouter = self
63 | .initial_spec
64 | .as_ref()
65 | .map_or_else(OpenApiRouter::new, |custom_spec_fn| {
66 | OpenApiRouter::with_openapi(custom_spec_fn(ctx))
67 | });
68 |
69 | // Merge all manually collected routes
70 | if let Some(ref routes_setup) = self.routes_setup {
71 | for route in routes_setup {
72 | api_router = api_router.merge(route.clone());
73 | }
74 | }
75 |
76 | // Merge all automatically collected routes
77 | api_router = api_router.merge(get_merged_router());
78 |
79 | // Collect the `OpenAPI` spec
80 | let (_, open_api_spec) = api_router.split_for_parts();
81 | set_openapi_spec(open_api_spec);
82 |
83 | // Use `_` prefix as config might be unused if no features are enabled
84 | let Some(_open_api_config) = get_openapi_config() else {
85 | // No config, return original router
86 | return Ok(router);
87 | };
88 |
89 | // Create a new router for UI endpoints
90 | #[allow(unused_mut)]
91 | let mut ui_router = AxumRouter::new();
92 |
93 | // Serve the `OpenAPI` spec using the enabled `OpenAPI` visualizers
94 | #[cfg(feature = "redoc")]
95 | if let Some(config::OpenAPIType::Redoc {
96 | url,
97 | spec_json_url,
98 | spec_yaml_url,
99 | }) = get_openapi_config().and_then(|c| c.redoc.as_ref())
100 | {
101 | ui_router = ui_router.merge(Redoc::with_url(url, get_openapi_spec().clone()));
102 | ui_router = add_openapi_endpoints(ui_router, spec_json_url, spec_yaml_url);
103 | }
104 |
105 | #[cfg(feature = "scalar")]
106 | if let Some(config::OpenAPIType::Scalar {
107 | url,
108 | spec_json_url,
109 | spec_yaml_url,
110 | }) = get_openapi_config().and_then(|c| c.scalar.as_ref())
111 | {
112 | ui_router = ui_router.merge(Scalar::with_url(url, get_openapi_spec().clone()));
113 | ui_router = add_openapi_endpoints(ui_router, spec_json_url, spec_yaml_url);
114 | }
115 |
116 | #[cfg(feature = "swagger")]
117 | if let Some(config::OpenAPIType::Swagger {
118 | url,
119 | spec_json_url,
120 | spec_yaml_url,
121 | }) = get_openapi_config().and_then(|c| c.swagger.as_ref())
122 | {
123 | ui_router = ui_router
124 | .merge(SwaggerUi::new(url).url(spec_json_url.clone(), get_openapi_spec().clone()));
125 | ui_router = add_openapi_endpoints(ui_router, &None, spec_yaml_url);
126 | }
127 |
128 | // Merge the UI router with the main router
129 | Ok(router.merge(ui_router))
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/auth.rs:
--------------------------------------------------------------------------------
1 | use std::sync::OnceLock;
2 |
3 | use utoipa::{
4 | openapi::security::{ApiKey, ApiKeyValue, HttpAuthScheme, HttpBuilder, SecurityScheme},
5 | Modify,
6 | };
7 |
8 | // Import Loco types for conversion
9 | use loco_rs::{app::AppContext, config::JWTLocation as LocoJWTLocation};
10 |
11 | // Our own JWTLocation enum that doesn't depend on Loco
12 | #[derive(Default, Debug, Clone, PartialEq, Eq)]
13 | pub enum JWTLocation {
14 | #[default]
15 | Bearer,
16 | Query(String),
17 | Cookie(String),
18 | }
19 |
20 | // Implement From trait for conversion from Loco type to our type
21 | impl From<&LocoJWTLocation> for JWTLocation {
22 | fn from(loco_location: &LocoJWTLocation) -> Self {
23 | match loco_location {
24 | LocoJWTLocation::Bearer => Self::Bearer,
25 | LocoJWTLocation::Query { name } => Self::Query(name.clone()),
26 | LocoJWTLocation::Cookie { name } => Self::Cookie(name.clone()),
27 | }
28 | }
29 | }
30 |
31 | // Support conversion from JWTLocationConfig (Single/Multiple) to JWTLocation
32 | impl From<&loco_rs::config::JWTLocationConfig> for JWTLocation {
33 | fn from(cfg: &loco_rs::config::JWTLocationConfig) -> Self {
34 | match cfg {
35 | loco_rs::config::JWTLocationConfig::Single(loc) => Self::from(loc),
36 | loco_rs::config::JWTLocationConfig::Multiple(locs) => {
37 | locs.first().map_or(Self::Bearer, Self::from)
38 | }
39 | }
40 | }
41 | }
42 |
43 | // Direct conversion from AppContext to JWTLocation for ease of use
44 | impl From<&AppContext> for JWTLocation {
45 | fn from(ctx: &AppContext) -> Self {
46 | ctx.config
47 | .auth
48 | .as_ref()
49 | .and_then(|auth| auth.jwt.as_ref())
50 | .and_then(|jwt| jwt.location.as_ref())
51 | .map_or(Self::Bearer, Self::from)
52 | }
53 | }
54 |
55 | static JWT_LOCATION: OnceLock