diff --git a/Cargo.lock b/Cargo.lock index 4d7f909..c66f6c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,8 +137,6 @@ dependencies = [ "tower-http", "tracing", "utoipa", - "utoipa-scalar", - "utoipa-swagger-ui", "validator", ] @@ -640,9 +638,11 @@ dependencies = [ "api", "app", "axum 0.7.5", + "doc", "http-body-util", "models", "sea-orm", + "serde_json", "shuttle-axum", "shuttle-runtime", "shuttle-shared-db", @@ -956,6 +956,18 @@ dependencies = [ "syn 2.0.71", ] +[[package]] +name = "doc" +version = "0.1.0" +dependencies = [ + "api", + "axum 0.7.5", + "models", + "utoipa", + "utoipa-scalar", + "utoipa-swagger-ui", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -2872,9 +2884,9 @@ dependencies = [ [[package]] name = "sea-query" -version = "0.31.0-rc.8" +version = "0.31.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f62d5f33113186ad8b0f47c3522e40f9c31cdbc3056a8d7ad402e7498fc29789" +checksum = "2ad45af4aedefa04c296d623ffb7e409a24d4bfc0b5599b43f5726d88cc7ff2a" dependencies = [ "educe", "inherent", diff --git a/Cargo.toml b/Cargo.toml index a75de96..38d91f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,13 +11,14 @@ license = "MIT" readme = "README.md" [workspace] -members = ["api", "app", "models", "migration", "utils"] +members = ["api", "app", "doc", "models", "migration", "utils"] [workspace.dependencies] axum = { version = "0.7.5", default-features = false } tower = { version = "0.4.13", default-features = false } sea-orm = { version = "1.0.0-rc.7", default-features = false } serde = { version = "1", features = ["derive"] } +serde_json = { version = "1", default-features = false } tracing = "0.1.40" utoipa = { version = "5.0.0-alpha.0", default-features = false } validator = { version = "0.18", default-features = false } @@ -25,6 +26,7 @@ validator = { version = "0.18", default-features = false } [dependencies] api = { path = "api" } utils = { path = "utils" } +doc = { path = "doc" } sea-orm = { workspace = true } @@ -48,6 +50,7 @@ app = { path = "app" } models = { path = "models" } http-body-util = "0.1.2" +serde_json = { workspace = true } [features] default = [] diff --git a/README.md b/README.md index e3903ca..633cb29 100644 --- a/README.md +++ b/README.md @@ -17,23 +17,51 @@ You probably don't need [Rust on Rails](https://github.com/loco-rs/loco). ## Module hierarchy -- `api`: Axum logic - - `api::routers`: Axum endpoints - - `api::doc`: Utoipa doc declaration - - `api::error`: Error handling - - `api::extractor` and `api::validation`: Axum extractor and JSON validation +### API logic + +- `api::routers`: Axum endpoints +- `api::error`: Models and traits for error handling +- `api::extractor` Custom Axum extractors + - `api::extractor::json`: `Json` for bodies and responses + - `api::extractor::valid`: `Valid` for JSON body validation +- `api::validation`: JSON validation model based on `validator` - `api::models`: Non domain model API models - `api::models::response`: JSON error response -- `app`: DB/API-agnostic logic - - `app::services`: DB manipulation (CRUD) functions - - `app::config`: DB or API configuration - - `app::state`: APP state, e.g. DB connection -- `models`: DB/API-agnostic models - - `models::domains`: SeaORM domain models - - `models::params`: Serde input parameters for creating/updating domain models in DB - - `models::schemas`: Serde output schemas for combining different domain models - - `models::queries`: Serde queries for filtering domain models + +### OpenAPI documentation + +- `doc`: Utoipa doc declaration + +### API-agonistic DB logic + +Main concept: Web framework is replaceable. + +All modules here should not include any specific API web framework logic. + +- `app::services`: DB manipulation (CRUD) functions +- `app::config`: DB or API server configuration +- `app::state`: APP state, e.g. DB connection +- `app::error`: APP errors used by `api::error`. e.g. "User not found" + +### DB/API-agnostic domain models + +Main concept: Database (Sqlite/MySQL/PostgreSQL) is replaceable. + +Except `models::domains` and `migration`, all modules are ORM library agnostic. + +- `models::domains`: SeaORM domain models +- `models::params`: Serde input parameters for creating/updating domain models in DB +- `models::schemas`: Serde output schemas for combining different domain models +- `models::queries`: Serde queries for filtering domain models - `migration`: SeaORM migration files + +### Unit and integration tests + +- `tests::api`: API integration tests. Hierarchy is the same as `api::routers` +- `tests::app`: DB/ORM-related unit tests. Hierarchy is the same as `app::services` + +### Others + - `utils`: Utility functions - `main`: Tokio and Shuttle conditional entry point diff --git a/api/Cargo.toml b/api/Cargo.toml index 5279b46..56a19de 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -20,14 +20,7 @@ dotenvy = "0.15.7" sea-orm = { workspace = true } # doc -utoipa = { workspace = true, features = ["axum_extras"] } -utoipa-swagger-ui = { version = "7.1.1-alpha.0", features = [ - "axum", - "vendored", -], default-features = false } -utoipa-scalar = { version = "0.2.0-alpha.0", features = [ - "axum", -], default-features = false } +utoipa = { workspace = true } # local dependencies app = { path = "../app" } diff --git a/api/src/doc/mod.rs b/api/src/doc/mod.rs deleted file mode 100644 index 7c90665..0000000 --- a/api/src/doc/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -use utoipa::OpenApi; - -mod root; -mod user; - -#[derive(OpenApi)] -#[openapi( - nest( - (path = "/", api = root::RootApi), - (path = "/users", api = user::UserApi), - ), - tags( - (name = "root", description = "Root API"), - (name = "user", description = "User API") - - ) -)] -pub struct ApiDoc; diff --git a/api/src/init.rs b/api/src/init.rs index 67c45e8..cd723a7 100644 --- a/api/src/init.rs +++ b/api/src/init.rs @@ -1,19 +1,14 @@ use axum::Router; use sea_orm::{ConnectOptions, Database, DatabaseConnection}; -use utoipa::OpenApi; -use utoipa_scalar::{Scalar, Servable as ScalarServable}; -use utoipa_swagger_ui::SwaggerUi; use app::config::Config; use app::state::AppState; -use crate::doc::ApiDoc; use crate::routers::create_router; +// TODO: middleware, logging, authentication pub fn setup_router(conn: DatabaseConnection) -> Router { create_router(AppState { conn }) - .merge(SwaggerUi::new("/docs").url("/openapi.json", ApiDoc::openapi())) - .merge(Scalar::with_url("/scalar", ApiDoc::openapi())) } pub fn setup_config() -> Config { diff --git a/api/src/lib.rs b/api/src/lib.rs index ec292e4..ceba9b9 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1,9 +1,9 @@ -mod doc; mod error; mod extractor; mod init; -mod models; -mod routers; mod validation; +pub mod models; +pub mod routers; + pub use init::{setup_config, setup_db, setup_router}; diff --git a/api/src/routers/mod.rs b/api/src/routers/mod.rs index 1d42239..e121934 100644 --- a/api/src/routers/mod.rs +++ b/api/src/routers/mod.rs @@ -7,7 +7,6 @@ use app::state::AppState; use root::create_root_router; use user::create_user_router; -// TODO: middleware, testing, logging pub fn create_router(state: AppState) -> Router { Router::new() .nest("/users", create_user_router(state.clone())) diff --git a/doc/Cargo.toml b/doc/Cargo.toml new file mode 100644 index 0000000..943526f --- /dev/null +++ b/doc/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "doc" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +axum = { workspace = true } +utoipa = { workspace = true, features = ["axum_extras"] } +utoipa-swagger-ui = { version = "7.1.1-alpha.0", features = [ + "axum", + "vendored", +], default-features = false } +utoipa-scalar = { version = "0.2.0-alpha.0", features = [ + "axum", +], default-features = false } + +api = { path = "../api" } +models = { path = "../models" } diff --git a/doc/src/lib.rs b/doc/src/lib.rs new file mode 100644 index 0000000..1b9cbfe --- /dev/null +++ b/doc/src/lib.rs @@ -0,0 +1,32 @@ +use axum::Router; +use utoipa::OpenApi; +use utoipa_scalar::{Scalar, Servable as ScalarServable}; +use utoipa_swagger_ui::SwaggerUi; + +mod root; +mod user; + +#[derive(OpenApi)] +#[openapi( + nest( + (path = "/", api = root::RootApi), + (path = "/users", api = user::UserApi), + ), + tags( + (name = "root", description = "Root API"), + (name = "user", description = "User API") + + ) +)] +struct _ApiDoc; + +pub trait ApiDoc { + fn attach_doc(self) -> Self; +} + +impl ApiDoc for Router { + fn attach_doc(self) -> Self { + self.merge(SwaggerUi::new("/docs").url("/openapi.json", _ApiDoc::openapi())) + .merge(Scalar::with_url("/scalar", _ApiDoc::openapi())) + } +} diff --git a/api/src/doc/root.rs b/doc/src/root.rs similarity index 55% rename from api/src/doc/root.rs rename to doc/src/root.rs index 787faee..c75de83 100644 --- a/api/src/doc/root.rs +++ b/doc/src/root.rs @@ -1,7 +1,7 @@ use utoipa::OpenApi; -use crate::routers::root::*; +use api::routers::root::*; #[derive(OpenApi)] #[openapi(paths(root))] -pub struct RootApi; +pub(super) struct RootApi; diff --git a/api/src/doc/user.rs b/doc/src/user.rs similarity index 76% rename from api/src/doc/user.rs rename to doc/src/user.rs index 02c3010..bb9d9e4 100644 --- a/api/src/doc/user.rs +++ b/doc/src/user.rs @@ -3,8 +3,8 @@ use utoipa::OpenApi; use models::params::user::CreateUserParams; use models::schemas::user::{UserListSchema, UserSchema}; -use crate::models::{ApiErrorResponse, ParamsErrorResponse}; -use crate::routers::user::*; +use api::models::{ApiErrorResponse, ParamsErrorResponse}; +use api::routers::user::*; #[derive(OpenApi)] #[openapi( @@ -17,4 +17,4 @@ use crate::routers::user::*; ParamsErrorResponse, )) )] -pub struct UserApi; +pub(super) struct UserApi; diff --git a/models/Cargo.toml b/models/Cargo.toml index 559344b..0fa6b0d 100644 --- a/models/Cargo.toml +++ b/models/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] serde = { workspace = true } -serde_json = "1" +serde_json = { workspace = true } sea-orm = { workspace = true, features = [ "sqlx-postgres", "sqlx-sqlite", diff --git a/src/main.rs b/src/main.rs index 8fc0908..9847571 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +use api::{setup_db, setup_router}; +use doc::ApiDoc; use utils::migrate; #[cfg(not(feature = "shuttle"))] @@ -19,10 +21,10 @@ async fn main() { let config = api::setup_config(); create_dev_db(&config.db_url); - let conn = api::setup_db(&config.db_url).await; + let conn = setup_db(&config.db_url).await; migrate(&conn).await.expect("Migration failed!"); - let router = api::setup_router(conn); + let router = setup_router(conn).attach_doc(); let listener = tokio::net::TcpListener::bind(&config.get_server_url()) .await .unwrap(); @@ -36,9 +38,9 @@ async fn main() { async fn main(#[shuttle_shared_db::Postgres] db_url: String) -> shuttle_axum::ShuttleAxum { tracing::info!("Starting with shuttle"); - let conn = api::setup_db(&db_url).await; + let conn = setup_db(&db_url).await; migrate(&conn).await.expect("Migration failed!"); - let router = api::setup_router(conn); + let router = setup_router(conn).attach_doc(); Ok(router.into()) }