use chrono::{DateTime, Utc};
use crate::pagination::{cursor::CursorPaginationMetadata, offset::OffsetPaginationMetadata};
#[derive(Default, Debug, Clone, serde::Serialize, bon::Builder)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ResponseMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(into)]
pagination: Option<PaginationMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
currency: Option<CurrencyMetadata>,
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct CurrencyMetadata {
pub display: String,
pub base: String,
pub rate: String,
pub as_of: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(untagged)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum PaginationMetadata {
Offset(OffsetPaginationMetadata),
Cursor(CursorPaginationMetadata),
}
impl From<OffsetPaginationMetadata> for PaginationMetadata {
fn from(offset: OffsetPaginationMetadata) -> Self {
PaginationMetadata::Offset(offset)
}
}
impl From<CursorPaginationMetadata> for PaginationMetadata {
fn from(cursor: CursorPaginationMetadata) -> Self {
PaginationMetadata::Cursor(cursor)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_response_metadata_default_is_empty() {
let meta = ResponseMetadata::default();
assert!(meta.pagination.is_none());
}
#[test]
fn test_response_metadata_with_offset_pagination() {
let offset = OffsetPaginationMetadata::new(100, 2, 20);
let meta = ResponseMetadata::builder().pagination(offset).build();
assert!(matches!(
meta.pagination,
Some(PaginationMetadata::Offset(_))
));
}
#[test]
fn test_response_metadata_with_cursor_pagination() {
let cursor = CursorPaginationMetadata {
has_more: true,
next_cursor: Some("abc".to_string()),
limit: 20,
};
let meta = ResponseMetadata::builder().pagination(cursor).build();
assert!(matches!(
meta.pagination,
Some(PaginationMetadata::Cursor(_))
));
}
#[test]
fn test_response_metadata_serializes_nested_pagination() {
let cursor = CursorPaginationMetadata {
has_more: true,
next_cursor: Some("xyz".to_string()),
limit: 10,
};
let meta = ResponseMetadata::builder().pagination(cursor).build();
let json = serde_json::to_value(&meta).unwrap();
assert_eq!(json["pagination"]["hasMore"], true);
assert_eq!(json["pagination"]["nextCursor"], "xyz");
assert_eq!(json["pagination"]["limit"], 10);
}
#[test]
fn test_response_metadata_empty_skips_pagination() {
let meta = ResponseMetadata::builder().build();
let json = serde_json::to_value(&meta).unwrap();
assert!(json.get("pagination").is_none());
}
fn currency() -> CurrencyMetadata {
CurrencyMetadata {
display: "CNY".to_string(),
base: "USD".to_string(),
rate: "7.2".to_string(),
as_of: None,
}
}
#[test]
fn test_response_metadata_currency_serializes_block() {
let meta = ResponseMetadata::builder().currency(currency()).build();
let json = serde_json::to_value(&meta).unwrap();
assert_eq!(json["currency"]["display"], "CNY");
assert_eq!(json["currency"]["base"], "USD");
assert_eq!(json["currency"]["rate"], "7.2");
assert!(json["currency"]["asOf"].is_null());
assert!(json.get("pagination").is_none());
}
#[test]
fn test_response_metadata_currency_coexists_with_pagination() {
let cursor = CursorPaginationMetadata {
has_more: true,
next_cursor: Some("xyz".to_string()),
limit: 10,
};
let meta = ResponseMetadata::builder()
.pagination(cursor)
.currency(currency())
.build();
let json = serde_json::to_value(&meta).unwrap();
assert_eq!(json["pagination"]["hasMore"], true);
assert_eq!(json["currency"]["display"], "CNY");
}
#[test]
fn test_response_metadata_maybe_currency_absent_skips_block() {
let meta = ResponseMetadata::builder()
.maybe_currency(None::<CurrencyMetadata>)
.build();
let json = serde_json::to_value(&meta).unwrap();
assert!(json.get("currency").is_none());
}
}