1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use url::Url;
8
9use crate::metadata::ItemMetadata;
10use crate::serde_util::deserialize_option_u64ish;
11use crate::{ItemIdentifier, TaskId};
12
13#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
15pub struct Item {
16 #[serde(default, deserialize_with = "deserialize_option_u64ish")]
18 pub created: Option<u64>,
19 #[serde(default)]
21 pub d1: Option<String>,
22 #[serde(default)]
24 pub d2: Option<String>,
25 #[serde(default)]
27 pub dir: Option<String>,
28 #[serde(default)]
30 pub files: Vec<ItemFile>,
31 #[serde(default, deserialize_with = "deserialize_option_u64ish")]
33 pub files_count: Option<u64>,
34 #[serde(default, deserialize_with = "deserialize_option_u64ish")]
36 pub item_last_updated: Option<u64>,
37 #[serde(default, deserialize_with = "deserialize_option_u64ish")]
39 pub item_size: Option<u64>,
40 #[serde(default)]
42 pub metadata: ItemMetadata,
43 #[serde(default)]
45 pub server: Option<String>,
46 #[serde(default, deserialize_with = "deserialize_option_u64ish")]
48 pub uniq: Option<u64>,
49 #[serde(default)]
51 pub workable_servers: Vec<String>,
52 #[serde(default, flatten)]
54 pub extra: BTreeMap<String, Value>,
55}
56
57impl Item {
58 #[must_use]
60 pub fn identifier(&self) -> Option<ItemIdentifier> {
61 self.metadata
62 .get_text("identifier")
63 .and_then(|value| ItemIdentifier::new(value).ok())
64 }
65
66 #[must_use]
68 pub fn file(&self, name: &str) -> Option<&ItemFile> {
69 self.files.iter().find(|file| file.name == name)
70 }
71}
72
73#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
75pub struct ItemFile {
76 pub name: String,
78 #[serde(default)]
80 pub source: Option<String>,
81 #[serde(default)]
83 pub format: Option<String>,
84 #[serde(default, deserialize_with = "deserialize_option_u64ish")]
86 pub mtime: Option<u64>,
87 #[serde(default, deserialize_with = "deserialize_option_u64ish")]
89 pub size: Option<u64>,
90 #[serde(default)]
92 pub md5: Option<String>,
93 #[serde(default)]
95 pub crc32: Option<String>,
96 #[serde(default)]
98 pub sha1: Option<String>,
99 #[serde(default)]
101 pub original: Option<String>,
102 #[serde(default, flatten)]
104 pub extra: BTreeMap<String, Value>,
105}
106
107#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
109pub struct MetadataWriteResponse {
110 pub success: bool,
112 #[serde(default)]
114 pub task_id: Option<TaskId>,
115 #[serde(default)]
117 pub log: Option<Url>,
118 #[serde(default)]
120 pub error: Option<String>,
121}
122
123#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
125pub struct SearchResponseHeader {
126 #[serde(default)]
128 pub status: i64,
129 #[serde(default)]
131 #[serde(rename = "QTime")]
132 pub q_time: Option<i64>,
133 #[serde(default)]
135 pub params: BTreeMap<String, Value>,
136}
137
138#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
140pub struct SearchResultPage {
141 #[serde(rename = "numFound")]
143 pub num_found: u64,
144 pub start: u64,
146 #[serde(default)]
148 pub docs: Vec<SearchDocument>,
149}
150
151#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
153pub struct SearchResponse {
154 #[serde(default)]
156 #[serde(rename = "responseHeader")]
157 pub response_header: SearchResponseHeader,
158 pub response: SearchResultPage,
160}
161
162#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
164#[serde(transparent)]
165pub struct SearchDocument(BTreeMap<String, Value>);
166
167impl SearchDocument {
168 #[must_use]
170 pub fn get(&self, key: &str) -> Option<&Value> {
171 self.0.get(key)
172 }
173
174 #[must_use]
176 pub fn get_text(&self, key: &str) -> Option<&str> {
177 self.get(key).and_then(Value::as_str)
178 }
179
180 #[must_use]
182 pub fn identifier(&self) -> Option<ItemIdentifier> {
183 self.get_text("identifier")
184 .and_then(|value| ItemIdentifier::new(value).ok())
185 }
186
187 #[must_use]
189 pub fn title(&self) -> Option<&str> {
190 self.get_text("title")
191 }
192
193 #[must_use]
195 pub fn as_map(&self) -> &BTreeMap<String, Value> {
196 &self.0
197 }
198}
199
200#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
202pub struct TaskSubmission {
203 pub task_id: TaskId,
205 pub log: Url,
207}
208
209#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
211pub struct S3LimitCheck {
212 pub bucket: String,
214 pub accesskey: String,
216 pub over_limit: i64,
218 #[serde(default)]
220 pub detail: Option<Value>,
221}
222
223#[cfg(test)]
224mod tests {
225 use super::{Item, SearchResponse};
226
227 #[test]
228 fn item_deserializes_realistic_metadata_payloads() {
229 let item: Item = serde_json::from_value(serde_json::json!({
230 "created": 1_776_513_537,
231 "files": [
232 {
233 "name": "xfetch.pdf",
234 "size": 419_170,
235 "md5": "abc"
236 }
237 ],
238 "metadata": {
239 "identifier": "xfetch",
240 "title": "XFETCH"
241 }
242 }))
243 .unwrap();
244
245 assert_eq!(item.file("xfetch.pdf").unwrap().size, Some(419_170));
246 assert_eq!(item.identifier().unwrap().as_str(), "xfetch");
247 }
248
249 #[test]
250 fn search_response_deserializes_advancedsearch_shape() {
251 let response: SearchResponse = serde_json::from_value(serde_json::json!({
252 "responseHeader": {
253 "status": 0,
254 "QTime": 12,
255 "params": { "query": "identifier:xfetch" }
256 },
257 "response": {
258 "numFound": 1,
259 "start": 0,
260 "docs": [
261 {
262 "identifier": "xfetch",
263 "title": "XFETCH"
264 }
265 ]
266 }
267 }))
268 .unwrap();
269
270 assert_eq!(
271 response.response.docs[0].identifier().unwrap().as_str(),
272 "xfetch"
273 );
274 assert_eq!(response.response.docs[0].title(), Some("XFETCH"));
275 assert_eq!(
276 response.response.docs[0].as_map()["title"],
277 serde_json::Value::String("XFETCH".to_owned())
278 );
279 }
280
281 #[test]
282 fn search_response_deserializes_without_response_header() {
283 let response: SearchResponse = serde_json::from_value(serde_json::json!({
284 "response": {
285 "numFound": 1,
286 "start": 0,
287 "docs": [
288 {
289 "identifier": "xfetch",
290 "title": "XFETCH"
291 }
292 ]
293 }
294 }))
295 .unwrap();
296
297 assert_eq!(response.response_header.status, 0);
298 assert!(response.response_header.params.is_empty());
299 assert_eq!(
300 response.response.docs[0].identifier().unwrap().as_str(),
301 "xfetch"
302 );
303 }
304}