road-runner-common 0.8.0

Shared Rust utilities for exchange ecosystem backend services.
Documentation
use serde::{Deserialize, Serialize};
use time::{format_description::well_known::Rfc3339, OffsetDateTime};

#[cfg(feature = "openapi")]
use utoipa::ToSchema;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ApiMeta {
    pub request_id: String,
    pub timestamp: String, // RFC3339
}

impl ApiMeta {
    pub fn new(request_id: impl Into<String>) -> Self {
        let ts = OffsetDateTime::now_utc()
            .format(&Rfc3339)
            .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string());

        Self {
            request_id: request_id.into(),
            timestamp: ts,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ApiErrorDetail {
    pub code: String,
    pub message: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub field: Option<String>,
}

impl ApiErrorDetail {
    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
        Self {
            code: code.into(),
            message: message.into(),
            field: None,
        }
    }

    pub fn with_field(mut self, field: impl Into<String>) -> Self {
        self.field = Some(field.into());
        self
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ApiResponse<T> {
    pub success: bool,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<T>,

    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub errors: Vec<ApiErrorDetail>,

    pub meta: ApiMeta,
}

impl<T> ApiResponse<T> {
    pub fn ok(data: T, request_id: impl Into<String>) -> Self {
        Self {
            success: true,
            data: Some(data),
            errors: vec![],
            meta: ApiMeta::new(request_id),
        }
    }

    pub fn empty_ok(request_id: impl Into<String>) -> Self {
        Self {
            success: true,
            data: None,
            errors: vec![],
            meta: ApiMeta::new(request_id),
        }
    }

    pub fn err(errors: Vec<ApiErrorDetail>, request_id: impl Into<String>) -> Self {
        Self {
            success: false,
            data: None,
            errors,
            meta: ApiMeta::new(request_id),
        }
    }
}