1use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
2use validator::Validate;
3
4use super::types::BatchItem;
5use crate::{ZaiResult, client::ZaiClient};
6
7#[derive(Clone, Serialize, Deserialize, Validate)]
9pub struct BatchListQuery {
10 #[serde(skip_serializing_if = "Option::is_none")]
12 #[validate(length(min = 1))]
13 pub after: Option<String>,
14
15 #[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 pub fn new() -> Self {
39 Self {
40 after: None,
41 limit: None,
42 }
43 }
44
45 pub fn with_after(mut self, after: impl Into<String>) -> Self {
47 self.after = Some(after.into());
48 self
49 }
50
51 pub fn with_limit(mut self, limit: u32) -> Self {
53 self.limit = Some(limit);
54 self
55 }
56}
57
58pub struct BatchListRequest {
60 query: BatchListQuery,
61}
62
63impl BatchListRequest {
64 pub fn new() -> Self {
66 Self {
67 query: BatchListQuery::new(),
68 }
69 }
70
71 pub fn with_query(mut self, q: BatchListQuery) -> Self {
73 self.query = q;
74 self
75 }
76
77 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#[derive(Debug, Clone, Serialize, Validate)]
116pub struct BatchListResponse {
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub object: Option<BatchListObject>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub data: Option<Vec<BatchItem>>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub first_id: Option<String>,
128
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub last_id: Option<String>,
132
133 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(rename_all = "lowercase")]
176pub enum BatchListObject {
177 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}