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}