use std::num::NonZeroUsize;
use reqwest::multipart::Part;
use serde::{Deserialize, Deserializer, Serialize};
use crate::KitError;
use crate::core::content::base64_bytes;
use crate::error::ValidationViolation;
use crate::generation::FileReference;
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct FileUploadRequest {
file_name: String,
media_type: String,
#[serde(with = "base64_bytes")]
data: Vec<u8>,
}
impl FileUploadRequest {
pub fn new(
file_name: impl Into<String>,
media_type: impl Into<String>,
data: Vec<u8>,
) -> Result<Self, KitError> {
let file_name = file_name.into();
let media_type = media_type.into();
if file_name.is_empty()
|| file_name.trim() != file_name
|| file_name.chars().any(char::is_control)
{
return Err(KitError::invalid_input(
ValidationViolation::InvalidFormat {
field: "file.name",
rule: "nonblank, unpadded, without control characters",
},
"upload file name must be nonblank, unpadded, and contain no control characters",
));
}
let parameters_valid = media_type.split(';').skip(1).all(|parameter| {
parameter
.split_once('=')
.is_some_and(|(name, value)| !name.trim().is_empty() && !value.trim().is_empty())
});
if media_type.is_empty()
|| media_type.trim() != media_type
|| media_type.chars().any(char::is_control)
|| !parameters_valid
|| Part::bytes(Vec::new()).mime_str(&media_type).is_err()
{
return Err(KitError::invalid_input(
ValidationViolation::InvalidFormat {
field: "file.media_type",
rule: "valid type/subtype",
},
"upload media type must be a valid type/subtype",
));
}
if data.is_empty() {
return Err(KitError::invalid_input(
ValidationViolation::Empty { field: "file.data" },
"upload data must not be empty",
));
}
Ok(Self {
file_name,
media_type,
data,
})
}
#[must_use]
pub fn file_name(&self) -> &str {
&self.file_name
}
#[must_use]
pub fn media_type(&self) -> &str {
&self.media_type
}
#[must_use]
pub fn data(&self) -> &[u8] {
&self.data
}
#[must_use]
pub fn into_parts(self) -> (String, String, Vec<u8>) {
(self.file_name, self.media_type, self.data)
}
}
impl<'de> Deserialize<'de> for FileUploadRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Wire {
file_name: String,
media_type: String,
#[serde(with = "base64_bytes")]
data: Vec<u8>,
}
let wire = Wire::deserialize(deserializer)?;
Self::new(wire.file_name, wire.media_type, wire.data).map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct FileListRequest {
limit: Option<NonZeroUsize>,
cursor: Option<String>,
}
impl FileListRequest {
#[must_use]
pub fn with_limit(mut self, limit: NonZeroUsize) -> Self {
self.limit = Some(limit);
self
}
pub fn with_cursor(mut self, cursor: impl Into<String>) -> Result<Self, KitError> {
let cursor = cursor.into();
if cursor.trim().is_empty() {
return Err(KitError::invalid_input(
ValidationViolation::Empty {
field: "files.cursor",
},
"file cursor must not be empty",
));
}
self.cursor = Some(cursor);
Ok(self)
}
#[must_use]
pub const fn limit(&self) -> Option<NonZeroUsize> {
self.limit
}
#[must_use]
pub fn cursor(&self) -> Option<&str> {
self.cursor.as_deref()
}
#[must_use]
pub fn into_parts(self) -> (Option<NonZeroUsize>, Option<String>) {
(self.limit, self.cursor)
}
}
impl<'de> Deserialize<'de> for FileListRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Wire {
limit: Option<NonZeroUsize>,
cursor: Option<String>,
}
let wire = Wire::deserialize(deserializer)?;
let mut request = Self {
limit: wire.limit,
cursor: None,
};
if let Some(cursor) = wire.cursor {
request = request
.with_cursor(cursor)
.map_err(serde::de::Error::custom)?;
}
Ok(request)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FileObject {
pub id: String,
pub file_name: Option<String>,
pub size_bytes: Option<u64>,
pub media_type: Option<String>,
pub created_at: Option<String>,
pub state: Option<FileState>,
pub input_reference: Option<FileReference>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum FileState {
Processing,
Active,
Failed {
code: Option<String>,
message: Option<String>,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FilePage {
pub files: Vec<FileObject>,
pub next_cursor: Option<String>,
}