use axum::routing::{get, post};
use axum::{Json, Router, http::StatusCode};
use axum_helpers::errors::AppError;
use axum_helpers::extractors::{UuidPath, ValidatedJson};
use axum_helpers::server::{create_router, health_router};
use core_config::app_info;
use serde::{Deserialize, Serialize};
use utoipa::OpenApi;
use uuid::Uuid;
use validator::Validate;
#[derive(OpenApi)]
#[openapi(paths())]
struct ApiDoc;
#[derive(Deserialize, Validate)]
struct CreateMessage {
#[validate(length(min = 1, max = 80))]
text: String,
}
#[derive(Serialize)]
struct Message {
id: Uuid,
text: String,
}
async fn create_message(
ValidatedJson(body): ValidatedJson<CreateMessage>,
) -> (StatusCode, Json<Message>) {
(
StatusCode::CREATED,
Json(Message {
id: Uuid::new_v4(),
text: body.text,
}),
)
}
async fn get_message(UuidPath(id): UuidPath) -> Result<Json<Message>, AppError> {
Err(AppError::NotFound(format!("message {id} not found")))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api = Router::new()
.route("/messages", post(create_message))
.route("/messages/{id}", get(get_message));
let router = create_router::<ApiDoc>(api)
.await?
.merge(health_router(app_info!()));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
println!("step:listening addr={addr}");
let server = tokio::spawn(async move { axum::serve(listener, router).await });
let base = format!("http://{addr}");
let client = reqwest::Client::new();
let resp = client.get(format!("{base}/health")).send().await?;
let status = resp.status().as_u16();
let body: serde_json::Value = resp.json().await?;
println!(
"step:health status={status} name={}",
body["name"].as_str().unwrap_or("?")
);
let resp = client
.post(format!("{base}/api/messages"))
.json(&serde_json::json!({ "text": "hello" }))
.send()
.await?;
let status = resp.status().as_u16();
let body: serde_json::Value = resp.json().await?;
println!(
"step:created status={status} id={}",
body["id"].as_str().unwrap_or("?")
);
if status != 201 {
return Err(format!("expected 201 from valid create, got {status}").into());
}
let resp = client
.post(format!("{base}/api/messages"))
.json(&serde_json::json!({ "text": "" }))
.send()
.await?;
let status = resp.status().as_u16();
let body: serde_json::Value = resp.json().await?;
println!(
"step:rejected status={status} error={}",
body["error"].as_str().unwrap_or("?")
);
let resp = client
.get(format!("{base}/api/messages/not-a-uuid"))
.send()
.await?;
let status = resp.status().as_u16();
let body: serde_json::Value = resp.json().await?;
println!(
"step:bad_uuid status={status} error={}",
body["error"].as_str().unwrap_or("?")
);
let resp = client
.get(format!("{base}/api/messages/{}", Uuid::new_v4()))
.send()
.await?;
let status = resp.status().as_u16();
let body: serde_json::Value = resp.json().await?;
println!(
"step:not_found status={status} error={}",
body["error"].as_str().unwrap_or("?")
);
let resp = client.get(format!("{base}/no-such-route")).send().await?;
let status = resp.status().as_u16();
let body: serde_json::Value = resp.json().await?;
println!(
"step:fallback status={status} error={}",
body["error"].as_str().unwrap_or("?")
);
server.abort();
println!("HTTP walkthrough complete");
Ok(())
}