1use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
2use validator::Validate;
3
4#[derive(Debug, Clone, Serialize, Validate)]
6pub struct FileListResponse {
7 #[serde(skip_serializing_if = "Option::is_none")]
9 pub object: Option<FileListObject>,
10 #[serde(skip_serializing_if = "Option::is_none")]
12 pub data: Option<Vec<FileObject>>,
13 #[serde(skip_serializing_if = "Option::is_none")]
15 pub has_more: Option<bool>,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum FileListObject {
22 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#[derive(Debug, Clone, Serialize, Validate)]
54pub struct FileObject {
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub id: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub object: Option<FileObjectKind>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub bytes: Option<u64>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub created_at: Option<u64>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub filename: Option<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub purpose: Option<String>,
73}
74
75pub type FileUploadResponse = FileObject;
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
80#[serde(rename_all = "lowercase")]
81pub enum FileObjectKind {
82 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#[derive(Debug, Clone, Serialize, Validate)]
126pub struct FileDeleteResponse {
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub id: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub object: Option<FileObjectKind>,
133 #[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}