Skip to main content

ironflow_types/
lib.rs

1//! Shared API envelope types for the Ironflow ecosystem.
2//!
3//! Defines the standard response envelope (`ApiResponse<T>` + `ApiMeta`)
4//! and error envelope (`ErrorEnvelope`) used by both the server
5//! ([`ironflow-api`]) and the client SDK ([`ironflow-sdk`]).
6//!
7//! # Features
8//!
9//! - **`openapi`** -- derive [`utoipa::ToSchema`] for OpenAPI spec generation.
10
11use serde::{Deserialize, Serialize};
12
13/// Pagination metadata returned by list endpoints.
14///
15/// # Examples
16///
17/// ```
18/// use ironflow_types::ApiMeta;
19///
20/// let meta = ApiMeta::paginated(2, 50, 200);
21/// assert_eq!(meta.page, Some(2));
22/// assert_eq!(meta.total, Some(200));
23/// ```
24#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ApiMeta {
27    /// Current page number (1-based).
28    pub page: Option<u32>,
29    /// Items per page.
30    pub per_page: Option<u32>,
31    /// Total number of items matching the filter.
32    pub total: Option<u64>,
33}
34
35impl ApiMeta {
36    /// Create an empty metadata object (no pagination).
37    pub fn empty() -> Self {
38        Self {
39            page: None,
40            per_page: None,
41            total: None,
42        }
43    }
44
45    /// Create pagination metadata.
46    pub fn paginated(page: u32, per_page: u32, total: u64) -> Self {
47        Self {
48            page: Some(page),
49            per_page: Some(per_page),
50            total: Some(total),
51        }
52    }
53}
54
55/// Standard response envelope for all successful API responses.
56///
57/// Serialized as: `{ "data": ..., "meta": { "page": ..., "total": ... } }`
58///
59/// # Examples
60///
61/// ```
62/// use ironflow_types::ApiResponse;
63///
64/// let json = r#"{"data": [1, 2, 3], "meta": null}"#;
65/// let resp: ApiResponse<Vec<i32>> = serde_json::from_str(json).unwrap();
66/// assert_eq!(resp.data.len(), 3);
67/// ```
68#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ApiResponse<T> {
71    /// The response payload.
72    pub data: T,
73    /// Optional pagination metadata.
74    pub meta: Option<ApiMeta>,
75}
76
77/// Error response body.
78///
79/// The inner part of the API error envelope:
80/// `{ "error": { "code": "...", "message": "..." } }`.
81///
82/// # Examples
83///
84/// ```
85/// use ironflow_types::ErrorEnvelope;
86///
87/// let json = r#"{"code": "RUN_NOT_FOUND", "message": "run not found"}"#;
88/// let err: ErrorEnvelope = serde_json::from_str(json).unwrap();
89/// assert_eq!(err.code, "RUN_NOT_FOUND");
90/// ```
91#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ErrorEnvelope {
94    /// Machine-readable error code (e.g., `RUN_NOT_FOUND`).
95    pub code: String,
96    /// Human-readable error message.
97    pub message: String,
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn api_meta_empty() {
106        let meta = ApiMeta::empty();
107        assert!(meta.page.is_none());
108        assert!(meta.per_page.is_none());
109        assert!(meta.total.is_none());
110    }
111
112    #[test]
113    fn api_meta_paginated() {
114        let meta = ApiMeta::paginated(2, 50, 200);
115        assert_eq!(meta.page, Some(2));
116        assert_eq!(meta.per_page, Some(50));
117        assert_eq!(meta.total, Some(200));
118    }
119
120    #[test]
121    fn api_response_roundtrip() {
122        let response = ApiResponse {
123            data: vec![1, 2, 3],
124            meta: Some(ApiMeta::paginated(1, 10, 50)),
125        };
126        let json = serde_json::to_string(&response).unwrap();
127        let deserialized: ApiResponse<Vec<i32>> = serde_json::from_str(&json).unwrap();
128        assert_eq!(deserialized.data, vec![1, 2, 3]);
129        assert_eq!(deserialized.meta.unwrap().total, Some(50));
130    }
131
132    #[test]
133    fn error_envelope_roundtrip() {
134        let envelope = ErrorEnvelope {
135            code: "BAD_REQUEST".to_string(),
136            message: "invalid input".to_string(),
137        };
138        let json = serde_json::to_string(&envelope).unwrap();
139        let deserialized: ErrorEnvelope = serde_json::from_str(&json).unwrap();
140        assert_eq!(deserialized.code, "BAD_REQUEST");
141        assert_eq!(deserialized.message, "invalid input");
142    }
143}