openai_ergonomic/builders/
files.rs

1//! Files API builders.
2//!
3//! This module provides ergonomic builders for `OpenAI` Files API operations,
4//! including uploading files, managing file metadata, and retrieving file content.
5//!
6//! Files are used for various purposes including:
7//! - Training data for fine-tuning
8//! - Documents for assistants and RAG applications
9//! - Images for vision models
10
11use std::path::Path;
12
13/// Builder for file upload operations.
14///
15/// This builder provides a fluent interface for uploading files to `OpenAI`
16/// with specified purposes and metadata.
17#[derive(Debug, Clone)]
18pub struct FileUploadBuilder {
19    filename: String,
20    purpose: FilePurpose,
21    content: Vec<u8>,
22}
23
24/// Purpose for which the file is being uploaded.
25#[derive(Debug, Clone)]
26pub enum FilePurpose {
27    /// File for fine-tuning operations
28    FineTune,
29    /// File for assistant operations (RAG, file search)
30    Assistants,
31    /// File for vision model operations
32    Vision,
33    /// File for batch operations
34    Batch,
35    /// Custom purpose (for future extensions)
36    Custom(String),
37}
38
39impl FileUploadBuilder {
40    /// Create a new file upload builder with file content and purpose.
41    ///
42    /// # Examples
43    ///
44    /// ```rust
45    /// use openai_ergonomic::builders::files::{FileUploadBuilder, FilePurpose};
46    ///
47    /// let content = b"Hello, world!";
48    /// let builder = FileUploadBuilder::new("hello.txt", FilePurpose::Assistants, content.to_vec());
49    /// ```
50    #[must_use]
51    pub fn new(filename: impl Into<String>, purpose: FilePurpose, content: Vec<u8>) -> Self {
52        Self {
53            filename: filename.into(),
54            purpose,
55            content,
56        }
57    }
58
59    /// Create a file upload builder from a file path.
60    ///
61    /// This is a convenience method that reads the file from disk.
62    /// Note: This is a sync operation and will read the entire file into memory.
63    pub fn from_path(path: impl AsRef<Path>, purpose: FilePurpose) -> Result<Self, std::io::Error> {
64        let path = path.as_ref();
65        let content = std::fs::read(path)?;
66        let filename = path
67            .file_name()
68            .and_then(|name| name.to_str())
69            .unwrap_or("file")
70            .to_string();
71
72        Ok(Self::new(filename, purpose, content))
73    }
74
75    /// Create a file upload builder from text content.
76    #[must_use]
77    pub fn from_text(
78        filename: impl Into<String>,
79        purpose: FilePurpose,
80        text: impl Into<String>,
81    ) -> Self {
82        Self::new(filename, purpose, text.into().into_bytes())
83    }
84
85    /// Create a file upload builder from JSON content.
86    pub fn from_json(
87        filename: impl Into<String>,
88        purpose: FilePurpose,
89        json: &serde_json::Value,
90    ) -> Result<Self, serde_json::Error> {
91        let content = serde_json::to_vec(json)?;
92        Ok(Self::new(filename, purpose, content))
93    }
94
95    /// Get the filename for this upload.
96    #[must_use]
97    pub fn filename(&self) -> &str {
98        &self.filename
99    }
100
101    /// Get the purpose for this upload.
102    #[must_use]
103    pub fn purpose(&self) -> &FilePurpose {
104        &self.purpose
105    }
106
107    /// Get the content for this upload.
108    #[must_use]
109    pub fn content(&self) -> &[u8] {
110        &self.content
111    }
112
113    /// Get the size of the content in bytes.
114    #[must_use]
115    pub fn content_size(&self) -> usize {
116        self.content.len()
117    }
118
119    /// Check if the file is empty.
120    #[must_use]
121    pub fn is_empty(&self) -> bool {
122        self.content.is_empty()
123    }
124
125    /// Get the content as a string (if it's valid UTF-8).
126    #[must_use]
127    pub fn content_as_string(&self) -> Option<String> {
128        String::from_utf8(self.content.clone()).ok()
129    }
130}
131
132/// Builder for file retrieval operations.
133#[derive(Debug, Clone)]
134pub struct FileRetrievalBuilder {
135    file_id: String,
136}
137
138impl FileRetrievalBuilder {
139    /// Create a new file retrieval builder.
140    #[must_use]
141    pub fn new(file_id: impl Into<String>) -> Self {
142        Self {
143            file_id: file_id.into(),
144        }
145    }
146
147    /// Get the file ID for this retrieval.
148    #[must_use]
149    pub fn file_id(&self) -> &str {
150        &self.file_id
151    }
152}
153
154/// Builder for file listing operations.
155#[derive(Debug, Clone, Default)]
156pub struct FileListBuilder {
157    purpose: Option<FilePurpose>,
158    limit: Option<i32>,
159    order: Option<FileOrder>,
160}
161
162/// Order for file listing.
163#[derive(Debug, Clone)]
164pub enum FileOrder {
165    /// Ascending order (oldest first)
166    Asc,
167    /// Descending order (newest first)
168    Desc,
169}
170
171impl FileListBuilder {
172    /// Create a new file list builder.
173    #[must_use]
174    pub fn new() -> Self {
175        Self::default()
176    }
177
178    /// Filter files by purpose.
179    #[must_use]
180    pub fn purpose(mut self, purpose: FilePurpose) -> Self {
181        self.purpose = Some(purpose);
182        self
183    }
184
185    /// Set the maximum number of files to return.
186    #[must_use]
187    pub fn limit(mut self, limit: i32) -> Self {
188        self.limit = Some(limit);
189        self
190    }
191
192    /// Set the order for file listing.
193    #[must_use]
194    pub fn order(mut self, order: FileOrder) -> Self {
195        self.order = Some(order);
196        self
197    }
198
199    /// Get the purpose filter.
200    #[must_use]
201    pub fn purpose_ref(&self) -> Option<&FilePurpose> {
202        self.purpose.as_ref()
203    }
204
205    /// Get the limit.
206    #[must_use]
207    pub fn limit_ref(&self) -> Option<i32> {
208        self.limit
209    }
210
211    /// Get the order.
212    #[must_use]
213    pub fn order_ref(&self) -> Option<&FileOrder> {
214        self.order.as_ref()
215    }
216}
217
218/// Builder for file deletion operations.
219#[derive(Debug, Clone)]
220pub struct FileDeleteBuilder {
221    file_id: String,
222}
223
224impl FileDeleteBuilder {
225    /// Create a new file delete builder.
226    #[must_use]
227    pub fn new(file_id: impl Into<String>) -> Self {
228        Self {
229            file_id: file_id.into(),
230        }
231    }
232
233    /// Get the file ID for this deletion.
234    #[must_use]
235    pub fn file_id(&self) -> &str {
236        &self.file_id
237    }
238}
239
240/// Helper function to upload a text file for fine-tuning.
241#[must_use]
242pub fn upload_fine_tune_file(
243    filename: impl Into<String>,
244    content: impl Into<String>,
245) -> FileUploadBuilder {
246    FileUploadBuilder::from_text(filename, FilePurpose::FineTune, content)
247}
248
249/// Helper function to upload a text file for assistants.
250#[must_use]
251pub fn upload_assistants_file(
252    filename: impl Into<String>,
253    content: impl Into<String>,
254) -> FileUploadBuilder {
255    FileUploadBuilder::from_text(filename, FilePurpose::Assistants, content)
256}
257
258/// Helper function to upload a JSON file.
259pub fn upload_json_file(
260    filename: impl Into<String>,
261    purpose: FilePurpose,
262    json: &serde_json::Value,
263) -> Result<FileUploadBuilder, serde_json::Error> {
264    FileUploadBuilder::from_json(filename, purpose, json)
265}
266
267/// Helper function to upload a file from a path.
268pub fn upload_file_from_path(
269    path: impl AsRef<Path>,
270    purpose: FilePurpose,
271) -> Result<FileUploadBuilder, std::io::Error> {
272    FileUploadBuilder::from_path(path, purpose)
273}
274
275/// Helper function to retrieve a file.
276#[must_use]
277pub fn retrieve_file(file_id: impl Into<String>) -> FileRetrievalBuilder {
278    FileRetrievalBuilder::new(file_id)
279}
280
281/// Helper function to list all files.
282#[must_use]
283pub fn list_files() -> FileListBuilder {
284    FileListBuilder::new()
285}
286
287/// Helper function to list files with a specific purpose.
288#[must_use]
289pub fn list_files_by_purpose(purpose: FilePurpose) -> FileListBuilder {
290    FileListBuilder::new().purpose(purpose)
291}
292
293/// Helper function to list files with a limit.
294#[must_use]
295pub fn list_files_with_limit(limit: i32) -> FileListBuilder {
296    FileListBuilder::new().limit(limit)
297}
298
299/// Helper function to delete a file.
300#[must_use]
301pub fn delete_file(file_id: impl Into<String>) -> FileDeleteBuilder {
302    FileDeleteBuilder::new(file_id)
303}
304
305impl std::fmt::Display for FilePurpose {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        match self {
308            FilePurpose::FineTune => write!(f, "fine-tune"),
309            FilePurpose::Assistants => write!(f, "assistants"),
310            FilePurpose::Vision => write!(f, "vision"),
311            FilePurpose::Batch => write!(f, "batch"),
312            FilePurpose::Custom(purpose) => write!(f, "{purpose}"),
313        }
314    }
315}
316
317impl std::fmt::Display for FileOrder {
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        match self {
320            FileOrder::Asc => write!(f, "asc"),
321            FileOrder::Desc => write!(f, "desc"),
322        }
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_file_upload_builder_new() {
332        let content = b"test content".to_vec();
333        let builder = FileUploadBuilder::new("test.txt", FilePurpose::Assistants, content.clone());
334
335        assert_eq!(builder.filename(), "test.txt");
336        assert_eq!(builder.content(), content.as_slice());
337        assert_eq!(builder.content_size(), content.len());
338        assert!(!builder.is_empty());
339        match builder.purpose() {
340            FilePurpose::Assistants => {}
341            _ => panic!("Expected Assistants purpose"),
342        }
343    }
344
345    #[test]
346    fn test_file_upload_builder_from_text() {
347        let builder =
348            FileUploadBuilder::from_text("hello.txt", FilePurpose::FineTune, "Hello, world!");
349
350        assert_eq!(builder.filename(), "hello.txt");
351        assert_eq!(
352            builder.content_as_string(),
353            Some("Hello, world!".to_string())
354        );
355        assert!(!builder.is_empty());
356        match builder.purpose() {
357            FilePurpose::FineTune => {}
358            _ => panic!("Expected FineTune purpose"),
359        }
360    }
361
362    #[test]
363    fn test_file_upload_builder_from_json() {
364        let json = serde_json::json!({
365            "name": "test",
366            "value": 42
367        });
368
369        let builder = FileUploadBuilder::from_json("data.json", FilePurpose::Batch, &json).unwrap();
370
371        assert_eq!(builder.filename(), "data.json");
372        assert!(!builder.is_empty());
373        assert!(builder.content_size() > 0);
374        match builder.purpose() {
375            FilePurpose::Batch => {}
376            _ => panic!("Expected Batch purpose"),
377        }
378    }
379
380    #[test]
381    fn test_file_retrieval_builder() {
382        let builder = FileRetrievalBuilder::new("file-123");
383        assert_eq!(builder.file_id(), "file-123");
384    }
385
386    #[test]
387    fn test_file_list_builder() {
388        let builder = FileListBuilder::new()
389            .purpose(FilePurpose::Assistants)
390            .limit(10)
391            .order(FileOrder::Desc);
392
393        match builder.purpose_ref() {
394            Some(FilePurpose::Assistants) => {}
395            _ => panic!("Expected Assistants purpose"),
396        }
397        assert_eq!(builder.limit_ref(), Some(10));
398        match builder.order_ref() {
399            Some(FileOrder::Desc) => {}
400            _ => panic!("Expected Desc order"),
401        }
402    }
403
404    #[test]
405    fn test_file_delete_builder() {
406        let builder = FileDeleteBuilder::new("file-456");
407        assert_eq!(builder.file_id(), "file-456");
408    }
409
410    #[test]
411    fn test_upload_fine_tune_file_helper() {
412        let builder = upload_fine_tune_file("training.jsonl", "test data");
413        assert_eq!(builder.filename(), "training.jsonl");
414        match builder.purpose() {
415            FilePurpose::FineTune => {}
416            _ => panic!("Expected FineTune purpose"),
417        }
418    }
419
420    #[test]
421    fn test_upload_assistants_file_helper() {
422        let builder = upload_assistants_file("doc.txt", "document content");
423        assert_eq!(builder.filename(), "doc.txt");
424        match builder.purpose() {
425            FilePurpose::Assistants => {}
426            _ => panic!("Expected Assistants purpose"),
427        }
428    }
429
430    #[test]
431    fn test_upload_json_file_helper() {
432        let json = serde_json::json!({"test": true});
433        let builder = upload_json_file("test.json", FilePurpose::Vision, &json).unwrap();
434        assert_eq!(builder.filename(), "test.json");
435        match builder.purpose() {
436            FilePurpose::Vision => {}
437            _ => panic!("Expected Vision purpose"),
438        }
439    }
440
441    #[test]
442    fn test_retrieve_file_helper() {
443        let builder = retrieve_file("file-789");
444        assert_eq!(builder.file_id(), "file-789");
445    }
446
447    #[test]
448    fn test_list_files_helper() {
449        let builder = list_files();
450        assert!(builder.purpose_ref().is_none());
451        assert!(builder.limit_ref().is_none());
452        assert!(builder.order_ref().is_none());
453    }
454
455    #[test]
456    fn test_list_files_by_purpose_helper() {
457        let builder = list_files_by_purpose(FilePurpose::FineTune);
458        match builder.purpose_ref() {
459            Some(FilePurpose::FineTune) => {}
460            _ => panic!("Expected FineTune purpose"),
461        }
462    }
463
464    #[test]
465    fn test_list_files_with_limit_helper() {
466        let builder = list_files_with_limit(5);
467        assert_eq!(builder.limit_ref(), Some(5));
468    }
469
470    #[test]
471    fn test_delete_file_helper() {
472        let builder = delete_file("file-delete");
473        assert_eq!(builder.file_id(), "file-delete");
474    }
475
476    #[test]
477    fn test_file_purpose_display() {
478        assert_eq!(FilePurpose::FineTune.to_string(), "fine-tune");
479        assert_eq!(FilePurpose::Assistants.to_string(), "assistants");
480        assert_eq!(FilePurpose::Vision.to_string(), "vision");
481        assert_eq!(FilePurpose::Batch.to_string(), "batch");
482        assert_eq!(
483            FilePurpose::Custom("custom".to_string()).to_string(),
484            "custom"
485        );
486    }
487
488    #[test]
489    fn test_file_order_display() {
490        assert_eq!(FileOrder::Asc.to_string(), "asc");
491        assert_eq!(FileOrder::Desc.to_string(), "desc");
492    }
493
494    #[test]
495    fn test_empty_file() {
496        let builder = FileUploadBuilder::new("empty.txt", FilePurpose::Assistants, vec![]);
497        assert!(builder.is_empty());
498        assert_eq!(builder.content_size(), 0);
499        assert_eq!(builder.content_as_string(), Some(String::new()));
500    }
501}