Skip to main content

ironflow_api/
response.rs

1//! Standard response types and helpers for the REST API.
2//!
3//! All successful responses are wrapped in the [`ApiResponse`] envelope
4//! with optional pagination metadata.
5
6use axum::Json;
7use serde::Serialize;
8
9/// Pagination metadata.
10///
11/// Included in the response when the endpoint returns paginated results.
12///
13/// # Examples
14///
15/// ```
16/// use ironflow_api::response::ApiMeta;
17///
18/// let meta = ApiMeta {
19///     page: Some(1),
20///     per_page: Some(20),
21///     total: Some(100),
22/// };
23/// assert_eq!(meta.page, Some(1));
24/// ```
25#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
26#[derive(Debug, Serialize, Clone)]
27pub struct ApiMeta {
28    /// Current page number (1-based).
29    pub page: Option<u32>,
30    /// Items per page.
31    pub per_page: Option<u32>,
32    /// Total number of items matching the filter.
33    pub total: Option<u64>,
34}
35
36impl ApiMeta {
37    /// Create an empty metadata object (no pagination).
38    pub fn empty() -> Self {
39        ApiMeta {
40            page: None,
41            per_page: None,
42            total: None,
43        }
44    }
45
46    /// Create pagination metadata.
47    pub fn paginated(page: u32, per_page: u32, total: u64) -> Self {
48        ApiMeta {
49            page: Some(page),
50            per_page: Some(per_page),
51            total: Some(total),
52        }
53    }
54}
55
56/// Standard response envelope for all successful API responses.
57///
58/// Serialized as: `{ "data": ..., "meta": { "page": ..., "total": ... } }`
59///
60/// # Examples
61///
62/// ```
63/// use ironflow_api::response::ApiResponse;
64///
65/// let response = ApiResponse {
66///     data: "hello".to_string(),
67///     meta: None,
68/// };
69/// assert_eq!(response.data, "hello");
70/// ```
71#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
72#[derive(Debug, Serialize)]
73pub struct ApiResponse<T: Serialize> {
74    /// The response payload.
75    pub data: T,
76    /// Optional pagination metadata.
77    pub meta: Option<ApiMeta>,
78}
79
80/// Helper to wrap data in a successful response without pagination.
81///
82/// # Examples
83///
84/// ```no_run
85/// use ironflow_api::response::ok;
86///
87/// # async fn handler() {
88/// let data = vec!["a", "b"];
89/// let response = ok(data);
90/// // Returns: { "data": ["a", "b"] }
91/// # }
92/// ```
93pub fn ok<T: Serialize>(data: T) -> Json<ApiResponse<T>> {
94    Json(ApiResponse { data, meta: None })
95}
96
97/// Helper to wrap paginated data in a successful response.
98///
99/// # Examples
100///
101/// ```no_run
102/// use ironflow_api::response::ok_paged;
103///
104/// # async fn handler() {
105/// let data = vec!["a", "b"];
106/// let response = ok_paged(data, 1, 20, 100);
107/// // Returns: { "data": ["a", "b"], "meta": { "page": 1, "per_page": 20, "total": 100 } }
108/// # }
109/// ```
110pub fn ok_paged<T: Serialize>(
111    data: T,
112    page: u32,
113    per_page: u32,
114    total: u64,
115) -> Json<ApiResponse<T>> {
116    Json(ApiResponse {
117        data,
118        meta: Some(ApiMeta::paginated(page, per_page, total)),
119    })
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use serde_json::json;
126
127    #[test]
128    fn api_meta_empty() {
129        let meta = ApiMeta::empty();
130        assert_eq!(meta.page, None);
131        assert_eq!(meta.per_page, None);
132        assert_eq!(meta.total, None);
133    }
134
135    #[test]
136    fn api_meta_paginated() {
137        let meta = ApiMeta::paginated(2, 50, 200);
138        assert_eq!(meta.page, Some(2));
139        assert_eq!(meta.per_page, Some(50));
140        assert_eq!(meta.total, Some(200));
141    }
142
143    #[test]
144    fn api_response_serialize() {
145        let response = ApiResponse {
146            data: vec![1, 2, 3],
147            meta: None,
148        };
149        let json_val = serde_json::to_value(&response).expect("serialize");
150        assert_eq!(json_val["data"], json!([1, 2, 3]));
151        assert_eq!(json_val["meta"], serde_json::json!(null));
152    }
153
154    #[test]
155    fn api_response_with_meta_serialize() {
156        let response = ApiResponse {
157            data: vec!["a"],
158            meta: Some(ApiMeta::paginated(1, 10, 50)),
159        };
160        let json_val = serde_json::to_value(&response).expect("serialize");
161        assert_eq!(json_val["data"], json!(["a"]));
162        assert_eq!(json_val["meta"]["page"], 1);
163        assert_eq!(json_val["meta"]["per_page"], 10);
164        assert_eq!(json_val["meta"]["total"], 50);
165    }
166}