Skip to content

Commit

Permalink
Merge branch 'master' into add-support-additional-model-field-types-a…
Browse files Browse the repository at this point in the history
…nd-arrays
  • Loading branch information
kaplanelad authored Jan 14, 2025
2 parents fe89071 + eb7ea64 commit 14b717d
Show file tree
Hide file tree
Showing 17 changed files with 372 additions and 6 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/loco-gen-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ on:
push:
branches:
- master
paths:
- "loco-gen/**"
pull_request:
paths:
- "loco-gen/**"

env:
RUST_TOOLCHAIN: stable
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/loco-gen-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ on:
push:
branches:
- master
paths:
- "loco-gen/**"
pull_request:
paths:
- "loco-gen/**"

env:
RUST_TOOLCHAIN: stable
Expand Down
68 changes: 68 additions & 0 deletions .github/workflows/loco-rs-ci-sanity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# To optimize CI runtime:
# A simpler "sanity check" workflow is introduced.
# This workflow only runs if changes in the PR do NOT include
# the `loco-gen` or `loco-new` paths.
# (When changes are made to `loco-gen` or `loco-new`,
# we run comprehensive tests to validate every generator command
# and template option.)

# Purpose of the sanity check:
# It performs basic validation by comparing the local changes
# against the templates.
# If any breaking changes are detected in the templates,
# the sanity check will fail, signaling an issue.

name: "[loco_rs:sanity]"

on:
push:
branches:
- master
paths-ignore:
- "loco-gen/**"
- "loco-new/**"
pull_request:
paths-ignore:
- "loco-gen/**"
- "loco-new/**"

env:
RUST_TOOLCHAIN: stable
TOOLCHAIN_PROFILE: minimal

jobs:
sanity:
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Checkout the code
uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2

- name: Install seaorm cli
run: cargo install sea-orm-cli

- run: cargo install --path loco-new

- run: |
loco new -n myappdb --db sqlite --bg async --assets serverside -a
cd myappdb
cargo check
env:
LOCO_DEV_MODE_PATH: ${{ github.workspace }}
- run: |
loco new -n myappnodb --db none --bg none --assets none -a
cd myappdb
cargo check
env:
LOCO_DEV_MODE_PATH: ${{ github.workspace }}
22 changes: 21 additions & 1 deletion .github/workflows/loco-rs-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,30 @@ jobs:
command: clippy
args: --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms

test:
check:
needs: [style]
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Checkout the code
uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@v2
with:
tool: cargo-hack
- run: cargo hack check --each-feature

test:
needs: [check, style]
runs-on: ubuntu-latest

permissions:
contents: read

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
* fix: bump shuttle to 0.51.0. [https://github.com/loco-rs/loco/pull/1169](https://github.com/loco-rs/loco/pull/1169)
* Return 422 status code for JSON rejection errors. [https://github.com/loco-rs/loco/pull/1173](https://github.com/loco-rs/loco/pull/1173)
* Address clippy warnings for Rust stable 1.84. [https://github.com/loco-rs/loco/pull/1168](https://github.com/loco-rs/loco/pull/1168)
* Bump shuttle to 0.51.0. [https://github.com/loco-rs/loco/pull/1169](https://github.com/loco-rs/loco/pull/1169)
* return 422 status code for JSON rejection errors. [https://github.com/loco-rs/loco/pull/1173](https://github.com/loco-rs/loco/pull/1173)
* return json validation details response. [https://github.com/loco-rs/loco/pull/1174](https://github.com/loco-rs/loco/pull/1174)
* fix example command after generating schedule. [https://github.com/loco-rs/loco/pull/1176](https://github.com/loco-rs/loco/pull/1176)
* fixed independent features. [https://github.com/loco-rs/loco/pull/1177](https://github.com/loco-rs/loco/pull/1177)


## v0.14
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ lettre = { version = "0.11.4", default-features = false, features = [
include_dir = "0.7.3"
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] }
tracing-appender = "0.2.3"

duct = { version = "0.13.6" }
Expand Down Expand Up @@ -146,6 +146,7 @@ english-to-cron = { version = "0.1.2" }
# bg_sqlt: sqlite workers
# bg_pg: postgres workers
sqlx = { version = "0.8.2", default-features = false, features = [
"json",
"postgres",
"chrono",
"sqlite",
Expand Down
64 changes: 64 additions & 0 deletions docs-site/content/docs/the-app/controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,70 @@ impl Hooks for App {
}
```

# Request Validation
`JsonValidate` extractor simplifies input [validation](https://github.com/Keats/validator) by integrating with the validator crate. Here's an example of how to validate incoming request data:

### Define Your Validation Rules
```rust
use axum::debug_handler;
use loco_rs::prelude::*;
use serde::Deserialize;
use validator::Validate;

#[derive(Debug, Deserialize, Validate)]
pub struct DataParams {
#[validate(length(min = 5, message = "custom message"))]
pub name: String,
#[validate(email)]
pub email: String,
}
```
### Create a Handler with Validation
```rust
use axum::debug_handler;
use loco_rs::prelude::*;

#[debug_handler]
pub async fn index(
State(_ctx): State<AppContext>,
JsonValidate(params): JsonValidate<DataParams>,
) -> Result<Response> {
format::empty()
}
```
Using the `JsonValidate` extractor, Loco automatically performs validation on the DataParams struct:
* If validation passes, the handler continues execution with params.
* If validation fails, a 400 Bad Request response is returned.

### Returning Validation Errors as JSON
If you'd like to return validation errors in a structured JSON format, use `JsonValidateWithMessage` instead of `JsonValidate`. The response format will look like this:

```json
{
"errors": {
"email": [
{
"code": "email",
"message": null,
"params": {
"value": "ad"
}
}
],
"name": [
{
"code": "length",
"message": "custom message",
"params": {
"min": 5,
"value": "d"
}
}
]
}
}
```

# 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.
Expand Down
2 changes: 1 addition & 1 deletion loco-gen/src/templates/scheduler/scheduler.t
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
to: "config/scheduler.yaml"
skip_exists: true
message: "A Scheduler job configuration was added successfully. Run with `cargo loco run scheduler --list`."
message: "A Scheduler job configuration was added successfully. Run with `cargo loco scheduler --list`."

---
output: stdout
Expand Down
2 changes: 1 addition & 1 deletion loco-gen/tests/templates/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fn can_generate() {

assert_eq!(
collect_messages(&gen_result),
r"* A Scheduler job configuration was added successfully. Run with `cargo loco run scheduler --list`.
r"* A Scheduler job configuration was added successfully. Run with `cargo loco scheduler --list`.
"
);

Expand Down
1 change: 1 addition & 0 deletions src/controller/extractor/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod validate;
78 changes: 78 additions & 0 deletions src/controller/extractor/validate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use crate::Error;
use axum::extract::{Form, FromRequest, Json, Request};
use serde::de::DeserializeOwned;
use validator::Validate;

#[derive(Debug, Clone, Copy, Default)]
pub struct JsonValidateWithMessage<T>(pub T);

impl<T, S> FromRequest<S> for JsonValidateWithMessage<T>
where
T: DeserializeOwned + Validate,
S: Send + Sync,
{
type Rejection = Error;

async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let Json(value) = Json::<T>::from_request(req, state).await?;
value.validate()?;
Ok(Self(value))
}
}

#[derive(Debug, Clone, Copy, Default)]
pub struct FormValidateWithMessage<T>(pub T);

impl<T, S> FromRequest<S> for FormValidateWithMessage<T>
where
T: DeserializeOwned + Validate,
S: Send + Sync,
{
type Rejection = Error;

async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let Form(value) = Form::<T>::from_request(req, state).await?;
value.validate()?;
Ok(Self(value))
}
}

#[derive(Debug, Clone, Copy, Default)]
pub struct JsonValidate<T>(pub T);

impl<T, S> FromRequest<S> for JsonValidate<T>
where
T: DeserializeOwned + Validate,
S: Send + Sync,
{
type Rejection = Error;

async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let Json(value) = Json::<T>::from_request(req, state).await?;
value.validate().map_err(|err| {
tracing::debug!(err = ?err, "request validation error occurred");
Error::BadRequest(String::new())
})?;
Ok(Self(value))
}
}

#[derive(Debug, Clone, Copy, Default)]
pub struct FormValidate<T>(pub T);

impl<T, S> FromRequest<S> for FormValidate<T>
where
T: DeserializeOwned + Validate,
S: Send + Sync,
{
type Rejection = Error;

async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let Form(value) = Form::<T>::from_request(req, state).await?;
value.validate().map_err(|err| {
tracing::debug!(err = ?err, "request validation error occurred");
Error::BadRequest(String::new())
})?;
Ok(Self(value))
}
}
28 changes: 26 additions & 2 deletions src/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ use crate::{errors::Error, Result};
mod app_routes;
mod backtrace;
mod describe;
pub mod extractor;
pub mod format;
#[cfg(feature = "with-db")]
mod health;
Expand Down Expand Up @@ -138,15 +139,19 @@ pub struct ErrorDetail {
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub errors: Option<serde_json::Value>,
}

impl ErrorDetail {
/// Create a new `ErrorDetail` with the specified error and description.
#[must_use]
pub fn new<T: Into<String>>(error: T, description: T) -> Self {
pub fn new<T: Into<String> + AsRef<str>>(error: T, description: T) -> Self {
let description = (!description.as_ref().is_empty()).then(|| description.into());
Self {
error: Some(error.into()),
description: Some(description.into()),
description,
errors: None,
}
}

Expand All @@ -156,6 +161,7 @@ impl ErrorDetail {
Self {
error: Some(error.into()),
description: None,
errors: None,
}
}
}
Expand Down Expand Up @@ -227,6 +233,24 @@ impl IntoResponse for Error {
(err.status(), ErrorDetail::with_reason("Bad Request"))
}

Self::ValidationError(ref errors) => serde_json::to_value(errors).map_or_else(
|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
ErrorDetail::new("internal_server_error", "Internal Server Error"),
)
},
|errors| {
(
StatusCode::BAD_REQUEST,
ErrorDetail {
error: None,
description: None,
errors: Some(errors),
},
)
},
),
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
ErrorDetail::new("internal_server_error", "Internal Server Error"),
Expand Down
6 changes: 6 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ pub enum Error {

#[error(transparent)]
Any(#[from] Box<dyn std::error::Error + Send + Sync>),

#[error(transparent)]
ValidationError(#[from] validator::ValidationErrors),

#[error(transparent)]
AxumFormRejection(#[from] axum::extract::rejection::FormRejection),
}

impl Error {
Expand Down
Loading

0 comments on commit 14b717d

Please sign in to comment.