Skip to main content

ling_http/
mvc.rs

1use axum::extract::{Path, State};
2use axum::routing::get;
3use axum::{Json, Router};
4use serde::de::DeserializeOwned;
5use serde::Serialize;
6
7use crate::error::Result;
8
9/// A REST-ish resource controller: implement this once for a type and get
10/// the five conventional CRUD routes (`GET /x`, `GET /x/:id`, `POST /x`,
11/// `PUT /x/:id`, `DELETE /x/:id`) wired up for free via [`resource`].
12///
13/// `State` is the app state (typically holding a [`crate::db::Db`]); `Id` is
14/// the identifier as it appears in the URL; `Item` is the JSON shape
15/// returned to clients; `Create`/`Update` are the accepted JSON bodies.
16#[async_trait::async_trait]
17pub trait Resource: Send + Sync + 'static {
18    type State: Clone + Send + Sync + 'static;
19    type Id: DeserializeOwned + Send + Sync + 'static;
20    type Item: Serialize + Send + Sync + 'static;
21    type Create: DeserializeOwned + Send + Sync + 'static;
22    type Update: DeserializeOwned + Send + Sync + 'static;
23
24    async fn index(state: &Self::State) -> Result<Vec<Self::Item>>;
25    async fn show(state: &Self::State, id: Self::Id) -> Result<Self::Item>;
26    async fn create(state: &Self::State, body: Self::Create) -> Result<Self::Item>;
27    async fn update(state: &Self::State, id: Self::Id, body: Self::Update) -> Result<Self::Item>;
28    async fn destroy(state: &Self::State, id: Self::Id) -> Result<()>;
29}
30
31/// Mounts a [`Resource`] at `path`, wiring the five CRUD routes onto it.
32///
33/// ```ignore
34/// let router = Router::new().merge(resource::<Notes>("/notes"));
35/// ```
36pub fn resource<R: Resource>(path: &str) -> Router<R::State> {
37    let item_path = format!("{path}/{{id}}");
38    Router::new()
39        .route(path, get(index::<R>).post(create::<R>))
40        .route(&item_path, get(show::<R>).put(update::<R>).delete(destroy::<R>))
41}
42
43async fn index<R: Resource>(State(state): State<R::State>) -> Result<Json<Vec<R::Item>>> {
44    Ok(Json(R::index(&state).await?))
45}
46
47async fn show<R: Resource>(
48    State(state): State<R::State>,
49    Path(id): Path<R::Id>,
50) -> Result<Json<R::Item>> {
51    Ok(Json(R::show(&state, id).await?))
52}
53
54async fn create<R: Resource>(
55    State(state): State<R::State>,
56    Json(body): Json<R::Create>,
57) -> Result<Json<R::Item>> {
58    Ok(Json(R::create(&state, body).await?))
59}
60
61async fn update<R: Resource>(
62    State(state): State<R::State>,
63    Path(id): Path<R::Id>,
64    Json(body): Json<R::Update>,
65) -> Result<Json<R::Item>> {
66    Ok(Json(R::update(&state, id, body).await?))
67}
68
69async fn destroy<R: Resource>(
70    State(state): State<R::State>,
71    Path(id): Path<R::Id>,
72) -> Result<axum::http::StatusCode> {
73    R::destroy(&state, id).await?;
74    Ok(axum::http::StatusCode::NO_CONTENT)
75}