rjapi 0.0.1

A framework-agnostic JSON:API 1.1 implementation for Rust
Documentation
//! JSON:API Response Handling
//!
//! This module provides data structures and utilities for creating JSON:API responses.
//! It includes structures for JSON:API documents, resources, errors, and helper functions
//! for creating compliant responses.

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

/// A trait for objects that can be represented as a JSON:API resource.
///
/// This trait provides a common interface for converting application models
/// into JSON:API resources, enforcing type safety and reducing boilerplate.
pub trait JsonApiResource {
    /// The JSON:API resource type.
    const RESOURCE_TYPE: &'static str;

    /// The resource's unique identifier.
    fn id(&self) -> String;

    /// The resource's attributes, serialized to `serde_json::Value`.
    fn attributes(&self) -> Value;
}

/// JSON:API Document
///
/// A generic top-level structure for a JSON:API document. This struct is
/// generic over the primary data type `T`, allowing for type-safe resources.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonApiDocument<T> {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<JsonApiData<T>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub errors: Option<Vec<JsonApiError>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub meta: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub jsonapi: Option<JsonApiVersion>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub links: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub included: Option<Vec<Resource<Value>>>,
}

/// JSON:API Data Document
///
/// Represents the primary data in a JSON:API document, which can be either a
/// single resource or a collection of resources.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum JsonApiData<T> {
    Single(Resource<T>),
    Multiple(Vec<Resource<T>>),
}

/// JSON:API Resource Object
///
/// A generic representation of a JSON:API resource object. It is generic over
/// the attributes type `T`, which allows for strongly-typed resource attributes.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Resource<T> {
    #[serde(rename = "type")]
    pub resource_type: String,
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attributes: Option<T>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub relationships: Option<HashMap<String, Relationship>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub links: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub meta: Option<Value>,
}

/// JSON:API Response Builder
///
/// A builder for creating JSON:API responses in a fluent and type-safe manner.
/// This builder simplifies the process of constructing complex JSON:API documents.
pub struct JsonApiResponse<T: Serialize> {
    data: JsonApiData<T>,
    links: Option<HashMap<String, String>>,
    meta: Option<Value>,
    included: Option<Vec<Resource<Value>>>,
}

impl<T: Serialize> JsonApiResponse<T> {
    /// Creates a new `JsonApiResponse` for a single resource.
    pub fn new(resource: Resource<T>) -> Self {
        Self {
            data: JsonApiData::Single(resource),
            links: None,
            meta: None,
            included: None,
        }
    }

    /// Creates a new `JsonApiResponse` for multiple resources.
    pub fn new_multiple(resources: Vec<Resource<T>>) -> Self {
        Self {
            data: JsonApiData::Multiple(resources),
            links: None,
            meta: None,
            included: None,
        }
    }

    /// Adds links to the response.
    pub fn with_links(mut self, links: HashMap<String, String>) -> Self {
        self.links = Some(links);
        self
    }

    /// Adds metadata to the response.
    pub fn with_meta(mut self, meta: Value) -> Self {
        self.meta = Some(meta);
        self
    }

    /// Adds included resources to the response.
    pub fn with_included(mut self, included: Vec<Resource<Value>>) -> Self {
        self.included = Some(included);
        self
    }

    /// Builds the final `JsonApiDocument`.
    pub fn build(self) -> JsonApiDocument<T> {
        JsonApiDocument {
            data: Some(self.data),
            errors: None,
            meta: self.meta,
            jsonapi: Some(JsonApiVersion {
                version: "1.1".to_string(),
                meta: None,
            }),
            links: self.links,
            included: self.included,
        }
    }
}

/// JSON:API Relationship
///
/// An object representing a relationship in JSON:API format.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Relationship {
    /// A links object containing links related to the relationship
    #[serde(skip_serializing_if = "Option::is_none")]
    pub links: Option<HashMap<String, String>>,

    /// A resource linkage or an array of resource linkages
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<RelationshipData>,

    /// A meta object containing non-standard meta-information about the relationship
    #[serde(skip_serializing_if = "Option::is_none")]
    pub meta: Option<Value>,
}

/// JSON:API Relationship Data
///
/// Represents the resource linkage in a relationship.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RelationshipData {
    /// A single resource linkage
    Single(ResourceIdentifier),
    /// An array of resource linkages
    Multiple(Vec<ResourceIdentifier>),
}

/// JSON:API Resource Identifier
///
/// Identifies an individual resource.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ResourceIdentifier {
    /// The resource type
    #[serde(rename = "type")]
    pub resource_type: String,

    /// The resource ID
    pub id: String,

    /// A meta object containing non-standard meta-information about the resource
    #[serde(skip_serializing_if = "Option::is_none")]
    pub meta: Option<Value>,
}

/// JSON:API Error
///
/// An error object in JSON:API format.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JsonApiError {
    /// A unique identifier for this particular occurrence of the problem
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,

    /// A links object containing links related to the error
    #[serde(skip_serializing_if = "Option::is_none")]
    pub links: Option<HashMap<String, String>>,

    /// The HTTP status code applicable to this problem
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,

    /// An application-specific error code
    #[serde(skip_serializing_if = "Option::is_none")]
    pub code: Option<String>,

    /// A short, human-readable summary of the problem
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,

    /// A human-readable explanation specific to this occurrence of the problem
    #[serde(skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,

    /// An object containing references to the source of the error
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source: Option<ErrorSource>,

    /// A meta object containing non-standard meta-information about the error
    #[serde(skip_serializing_if = "Option::is_none")]
    pub meta: Option<Value>,
}

/// JSON:API Error Source
///
/// An object containing references to the source of the error.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ErrorSource {
    /// A JSON Pointer to the associated entity in the request document
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pointer: Option<String>,

    /// A string indicating which URI query parameter caused the error
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parameter: Option<String>,
}

/// JSON:API Version
///
/// An object describing the server's implementation.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JsonApiVersion {
    /// The version of the JSON:API specification supported
    pub version: String,

    /// A meta object containing non-standard meta-information
    #[serde(skip_serializing_if = "Option::is_none")]
    pub meta: Option<Value>,
}

// Builder for creating `JsonApiError` objects.
impl JsonApiError {
    pub fn builder() -> JsonApiErrorBuilder {
        JsonApiErrorBuilder::default()
    }
}

#[derive(Default)]
pub struct JsonApiErrorBuilder {
    id: Option<String>,
    links: Option<HashMap<String, String>>,
    status: Option<String>,
    code: Option<String>,
    title: Option<String>,
    detail: Option<String>,
    source: Option<ErrorSource>,
    meta: Option<Value>,
}

impl JsonApiErrorBuilder {
    pub fn status(mut self, status: &str) -> Self {
        self.status = Some(status.to_string());
        self
    }

    pub fn code(mut self, code: &str) -> Self {
        self.code = Some(code.to_string());
        self
    }

    pub fn title(mut self, title: &str) -> Self {
        self.title = Some(title.to_string());
        self
    }

    pub fn detail(mut self, detail: &str) -> Self {
        self.detail = Some(detail.to_string());
        self
    }

    pub fn source(mut self, pointer: Option<String>, parameter: Option<String>) -> Self {
        self.source = Some(ErrorSource { pointer, parameter });
        self
    }

    pub fn build(self) -> JsonApiError {
        JsonApiError {
            id: self.id,
            links: self.links,
            status: self.status,
            code: self.code,
            title: self.title,
            detail: self.detail,
            source: self.source,
            meta: self.meta,
        }
    }
}