Skip to content

Commit

Permalink
Add parameter validation (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
kigawas authored Jul 19, 2024
1 parent edb20cf commit a7c8345
Show file tree
Hide file tree
Showing 35 changed files with 327 additions and 99 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ jobs:

- uses: Swatinem/rust-cache@v2

- name: Check and run lint
run: cargo check && cargo fmt && cargo clippy

# cargo build before check to speed up check
- run: cargo build

- run: cargo check && cargo fmt && cargo clippy

- run: cargo test

- name: Run server
Expand Down
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
"cSpell.words": [
"codegen",
"dotenv",
"dotenvy",
"oneshot",
"openapi",
"Serde",
"Utoipa"
],
"cSpell.ignorePaths": [
"**/.cargo",
"**/.rustup",
".vscode",
"LICENSE"
]
Expand Down
92 changes: 84 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ sea-orm = { version = "1.0.0-rc.7", default-features = false }
serde = { version = "1", features = ["derive"] }
tracing = "0.1.40"
utoipa = { version = "5.0.0-alpha.0", default-features = false }
validator = { version = "0.18", default-features = false }

[dependencies]
api = { path = "api" }
utils = { path = "utils" }

sea-orm = { workspace = true }

# logging
tracing = { workspace = true }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@ You probably don't need [Rust on Rails](https://github.com/loco-rs/loco).
- Completely separated API routers and DB-related logic (named "services")
- Completely separated input parameters, queries and output schemas
- OpenAPI documentation ([Swagger UI](https://clean-axum.shuttleapp.rs/docs) and [Scalar](https://clean-axum.shuttleapp.rs/scalar)) powered by [Utoipa](https://github.com/juhaku/utoipa)
- Optional [Shuttle](https://www.shuttle.rs/) runtime
- Error handling with [Anyhow](https://github.com/dtolnay/anyhow)
- Custom parameter validation with [validator](https://github.com/Keats/validator)
- Optional [Shuttle](https://www.shuttle.rs/) runtime

## 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::models`: Non domain model API models
- `api::models::response`: JSON error response
- `api::models::request`: Custom Axum Json extractor for error handling
- `app`: DB/API-agnostic logic
- `app::services`: DB manipulation (CRUD) functions
- `app::config`: DB or API configuration
Expand Down
4 changes: 4 additions & 0 deletions api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ axum = { workspace = true, features = ["macros", "query"] }
serde = { workspace = true }
tower = { workspace = true }
tracing = { workspace = true }
validator = { workspace = true, features = ["derive"] }

tower-http = { version = "0.5.2", features = ["fs"] }
tower-cookies = "0.10.0"
anyhow = "1.0.86"
dotenvy = "0.15.7"

# db
sea-orm = { workspace = true }

# doc
utoipa = { workspace = true, features = ["axum_extras"] }
utoipa-swagger-ui = { version = "7.1.1-alpha.0", features = [
Expand Down
10 changes: 8 additions & 2 deletions api/src/doc/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ use utoipa::OpenApi;
use models::params::user::CreateUserParams;
use models::schemas::user::{UserListSchema, UserSchema};

use crate::models::ErrorResponse;
use crate::models::{ApiErrorResponse, ParamsErrorResponse};
use crate::routers::user::*;

#[derive(OpenApi)]
#[openapi(
paths(users_get, users_id_get, users_post),
components(schemas(CreateUserParams, UserListSchema, UserSchema, ErrorResponse))
components(schemas(
CreateUserParams,
UserListSchema,
UserSchema,
ApiErrorResponse,
ParamsErrorResponse,
))
)]
pub struct UserApi;
32 changes: 32 additions & 0 deletions api/src/error/adapter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use axum::{extract::rejection::JsonRejection, http::StatusCode};
use sea_orm::DbErr;

use app::error::UserError;

use super::traits::HTTPError;

impl HTTPError for JsonRejection {
fn to_status_code(&self) -> StatusCode {
match self {
JsonRejection::JsonSyntaxError(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::BAD_REQUEST,
}
}
}

impl HTTPError for DbErr {
fn to_status_code(&self) -> StatusCode {
match self {
DbErr::ConnectionAcquire(_) => StatusCode::INTERNAL_SERVER_ERROR,
_ => StatusCode::INTERNAL_SERVER_ERROR, // TODO:: more granularity
}
}
}

impl HTTPError for UserError {
fn to_status_code(&self) -> StatusCode {
match self {
UserError::NotFound => StatusCode::NOT_FOUND,
}
}
}
28 changes: 7 additions & 21 deletions api/src/error/core.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,10 @@
use axum::{extract::rejection::JsonRejection, http::StatusCode};
use models::orm::DbErr;
pub struct ApiError(pub(super) anyhow::Error);

pub trait HTTPError {
fn to_status_code(&self) -> StatusCode;
}

impl HTTPError for JsonRejection {
fn to_status_code(&self) -> StatusCode {
match self {
JsonRejection::JsonSyntaxError(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::BAD_REQUEST,
}
}
}

impl HTTPError for DbErr {
fn to_status_code(&self) -> StatusCode {
match self {
DbErr::ConnectionAcquire(_) => StatusCode::INTERNAL_SERVER_ERROR,
_ => StatusCode::INTERNAL_SERVER_ERROR, // TODO:: more granularity
}
impl<E> From<E> for ApiError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}
Loading

1 comment on commit a7c8345

@eliasdiek
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kigawas hey ,I'm looking for someone with your stack to work on our platform ,either as a freelancer or in a full time role . are you up for it ? contact me on telegram @eliasdiek or via email at [email protected]

Please sign in to comment.