openai_ergonomic/builders/
uploads.rs1use openai_client_base::models::create_upload_request::Purpose;
7use openai_client_base::models::{file_expiration_after, CreateUploadRequest, FileExpirationAfter};
8
9use crate::{Builder, Error, Result};
10
11#[derive(Debug, Clone)]
32pub struct UploadBuilder {
33 filename: String,
34 purpose: Purpose,
35 bytes: i32,
36 mime_type: String,
37 expires_after: Option<FileExpirationAfter>,
38}
39
40impl UploadBuilder {
41 #[must_use]
43 pub fn new(
44 filename: impl Into<String>,
45 purpose: Purpose,
46 bytes: i32,
47 mime_type: impl Into<String>,
48 ) -> Self {
49 Self {
50 filename: filename.into(),
51 purpose,
52 bytes,
53 mime_type: mime_type.into(),
54 expires_after: None,
55 }
56 }
57
58 #[must_use]
60 pub fn filename(mut self, filename: impl Into<String>) -> Self {
61 self.filename = filename.into();
62 self
63 }
64
65 #[must_use]
67 pub fn bytes(mut self, bytes: i32) -> Self {
68 self.bytes = bytes;
69 self
70 }
71
72 #[must_use]
74 pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
75 self.mime_type = mime_type.into();
76 self
77 }
78
79 #[must_use]
81 pub fn purpose(mut self, purpose: Purpose) -> Self {
82 self.purpose = purpose;
83 self
84 }
85
86 #[must_use]
88 pub fn expires_after(mut self, expiration: FileExpirationAfter) -> Self {
89 self.expires_after = Some(expiration);
90 self
91 }
92
93 #[must_use]
95 pub fn expires_after_seconds(mut self, seconds: i32) -> Self {
96 let expiration =
97 FileExpirationAfter::new(file_expiration_after::Anchor::CreatedAt, seconds);
98 self.expires_after = Some(expiration);
99 self
100 }
101
102 #[must_use]
104 pub fn purpose_ref(&self) -> Purpose {
105 self.purpose
106 }
107
108 #[must_use]
110 pub fn expires_after_ref(&self) -> Option<&FileExpirationAfter> {
111 self.expires_after.as_ref()
112 }
113
114 fn validate(&self) -> Result<()> {
115 if self.bytes <= 0 {
116 return Err(Error::InvalidRequest(
117 "Upload byte size must be positive".to_string(),
118 ));
119 }
120
121 if let Some(expiration) = &self.expires_after {
122 if !(3600..=2_592_000).contains(&expiration.seconds) {
123 return Err(Error::InvalidRequest(format!(
124 "Expiration seconds must be between 3600 and 2592000 (got {})",
125 expiration.seconds
126 )));
127 }
128 }
129
130 Ok(())
131 }
132}
133
134impl Builder<CreateUploadRequest> for UploadBuilder {
135 fn build(self) -> Result<CreateUploadRequest> {
136 self.validate()?;
137 let mut request =
138 CreateUploadRequest::new(self.filename, self.purpose, self.bytes, self.mime_type);
139 request.expires_after = self.expires_after.map(Box::new);
140 Ok(request)
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn builds_valid_request() {
150 let builder = UploadBuilder::new(
151 "transcript.zip",
152 Purpose::Assistants,
153 1024,
154 "application/zip",
155 )
156 .expires_after_seconds(7200);
157
158 let request = builder.build().expect("builder should succeed");
159 assert_eq!(request.filename, "transcript.zip");
160 assert_eq!(request.bytes, 1024);
161 assert_eq!(request.mime_type, "application/zip");
162 assert!(request.expires_after.is_some());
163 }
164
165 #[test]
166 fn enforces_positive_bytes() {
167 let builder = UploadBuilder::new("file", Purpose::Assistants, 0, "text/plain");
168 let error = builder.build().expect_err("should fail validation");
169 assert!(matches!(error, Error::InvalidRequest(message) if message.contains("positive")));
170 }
171
172 #[test]
173 fn validates_expiration_range() {
174 let builder = UploadBuilder::new("file", Purpose::Assistants, 1_024, "text/plain")
175 .expires_after_seconds(10);
176 let error = builder.build().expect_err("should enforce range");
177 assert!(matches!(
178 error,
179 Error::InvalidRequest(message) if message.contains("3600")
180 ));
181 }
182}