spring-axum 0.1.3

Spring-like application framework for Axum with macro-based auto discovery, validation, transactions, caching, events, and SQL/Mapper integrations.
Documentation
#![cfg(feature = "validator")]

use axum::{Router, routing::{post, get}, http::{Request, StatusCode}};
use axum::body::to_bytes;
use axum::body::Body;
use tower::ServiceExt; // for `oneshot`
use serde::{Deserialize, Serialize};
use validator::Validate;

#[derive(Debug, Deserialize, Serialize, Validate)]
struct User {
    #[validate(length(min = 1))]
    name: String,
    #[validate(range(min = 18))]
    age: u8,
}

async fn create_user(spring_axum::ValidatedJson(user): spring_axum::ValidatedJson<User>) -> String {
    format!("ok:{}:{}", user.name, user.age)
}

#[derive(Debug, Deserialize, Validate)]
struct SearchParams {
    #[validate(length(min = 2))]
    q: String,
}

async fn search(spring_axum::ValidatedQuery(params): spring_axum::ValidatedQuery<SearchParams>) -> String {
    format!("found:{}", params.q)
}

#[tokio::test]
async fn json_validation_failure_and_success() {
    let app = Router::new()
        .route("/users", post(create_user));

    // invalid: empty name, age < 18
    let req = Request::builder()
        .method("POST")
        .uri("/users")
        .header("content-type", "application/json")
        .body(Body::from(r#"{"name":"","age":17}"#))
        .unwrap();

    let res = app.clone().oneshot(req).await.unwrap();
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);
    let body = to_bytes(res.into_body(), usize::MAX).await.unwrap();
    let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
    assert!(v.get("errors").is_some(), "expected structured validation errors");
    let errs = v.get("errors").unwrap();
    assert!(errs.get("name").is_some());
    assert!(errs.get("age").is_some());

    // valid
    let req_ok = Request::builder()
        .method("POST")
        .uri("/users")
        .header("content-type", "application/json")
        .body(Body::from(r#"{"name":"alice","age":18}"#))
        .unwrap();
    let res_ok = app.clone().oneshot(req_ok).await.unwrap();
    assert_eq!(res_ok.status(), StatusCode::OK);
    let body_ok = to_bytes(res_ok.into_body(), usize::MAX).await.unwrap();
    assert_eq!(std::str::from_utf8(&body_ok).unwrap(), "ok:alice:18");
}

#[tokio::test]
async fn query_validation_failure_and_success() {
    let app = Router::new()
        .route("/search", get(search));

    // invalid: q too short
    let req = Request::builder()
        .method("GET")
        .uri("/search?q=a")
        .body(Body::empty())
        .unwrap();
    let res = app.clone().oneshot(req).await.unwrap();
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);
    let body = to_bytes(res.into_body(), usize::MAX).await.unwrap();
    let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
    assert!(v.get("errors").is_some());
    assert!(v.get("errors").unwrap().get("q").is_some());

    // valid
    let req_ok = Request::builder()
        .method("GET")
        .uri("/search?q=rust")
        .body(Body::empty())
        .unwrap();
    let res_ok = app.clone().oneshot(req_ok).await.unwrap();
    assert_eq!(res_ok.status(), StatusCode::OK);
    let body_ok = to_bytes(res_ok.into_body(), usize::MAX).await.unwrap();
    assert_eq!(std::str::from_utf8(&body_ok).unwrap(), "found:rust");
}