use axum::{
Json, Router,
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
};
use rjapi::request::JsonApiRequest;
use rjapi::{JsonApiResource, JsonApiResponse, Resource};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
struct AppState {
posts: Arc<RwLock<Vec<Post>>>,
}
#[derive(Clone, Serialize, Deserialize)]
struct Post {
id: String,
title: String,
content: String,
}
#[derive(Deserialize)]
struct PostAttributes {
title: String,
content: String,
}
impl JsonApiResource for Post {
const RESOURCE_TYPE: &'static str = "posts";
fn id(&self) -> String {
self.id.clone()
}
fn attributes(&self) -> serde_json::Value {
json!({
"title": self.title,
"content": self.content,
})
}
}
enum AppError {
JsonApiError(rjapi::JsonApiErrorResponse),
}
impl From<rjapi::JsonApiErrorResponse> for AppError {
fn from(err: rjapi::JsonApiErrorResponse) -> Self {
AppError::JsonApiError(err)
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
match self {
AppError::JsonApiError(err) => {
let doc = err.to_document::<()>();
(err.status, Json(doc)).into_response()
}
}
}
}
async fn json_api_middleware(
req: axum::http::Request<axum::body::Body>,
next: axum::middleware::Next,
) -> Response {
if let Err(err) = rjapi::middleware::validate_content_type(&req) {
return AppError::from(err).into_response();
}
let mut res = next.run(req).await;
rjapi::middleware::set_content_type(&mut res);
res
}
async fn create_post(
State(state): State<AppState>,
Json(request): Json<JsonApiRequest<PostAttributes>>,
) -> Result<impl IntoResponse, AppError> {
if request.data.resource_type != "posts" {
return Err(AppError::from(rjapi::bad_request_error(
"Invalid Resource Type",
"Expected 'posts'",
)));
}
let attributes = request.data.attributes;
let post = Post {
id: uuid::Uuid::new_v4().to_string(),
title: attributes.title,
content: attributes.content,
};
let mut posts = state.posts.write().await;
posts.push(post.clone());
let resource = Resource {
resource_type: Post::RESOURCE_TYPE.to_string(),
id: post.id(),
attributes: Some(serde_json::to_value(&post).unwrap()),
relationships: None,
links: None,
meta: None,
};
let response = JsonApiResponse::new(resource).build();
Ok((StatusCode::CREATED, Json(response)))
}
async fn get_posts(State(state): State<AppState>) -> Result<impl IntoResponse, AppError> {
let posts = state.posts.read().await;
let resources: Vec<Resource<_>> = posts
.iter()
.map(|post| Resource {
resource_type: Post::RESOURCE_TYPE.to_string(),
id: post.id(),
attributes: Some(serde_json::to_value(post).unwrap()),
relationships: None,
links: None,
meta: None,
})
.collect();
let response = JsonApiResponse::new_multiple(resources).build();
Ok((StatusCode::OK, Json(response)))
}
#[tokio::main]
async fn main() {
let state = AppState {
posts: Arc::new(RwLock::new(Vec::new())),
};
let app = Router::new()
.route("/posts", axum::routing::get(get_posts).post(create_post))
.layer(axum::middleware::from_fn(json_api_middleware))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://0.0.0.0:3000");
axum::serve(listener, app).await.unwrap();
}