1use crate::error::LingerError;
2use crate::transport::{BodyStream, HttpRequest};
3use crate::RequestId;
4use bytes::Bytes;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::BTreeMap;
8use std::fmt;
9
10#[derive(Clone, Debug, Serialize, PartialEq)]
13#[non_exhaustive]
14pub struct CreateContainerRequest {
15 pub name: String,
18 #[serde(skip_serializing_if = "Vec::is_empty")]
21 pub file_ids: Vec<String>,
22 #[serde(flatten)]
25 pub extra: BTreeMap<String, Value>,
26}
27
28impl CreateContainerRequest {
29 pub fn builder() -> CreateContainerRequestBuilder {
32 CreateContainerRequestBuilder::default()
33 }
34}
35
36#[derive(Clone, Debug, Default)]
39#[non_exhaustive]
40pub struct CreateContainerRequestBuilder {
41 name: Option<String>,
42 file_ids: Vec<String>,
43 extra: BTreeMap<String, Value>,
44}
45
46impl CreateContainerRequestBuilder {
47 pub fn name(mut self, name: impl Into<String>) -> Self {
50 self.name = Some(name.into());
51 self
52 }
53
54 pub fn file_id(mut self, file_id: impl Into<String>) -> Self {
57 self.file_ids.push(file_id.into());
58 self
59 }
60
61 pub fn file_ids(mut self, file_ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
64 self.file_ids = file_ids.into_iter().map(Into::into).collect();
65 self
66 }
67
68 pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
71 self.extra.insert(name.into(), value);
72 self
73 }
74
75 pub fn build(self) -> Result<CreateContainerRequest, LingerError> {
78 validate_non_empty_values("file_ids", &self.file_ids, false)?;
79 validate_extra_fields(&self.extra)?;
80 Ok(CreateContainerRequest {
81 name: required_string("name", self.name)?,
82 file_ids: self.file_ids,
83 extra: self.extra,
84 })
85 }
86}
87
88#[derive(Clone, Debug, PartialEq, Eq)]
91#[non_exhaustive]
92pub struct CreateContainerFileRequest {
93 pub file_id: Option<String>,
96 pub file: Option<ContainerFileUpload>,
99}
100
101impl CreateContainerFileRequest {
102 pub fn builder() -> CreateContainerFileRequestBuilder {
105 CreateContainerFileRequestBuilder::default()
106 }
107
108 pub(crate) fn apply_body(&self, request: &mut HttpRequest) -> Result<(), LingerError> {
109 match (&self.file_id, &self.file) {
110 (Some(file_id), None) => {
111 request.insert_header("content-type", "application/json");
112 request.set_body(serde_json::to_vec(&ContainerFileIdBody { file_id })?);
113 }
114 (None, Some(file)) => {
115 let boundary = multipart_boundary(&file.content);
116 request.insert_header(
117 "content-type",
118 format!("multipart/form-data; boundary={boundary}"),
119 );
120 request.set_body_stream(file.multipart_stream(boundary));
121 }
122 _ => {
123 return Err(LingerError::invalid_config(
124 "exactly one of file_id or file is required",
125 ));
126 }
127 }
128 Ok(())
129 }
130}
131
132#[derive(Serialize)]
133struct ContainerFileIdBody<'a> {
134 file_id: &'a str,
135}
136
137#[derive(Clone, Debug, Default)]
140#[non_exhaustive]
141pub struct CreateContainerFileRequestBuilder {
142 file_id: Option<String>,
143 file: Option<ContainerFileUpload>,
144}
145
146impl CreateContainerFileRequestBuilder {
147 pub fn file_id(mut self, file_id: impl Into<String>) -> Self {
150 self.file_id = Some(file_id.into());
151 self
152 }
153
154 pub fn file(mut self, file: ContainerFileUpload) -> Self {
157 self.file = Some(file);
158 self
159 }
160
161 pub fn build(self) -> Result<CreateContainerFileRequest, LingerError> {
164 match (&self.file_id, &self.file) {
165 (Some(file_id), None) if !file_id.trim().is_empty() => {}
166 (Some(_), None) => return Err(LingerError::invalid_config("file_id is required")),
167 (None, Some(_)) => {}
168 (None, None) => {
169 return Err(LingerError::invalid_config(
170 "exactly one of file_id or file is required",
171 ));
172 }
173 (Some(_), Some(_)) => {
174 return Err(LingerError::invalid_config(
175 "file_id and file are mutually exclusive",
176 ));
177 }
178 }
179 Ok(CreateContainerFileRequest {
180 file_id: self.file_id,
181 file: self.file,
182 })
183 }
184}
185
186#[derive(Clone, Debug, PartialEq, Eq)]
189#[non_exhaustive]
190pub struct ContainerFileUpload {
191 pub filename: String,
194 pub content_type: String,
197 content: Bytes,
198}
199
200impl ContainerFileUpload {
201 pub fn from_bytes(
204 filename: impl Into<String>,
205 content: impl Into<Bytes>,
206 ) -> Result<Self, LingerError> {
207 let filename = filename.into();
208 validate_header_param("filename", &filename)?;
209 Ok(Self {
210 filename,
211 content_type: "application/octet-stream".to_string(),
212 content: content.into(),
213 })
214 }
215
216 pub fn content_type(mut self, content_type: impl Into<String>) -> Result<Self, LingerError> {
219 let content_type = content_type.into();
220 validate_header_value("content_type", &content_type)?;
221 self.content_type = content_type;
222 Ok(self)
223 }
224
225 pub fn bytes(&self) -> Bytes {
228 self.content.clone()
229 }
230
231 fn multipart_stream(
232 &self,
233 boundary: String,
234 ) -> impl futures_core::Stream<Item = Result<Bytes, LingerError>> {
235 let chunks = vec![
236 Ok(Bytes::from(format!(
237 "--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
238 escape_multipart_param(&self.filename),
239 self.content_type
240 ))),
241 Ok(self.content.clone()),
242 Ok(Bytes::from(format!("\r\n--{boundary}--\r\n"))),
243 ];
244 futures_util::stream::iter(chunks)
245 }
246}
247
248#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
251#[non_exhaustive]
252pub struct Container {
253 pub id: String,
256 pub object: String,
259 pub created_at: u64,
262 pub name: String,
265 pub status: String,
268 #[serde(default)]
271 pub expires_after: Option<Value>,
272 #[serde(default)]
275 pub last_active_at: Option<u64>,
276 #[serde(default)]
279 pub memory_limit: Option<String>,
280 #[serde(default)]
283 pub network_policy: Option<Value>,
284 #[serde(flatten)]
287 pub extra: BTreeMap<String, Value>,
288 #[serde(skip)]
291 request_id: Option<RequestId>,
292}
293
294impl Container {
295 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
296 self.request_id = request_id;
297 self
298 }
299
300 pub fn request_id(&self) -> Option<&RequestId> {
303 self.request_id.as_ref()
304 }
305}
306
307#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
310#[non_exhaustive]
311pub struct ContainerPage {
312 pub object: String,
315 #[serde(default)]
318 pub data: Vec<Container>,
319 #[serde(default)]
322 pub first_id: Option<String>,
323 #[serde(default)]
326 pub last_id: Option<String>,
327 pub has_more: bool,
330 #[serde(skip)]
333 request_id: Option<RequestId>,
334}
335
336impl ContainerPage {
337 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
338 self.request_id = request_id;
339 self
340 }
341
342 pub fn request_id(&self) -> Option<&RequestId> {
345 self.request_id.as_ref()
346 }
347}
348
349#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
352#[non_exhaustive]
353pub struct ContainerDeletion {
354 pub id: String,
357 pub object: String,
360 pub deleted: bool,
363 #[serde(skip)]
366 request_id: Option<RequestId>,
367}
368
369impl ContainerDeletion {
370 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
371 self.request_id = request_id;
372 self
373 }
374
375 pub fn request_id(&self) -> Option<&RequestId> {
378 self.request_id.as_ref()
379 }
380}
381
382#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
385#[non_exhaustive]
386pub struct ContainerFile {
387 pub id: String,
390 pub object: String,
393 pub created_at: u64,
396 pub bytes: u64,
399 pub container_id: String,
402 pub path: String,
405 pub source: String,
408 #[serde(flatten)]
411 pub extra: BTreeMap<String, Value>,
412 #[serde(skip)]
415 request_id: Option<RequestId>,
416}
417
418impl ContainerFile {
419 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
420 self.request_id = request_id;
421 self
422 }
423
424 pub fn request_id(&self) -> Option<&RequestId> {
427 self.request_id.as_ref()
428 }
429}
430
431#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
434#[non_exhaustive]
435pub struct ContainerFilePage {
436 pub object: String,
439 #[serde(default)]
442 pub data: Vec<ContainerFile>,
443 #[serde(default)]
446 pub first_id: Option<String>,
447 #[serde(default)]
450 pub last_id: Option<String>,
451 pub has_more: bool,
454 #[serde(skip)]
457 request_id: Option<RequestId>,
458}
459
460impl ContainerFilePage {
461 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
462 self.request_id = request_id;
463 self
464 }
465
466 pub fn request_id(&self) -> Option<&RequestId> {
469 self.request_id.as_ref()
470 }
471}
472
473#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
476#[non_exhaustive]
477pub struct ContainerFileDeletion {
478 pub id: String,
481 pub object: String,
484 pub deleted: bool,
487 #[serde(skip)]
490 request_id: Option<RequestId>,
491}
492
493impl ContainerFileDeletion {
494 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
495 self.request_id = request_id;
496 self
497 }
498
499 pub fn request_id(&self) -> Option<&RequestId> {
502 self.request_id.as_ref()
503 }
504}
505
506pub struct ContainerFileContent {
509 request_id: Option<RequestId>,
510 body: BodyStream,
511}
512
513impl fmt::Debug for ContainerFileContent {
514 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
515 f.debug_struct("ContainerFileContent")
516 .field("request_id", &self.request_id)
517 .field("body", &"<stream>")
518 .finish()
519 }
520}
521
522impl ContainerFileContent {
523 pub(crate) fn new(request_id: Option<RequestId>, body: BodyStream) -> Self {
524 Self { request_id, body }
525 }
526
527 pub fn request_id(&self) -> Option<&RequestId> {
530 self.request_id.as_ref()
531 }
532
533 pub fn into_stream(self) -> BodyStream {
536 self.body
537 }
538}
539
540fn required_string(name: &str, value: Option<String>) -> Result<String, LingerError> {
541 value
542 .filter(|value| !value.trim().is_empty())
543 .ok_or_else(|| LingerError::invalid_config(format!("{name} is required")))
544}
545
546fn validate_non_empty_values(
547 name: &str,
548 values: &[String],
549 require_non_empty: bool,
550) -> Result<(), LingerError> {
551 if require_non_empty && values.is_empty() {
552 return Err(LingerError::invalid_config(format!("{name} is required")));
553 }
554 if values.iter().any(|value| value.trim().is_empty()) {
555 return Err(LingerError::invalid_config(format!(
556 "{name} must not contain empty values"
557 )));
558 }
559 Ok(())
560}
561
562fn validate_extra_fields(extra: &BTreeMap<String, Value>) -> Result<(), LingerError> {
563 for (key, value) in extra {
564 if key.trim().is_empty() {
565 return Err(LingerError::invalid_config(
566 "extra field names must not be empty",
567 ));
568 }
569 if value.is_null() {
570 return Err(LingerError::invalid_config(format!(
571 "extra field {key} must not be null"
572 )));
573 }
574 }
575 Ok(())
576}
577
578fn multipart_boundary(content: &Bytes) -> String {
579 for counter in 0.. {
580 let boundary = format!("linger-openai-sdk-boundary-{counter}");
581 if !contains_bytes(content, boundary.as_bytes()) {
582 return boundary;
583 }
584 }
585 unreachable!("unbounded boundary counter")
586}
587
588fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
589 if needle.is_empty() {
590 return true;
591 }
592 haystack
593 .windows(needle.len())
594 .any(|window| window == needle)
595}
596
597fn validate_header_param(name: &str, value: &str) -> Result<(), LingerError> {
598 if value.trim().is_empty() {
599 return Err(LingerError::invalid_config(format!("{name} is required")));
600 }
601 validate_header_value(name, value)
602}
603
604fn validate_header_value(name: &str, value: &str) -> Result<(), LingerError> {
605 if value.contains('\r') || value.contains('\n') {
606 return Err(LingerError::invalid_config(format!(
607 "{name} must not contain CR or LF"
608 )));
609 }
610 Ok(())
611}
612
613fn escape_multipart_param(value: &str) -> String {
614 value.replace('\\', "\\\\").replace('"', "\\\"")
615}