rjapi 0.0.1

A framework-agnostic JSON:API 1.1 implementation for Rust
Documentation
//! JSON:API Request Handling
//!
//! This module provides data structures and utilities for handling JSON:API requests.
//! It includes structures for parsing JSON:API request documents and query parameters,
//! as well as helper functions for validation and extraction.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Generic JSON:API request data structure
///
/// This struct represents the `data` member of a JSON:API request document.
/// It contains the resource type and attributes for the resource being created or updated.
///
/// # Type Parameters
///
/// * `T` - The type representing the resource attributes
#[derive(Deserialize, Debug)]
pub struct JsonApiRequestData<T> {
    /// The resource type, which must match the expected type for the endpoint
    #[serde(rename = "type")]
    pub resource_type: String,
    /// The resource attributes
    pub attributes: T,
}

/// JSON:API request wrapper
///
/// This struct represents a complete JSON:API request document.
/// It contains the `data` member which holds the resource information.
///
/// # Type Parameters
///
/// * `T` - The type representing the resource attributes
#[derive(Deserialize, Debug)]
pub struct JsonApiRequest<T> {
    /// The data member containing the resource information
    pub data: JsonApiRequestData<T>,
}

/// JSON:API Query Parameters for 1.1 compliance
///
/// This struct represents the query parameters supported by JSON:API 1.1.
/// It includes fields for inclusion, sparse fieldsets, sorting, pagination, and filtering.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonApiQueryParams {
    /// Include related resources
    ///
    /// A comma-separated list of relationship paths to include in the response.
    /// Example: `include=author,comments.author`
    #[serde(skip_serializing_if = "Option::is_none")]
    pub include: Option<String>,

    /// Sparse fieldsets
    ///
    /// A map of resource types to comma-separated lists of fields to include.
    /// Example: `fields[articles]=title,body&fields[people]=name`
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fields: Option<HashMap<String, String>>,

    /// Sorting
    ///
    /// A comma-separated list of fields to sort by. Prefix with `-` for descending order.
    /// Example: `sort=-created,title`
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sort: Option<String>,

    /// Pagination
    ///
    /// A map of pagination parameters.
    /// Example: `page[number]=1&page[size]=10` or `page[offset]=0&page[limit]=10`
    #[serde(skip_serializing_if = "Option::is_none")]
    pub page: Option<HashMap<String, String>>,

    /// Filtering
    ///
    /// A map of filter parameters.
    /// Example: `filter[title]=JSON:API`
    #[serde(skip_serializing_if = "Option::is_none")]
    pub filter: Option<HashMap<String, String>>,
}

/// A trait for defining a JSON:API resource type.
///
/// This trait is implemented by application-specific models to specify their
/// JSON:API resource type. This is used by the `JsonApi` extractor to validate
/// the incoming request's `type` member.
///
/// # Example
///
/// ```rust
/// use rjapi::JsonApiResource;
/// use serde::Deserialize;
///
/// #[derive(Deserialize, serde::Serialize)]
/// pub struct Post {
///     pub title: String,
///     pub content: String,
/// }
///
/// impl JsonApiResource for Post {
///     const RESOURCE_TYPE: &'static str = "posts";
///     fn id(&self) -> String { "1".to_string() }
///     fn attributes(&self) -> serde_json::Value { serde_json::to_value(self).unwrap() }
/// }
/// ```
pub trait JsonApiResource {
    /// The JSON:API resource type.
    const RESOURCE_TYPE: &'static str;
}

/// A `FromRequest` extractor for JSON:API requests.
///
/// This extractor deserializes the request body into a `JsonApiRequest<T>`
/// and then validates the resource `type`. If the `type` does not match
/// `T::RESOURCE_TYPE`, a `400 Bad Request` response is returned.
///
/// This simplifies request handling in frameworks like Axum, where you can
/// use `JsonApi<T>` directly as a handler parameter.
#[derive(Debug, Clone)]
pub struct JsonApi<T>(pub T);

// In a real application, you would use a macro or a library to implement
// `FromRequest` for different web frameworks. For simplicity, we'll
// keep this as a conceptual example.

// Note: To make this code compile, you'd need to add `async-trait` and
// a web framework like `axum` as a dependency. The following is a
// conceptual implementation for `axum`.

/*
use async_trait::async_trait;
use axum::{
    extract::{FromRequest, Request},
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};

#[async_trait]
impl<S, T> FromRequest<S> for JsonApi<T>
where
    S: Send + Sync,
    T: JsonApiResource + for<'de> Deserialize<'de>,
{
    type Rejection = (StatusCode, String);

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        let Json(request): Json<JsonApiRequest<T>> = Json::from_request(req, state)
            .await
            .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;

        if request.data.resource_type != T::RESOURCE_TYPE {
            return Err((
                StatusCode::BAD_REQUEST,
                format!("Expected resource type '{}'", T::RESOURCE_TYPE),
            ));
        }

        Ok(JsonApi(request.data.attributes))
    }
}
*/