use axum::{
body::{Body, to_bytes},
http::Request,
};
use garde::Validate;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tower::ServiceExt;
use xecute::prelude::*;
#[derive(Deserialize, Validate, JsonSchema)]
struct Payload {
#[garde(length(chars, min = 3))]
name: String,
}
#[derive(Serialize, JsonSchema)]
struct Reply {
name: String,
}
#[controller("/v1/test")]
#[derive(Injectable)]
struct TestController {}
#[routes]
impl TestController {
#[post("/create")]
#[api(summary = "Create", tag = "Tests", operation_id = "createTest")]
#[body(json = Payload)]
#[resp(200, "Created", json = Reply)]
#[resp(422, "Invalid payload")]
async fn create(&self, #[body] request: Payload) -> Result<Json<Reply>, HttpError> {
Ok(Json(Reply { name: request.name }))
}
#[get("/search")]
#[resp(200, "Query result", json = Reply)]
async fn search(&self, #[query] request: Payload) -> Json<Reply> {
Json(Reply { name: request.name })
}
}
#[tokio::test]
async fn receiver_handlers_validate_before_running() {
let controller = xecute::Container::builder()
.injectable::<TestController>()
.build()
.unwrap()
.resolve()
.unwrap();
let app = TestController::router(controller);
for (payload, expected) in [
(r#"{"name":"ab"}"#, 422),
(r#"{"name":"Valid"}"#, 200),
("invalid", 400),
] {
let request = Request::builder()
.method("POST")
.uri("/v1/test/create")
.header("content-type", "application/json")
.body(Body::from(payload))
.unwrap();
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status().as_u16(), expected);
if expected == 200 {
assert_eq!(
serde_json::from_slice::<serde_json::Value>(
&to_bytes(response.into_body(), 4096).await.unwrap()
)
.unwrap()["name"],
"Valid"
);
}
}
for (path, expected) in [
("/v1/test/search?name=ab", 422),
("/v1/test/search?name=Valid", 200),
] {
assert_eq!(
app.clone()
.oneshot(Request::builder().uri(path).body(Body::empty()).unwrap())
.await
.unwrap()
.status()
.as_u16(),
expected
);
}
}
#[test]
fn openapi_includes_routes_payloads_and_response_contracts() {
let spec = xecute::openapi_document("Test", "1", TestController::api_operations()).unwrap();
assert_eq!(spec["openapi"], "3.1.0");
let create = &spec["paths"]["/v1/test/create"]["post"];
assert_eq!(create["operationId"], "createTest");
assert_eq!(
create["requestBody"]["content"]["application/json"]["schema"]["properties"]["name"]["type"],
"string"
);
assert!(
create["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["name"]
.is_object()
);
assert!(
xecute::openapi_document(
"Test",
"1",
TestController::api_operations()
.into_iter()
.chain(TestController::api_operations())
.collect()
)
.is_err()
);
}
#[derive(Injectable)]
struct LegacyController {}
#[xecute::controller("/legacy")]
impl LegacyController {
#[get("")]
async fn list() -> Json<Vec<String>> {
Json(Vec::new())
}
}
#[tokio::test]
async fn original_impl_controller_syntax_remains_supported() {
let app = LegacyController::router(Arc::new(LegacyController {}));
assert_eq!(
app.oneshot(
Request::builder()
.uri("/legacy")
.body(Body::empty())
.unwrap()
)
.await
.unwrap()
.status(),
HttpStatus::OK
);
}