rjapi 0.0.1

A framework-agnostic JSON:API 1.1 implementation for Rust
Documentation
//! Example of using the JSON:API library with Axum

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;

// In-memory storage for demonstration purposes
#[derive(Clone)]
struct AppState {
    posts: Arc<RwLock<Vec<Post>>>,
}

// The `Post` model, representing our application's data
#[derive(Clone, Serialize, Deserialize)]
struct Post {
    id: String,
    title: String,
    content: String,
}

// `PostAttributes` for creating a new post
#[derive(Deserialize)]
struct PostAttributes {
    title: String,
    content: String,
}

// Implementation of `JsonApiResource` for the `Post` model
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,
        })
    }
}

// Custom error type for the application
enum AppError {
    JsonApiError(rjapi::JsonApiErrorResponse),
    // Other application-specific errors could be added here
}

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()
            }
        }
    }
}

// Axum middleware to enforce JSON:API content type headers
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
}

// Handler to create a new post
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)))
}

// Handler to get all posts
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() {
    // Initialize state
    let state = AppState {
        posts: Arc::new(RwLock::new(Vec::new())),
    };

    // Build router
    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);

    // Start server
    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();
}