Skip to main content

zai_rs/batches/
list.rs

1use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
2use validator::Validate;
3
4use super::types::BatchItem;
5use crate::{ZaiResult, client::ZaiClient};
6
7/// Query parameters for listing batch processing tasks
8#[derive(Clone, Serialize, Deserialize, Validate)]
9pub struct BatchListQuery {
10    /// Pagination cursor: return results after this ID
11    #[serde(skip_serializing_if = "Option::is_none")]
12    #[validate(length(min = 1))]
13    pub after: Option<String>,
14
15    /// Page size (server default 20).
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub limit: Option<u32>,
18}
19
20impl std::fmt::Debug for BatchListQuery {
21    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        formatter
23            .debug_struct("BatchListQuery")
24            .field("after", &self.after.as_ref().map(|_| "[REDACTED]"))
25            .field("limit", &self.limit)
26            .finish()
27    }
28}
29
30impl Default for BatchListQuery {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl BatchListQuery {
37    /// Create an empty query (no filters)
38    pub fn new() -> Self {
39        Self {
40            after: None,
41            limit: None,
42        }
43    }
44
45    /// Set the pagination cursor
46    pub fn with_after(mut self, after: impl Into<String>) -> Self {
47        self.after = Some(after.into());
48        self
49    }
50
51    /// Set the page size.
52    pub fn with_limit(mut self, limit: u32) -> Self {
53        self.limit = Some(limit);
54        self
55    }
56}
57
58/// Batches list request (GET /paas/v4/batches)
59pub struct BatchListRequest {
60    query: BatchListQuery,
61}
62
63impl BatchListRequest {
64    /// Create a request targeting the batches list endpoint
65    pub fn new() -> Self {
66        Self {
67            query: BatchListQuery::new(),
68        }
69    }
70
71    /// Attach a query to this request
72    pub fn with_query(mut self, q: BatchListQuery) -> Self {
73        self.query = q;
74        self
75    }
76
77    /// Validate the query, send the request, and parse the typed response.
78    pub async fn send_via(&self, client: &ZaiClient) -> ZaiResult<BatchListResponse> {
79        self.query.validate()?;
80        if self
81            .query
82            .after
83            .as_deref()
84            .is_some_and(|after| after.trim().is_empty())
85        {
86            return Err(crate::client::validation::invalid(
87                "after cannot be blank when provided",
88            ));
89        }
90        let mut params: Vec<(&str, String)> = Vec::new();
91        if let Some(after) = self.query.after.as_ref() {
92            params.push(("after", after.clone()));
93        }
94        if let Some(limit) = self.query.limit.as_ref() {
95            params.push(("limit", limit.to_string()));
96        }
97        let borrowed: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
98        let route = crate::client::routes::BATCHES_LIST;
99        let url = client
100            .endpoints()
101            .resolve_route_with_query(route, &[], &borrowed)?;
102        client
103            .send_empty::<BatchListResponse>(route.method(), url)
104            .await
105    }
106}
107
108impl Default for BatchListRequest {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114/// Response for listing batch processing tasks
115#[derive(Debug, Clone, Serialize, Validate)]
116pub struct BatchListResponse {
117    /// Response type ("list")
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub object: Option<BatchListObject>,
120
121    /// Batch task entries
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub data: Option<Vec<BatchItem>>,
124
125    /// First ID in this page
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub first_id: Option<String>,
128
129    /// Last ID in this page
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub last_id: Option<String>,
132
133    /// Whether more data is available
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub has_more: Option<bool>,
136}
137
138#[derive(Deserialize)]
139struct BatchListResponseWire {
140    object: Option<BatchListObject>,
141    data: Option<Vec<BatchItem>>,
142    first_id: Option<String>,
143    last_id: Option<String>,
144    has_more: Option<bool>,
145}
146
147impl<'de> Deserialize<'de> for BatchListResponse {
148    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
149    where
150        D: Deserializer<'de>,
151    {
152        let wire = BatchListResponseWire::deserialize(deserializer)?;
153        if wire.object.is_none()
154            && wire.data.is_none()
155            && wire.first_id.is_none()
156            && wire.last_id.is_none()
157            && wire.has_more.is_none()
158        {
159            return Err(D::Error::custom(
160                "batch-list response contained no documented non-null fields",
161            ));
162        }
163        Ok(Self {
164            object: wire.object,
165            data: wire.data,
166            first_id: wire.first_id,
167            last_id: wire.last_id,
168            has_more: wire.has_more,
169        })
170    }
171}
172
173/// Object type for list responses
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(rename_all = "lowercase")]
176pub enum BatchListObject {
177    /// List marker
178    List,
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn list_response_requires_a_documented_non_null_field() {
187        assert!(serde_json::from_str::<BatchListResponse>("{}").is_err());
188        assert!(serde_json::from_str::<BatchListResponse>(r#"{"data":null}"#).is_err());
189        assert!(serde_json::from_str::<BatchListResponse>(r#"{"data":[]}"#).is_ok());
190        assert!(serde_json::from_str::<BatchListResponse>(r#"{"object":"future"}"#).is_err());
191    }
192
193    #[test]
194    fn query_debug_redacts_the_pagination_identifier() {
195        let query = BatchListQuery::new().with_after("private-batch-id");
196        assert!(!format!("{query:?}").contains("private-batch-id"));
197    }
198
199    #[test]
200    fn limit_does_not_invent_an_upstream_range() {
201        assert!(BatchListQuery::new().with_limit(0).validate().is_ok());
202    }
203}