Skip to main content

zai_rs/file/
response.rs

1use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
2use validator::Validate;
3
4/// Response for files listing
5#[derive(Debug, Clone, Serialize, Validate)]
6pub struct FileListResponse {
7    /// Response type: expected "list"
8    #[serde(skip_serializing_if = "Option::is_none")]
9    pub object: Option<FileListObject>,
10    /// File entries
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub data: Option<Vec<FileObject>>,
13    /// Whether there are more results
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub has_more: Option<bool>,
16}
17
18/// Object marker returned by the file-list endpoint.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum FileListObject {
22    /// A list response.
23    List,
24}
25
26#[derive(Deserialize)]
27struct FileListResponseWire {
28    object: Option<FileListObject>,
29    data: Option<Vec<FileObject>>,
30    has_more: Option<bool>,
31}
32
33impl<'de> Deserialize<'de> for FileListResponse {
34    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
35    where
36        D: Deserializer<'de>,
37    {
38        let wire = FileListResponseWire::deserialize(deserializer)?;
39        if wire.object.is_none() && wire.data.is_none() && wire.has_more.is_none() {
40            return Err(D::Error::custom(
41                "file-list response contained no documented non-null fields",
42            ));
43        }
44        Ok(Self {
45            object: wire.object,
46            data: wire.data,
47            has_more: wire.has_more,
48        })
49    }
50}
51
52/// File metadata object (as returned by list/upload APIs)
53#[derive(Debug, Clone, Serialize, Validate)]
54pub struct FileObject {
55    /// Unique file identifier
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub id: Option<String>,
58    /// Object type: expected "file"
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub object: Option<FileObjectKind>,
61    /// File size in bytes
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub bytes: Option<u64>,
64    /// UNIX timestamp of creation
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub created_at: Option<u64>,
67    /// Original filename
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub filename: Option<String>,
70    /// Purpose string (e.g., batch, file-extract, ...)
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub purpose: Option<String>,
73}
74
75/// Response returned by the file-upload operation.
76pub type FileUploadResponse = FileObject;
77
78/// Object marker returned for one file resource.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
80#[serde(rename_all = "lowercase")]
81pub enum FileObjectKind {
82    /// A file resource.
83    File,
84}
85
86#[derive(Deserialize)]
87struct FileObjectWire {
88    id: Option<String>,
89    object: Option<FileObjectKind>,
90    bytes: Option<u64>,
91    created_at: Option<u64>,
92    filename: Option<String>,
93    purpose: Option<String>,
94}
95
96impl<'de> Deserialize<'de> for FileObject {
97    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
98    where
99        D: Deserializer<'de>,
100    {
101        let wire = FileObjectWire::deserialize(deserializer)?;
102        if wire.id.is_none()
103            && wire.object.is_none()
104            && wire.bytes.is_none()
105            && wire.created_at.is_none()
106            && wire.filename.is_none()
107            && wire.purpose.is_none()
108        {
109            return Err(D::Error::custom(
110                "file response contained no documented non-null fields",
111            ));
112        }
113        Ok(Self {
114            id: wire.id,
115            object: wire.object,
116            bytes: wire.bytes,
117            created_at: wire.created_at,
118            filename: wire.filename,
119            purpose: wire.purpose,
120        })
121    }
122}
123
124/// Response for file deletion (DELETE /files/{file_id})
125#[derive(Debug, Clone, Serialize, Validate)]
126pub struct FileDeleteResponse {
127    /// Deleted resource id
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub id: Option<String>,
130    /// Resource type: expected "file"
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub object: Option<FileObjectKind>,
133    /// Whether deletion succeeded
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub deleted: Option<bool>,
136}
137
138#[derive(Deserialize)]
139struct FileDeleteResponseWire {
140    id: Option<String>,
141    object: Option<FileObjectKind>,
142    deleted: Option<bool>,
143}
144
145impl<'de> Deserialize<'de> for FileDeleteResponse {
146    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147    where
148        D: Deserializer<'de>,
149    {
150        let wire = FileDeleteResponseWire::deserialize(deserializer)?;
151        if wire.id.is_none() && wire.object.is_none() && wire.deleted.is_none() {
152            return Err(D::Error::custom(
153                "file-delete response contained no documented non-null fields",
154            ));
155        }
156        Ok(Self {
157            id: wire.id,
158            object: wire.object,
159            deleted: wire.deleted,
160        })
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn file_responses_reject_empty_success_objects() {
170        assert!(serde_json::from_str::<FileListResponse>("{}").is_err());
171        assert!(serde_json::from_str::<FileListResponse>(r#"{"data":[]}"#).is_ok());
172        assert!(serde_json::from_str::<FileObject>("{}").is_err());
173        assert!(serde_json::from_str::<FileObject>(r#"{"bytes":0}"#).is_ok());
174        assert!(serde_json::from_str::<FileDeleteResponse>("{}").is_err());
175        assert!(serde_json::from_str::<FileDeleteResponse>(r#"{"deleted":false}"#).is_ok());
176    }
177
178    #[test]
179    fn file_object_markers_are_closed_enums() {
180        assert!(serde_json::from_str::<FileListResponse>(r#"{"object":"list"}"#).is_ok());
181        assert!(serde_json::from_str::<FileListResponse>(r#"{"object":"future"}"#).is_err());
182        assert!(serde_json::from_str::<FileObject>(r#"{"object":"file"}"#).is_ok());
183        assert!(serde_json::from_str::<FileObject>(r#"{"object":"future"}"#).is_err());
184    }
185}