1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Deserialize)]
7pub struct FileObject {
8 pub id: String,
10 pub filename: String,
12 pub bytes: u64,
14 pub created_at: i64,
16 pub purpose: String,
18 #[serde(default)]
20 pub object: String,
21}
22
23#[derive(Debug, Clone, Deserialize)]
25pub struct FileListResponse {
26 pub data: Vec<FileObject>,
28 #[serde(default)]
30 pub object: String,
31}
32
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum FilePurpose {
37 #[default]
39 Assistants,
40 FineTune,
42 Batch,
44}
45
46impl FilePurpose {
47 pub fn as_str(&self) -> &'static str {
51 match self {
52 FilePurpose::Assistants => "assistants",
53 FilePurpose::FineTune => "fine_tune",
54 FilePurpose::Batch => "batch",
55 }
56 }
57}
58
59impl std::fmt::Display for FilePurpose {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 write!(f, "{}", self.as_str())
62 }
63}
64
65#[derive(Debug, Clone, Deserialize)]
67pub struct DeleteFileResponse {
68 pub id: String,
70 pub deleted: bool,
72 #[serde(default)]
74 pub object: String,
75}
76
77#[derive(Debug, Clone, Serialize)]
79pub struct FileDownloadRequest {
80 pub file_id: String,
82}
83
84#[derive(Debug, Clone, Serialize)]
86pub struct FileUploadInitializeRequest {
87 pub filename: String,
89 pub purpose: FilePurpose,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub total_size_bytes: Option<u64>,
94}
95
96impl FileUploadInitializeRequest {
97 pub fn new(filename: impl Into<String>, purpose: FilePurpose) -> Self {
99 Self {
100 filename: filename.into(),
101 purpose,
102 total_size_bytes: None,
103 }
104 }
105
106 pub fn total_size_bytes(mut self, size: u64) -> Self {
108 self.total_size_bytes = Some(size);
109 self
110 }
111}
112
113#[derive(Debug, Clone, Deserialize)]
115pub struct FileUploadInitializeResponse {
116 #[serde(default)]
118 pub upload_id: String,
119 #[serde(default)]
121 pub object: String,
122 #[serde(default)]
124 pub chunk_size: Option<u64>,
125 #[serde(default)]
127 pub expires_at: Option<i64>,
128}
129
130#[derive(Debug, Clone, Serialize)]
132pub struct FileUploadChunksRequest {
133 pub upload_id: String,
135 pub part: u32,
137 pub chunk: String,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub checksum: Option<String>,
142}
143
144impl FileUploadChunksRequest {
145 pub fn new(upload_id: impl Into<String>, part: u32, chunk: impl Into<String>) -> Self {
147 Self {
148 upload_id: upload_id.into(),
149 part,
150 chunk: chunk.into(),
151 checksum: None,
152 }
153 }
154
155 pub fn checksum(mut self, checksum: impl Into<String>) -> Self {
157 self.checksum = Some(checksum.into());
158 self
159 }
160}
161
162#[derive(Debug, Clone, Deserialize)]
164pub struct FileUploadChunksResponse {
165 #[serde(default)]
167 pub upload_id: String,
168 #[serde(default)]
170 pub complete: bool,
171 #[serde(default)]
173 pub status: Option<String>,
174 #[serde(default)]
176 pub uploaded_part: Option<u32>,
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
186 fn file_purpose_roundtrip_all_variants() {
187 for (variant, expected) in [
188 (FilePurpose::Assistants, "assistants"),
189 (FilePurpose::FineTune, "fine_tune"),
190 (FilePurpose::Batch, "batch"),
191 ] {
192 let json = serde_json::to_string(&variant).unwrap();
193 assert_eq!(json, format!("\"{expected}\""));
194
195 let back: FilePurpose = serde_json::from_str(&json).unwrap();
196 assert_eq!(back, variant);
197 }
198 }
199
200 #[test]
201 fn file_purpose_as_str_matches_serde() {
202 for variant in [
203 FilePurpose::Assistants,
204 FilePurpose::FineTune,
205 FilePurpose::Batch,
206 ] {
207 let serde_str = serde_json::to_value(variant)
208 .unwrap()
209 .as_str()
210 .unwrap()
211 .to_string();
212 assert_eq!(variant.as_str(), serde_str);
213 }
214 }
215
216 #[test]
217 fn file_purpose_display_matches_as_str() {
218 for variant in [
219 FilePurpose::Assistants,
220 FilePurpose::FineTune,
221 FilePurpose::Batch,
222 ] {
223 assert_eq!(format!("{variant}"), variant.as_str());
224 }
225 }
226
227 #[test]
228 fn file_purpose_default_is_assistants() {
229 assert_eq!(FilePurpose::default(), FilePurpose::Assistants);
230 }
231
232 #[test]
233 fn file_purpose_rejects_unknown() {
234 let result = serde_json::from_str::<FilePurpose>(r#""unknown_purpose""#);
235 assert!(result.is_err());
236 }
237
238 #[test]
241 fn file_object_deserialize() {
242 let json = serde_json::json!({
243 "id": "file-abc",
244 "filename": "test.txt",
245 "bytes": 1024,
246 "created_at": 1700000000,
247 "purpose": "assistants",
248 "object": "file"
249 });
250 let fo: FileObject = serde_json::from_value(json).unwrap();
251 assert_eq!(fo.id, "file-abc");
252 assert_eq!(fo.filename, "test.txt");
253 assert_eq!(fo.bytes, 1024);
254 assert_eq!(fo.purpose, "assistants");
255 }
256
257 #[test]
258 fn file_object_object_field_defaults() {
259 let json = serde_json::json!({
260 "id": "file-abc",
261 "filename": "test.txt",
262 "bytes": 0,
263 "created_at": 0,
264 "purpose": "batch"
265 });
266 let fo: FileObject = serde_json::from_value(json).unwrap();
267 assert_eq!(fo.object, ""); }
269
270 #[test]
273 fn delete_file_response_deserialize() {
274 let json = serde_json::json!({
275 "id": "file-xyz",
276 "deleted": true,
277 "object": "file"
278 });
279 let resp: DeleteFileResponse = serde_json::from_value(json).unwrap();
280 assert_eq!(resp.id, "file-xyz");
281 assert!(resp.deleted);
282 }
283
284 #[test]
287 fn file_list_response_deserialize() {
288 let json = serde_json::json!({
289 "data": [{
290 "id": "file-1",
291 "filename": "a.txt",
292 "bytes": 100,
293 "created_at": 0,
294 "purpose": "assistants"
295 }],
296 "object": "list"
297 });
298 let list: FileListResponse = serde_json::from_value(json).unwrap();
299 assert_eq!(list.data.len(), 1);
300 assert_eq!(list.data[0].id, "file-1");
301 }
302
303 #[test]
304 fn file_upload_initialize_request_roundtrip() {
305 let request = FileUploadInitializeRequest::new("video.mp4", FilePurpose::FineTune)
306 .total_size_bytes(12345);
307 let json = serde_json::to_value(&request).unwrap();
308 assert_eq!(json["filename"], "video.mp4");
309 assert_eq!(json["purpose"], "fine_tune");
310 assert_eq!(json["total_size_bytes"], 12345);
311 }
312
313 #[test]
314 fn file_upload_chunks_request_roundtrip() {
315 let request = FileUploadChunksRequest::new("upload-1", 2, "YQ==").checksum("abc");
316 let json = serde_json::to_value(&request).unwrap();
317 assert_eq!(json["upload_id"], "upload-1");
318 assert_eq!(json["part"], 2);
319 assert_eq!(json["chunk"], "YQ==");
320 assert_eq!(json["checksum"], "abc");
321 }
322
323 #[test]
324 fn file_upload_chunks_response_deserialize() {
325 let json = serde_json::json!({
326 "upload_id": "upload-1",
327 "complete": true,
328 "status": "complete",
329 "uploaded_part": 2,
330 });
331 let response: FileUploadChunksResponse = serde_json::from_value(json).unwrap();
332 assert_eq!(response.upload_id, "upload-1");
333 assert!(response.complete);
334 assert_eq!(response.uploaded_part, Some(2));
335 }
336}