use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use url::Url;
use crate::metadata::ItemMetadata;
use crate::serde_util::deserialize_option_u64ish;
use crate::{ItemIdentifier, TaskId};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Item {
#[serde(default, deserialize_with = "deserialize_option_u64ish")]
pub created: Option<u64>,
#[serde(default)]
pub d1: Option<String>,
#[serde(default)]
pub d2: Option<String>,
#[serde(default)]
pub dir: Option<String>,
#[serde(default)]
pub files: Vec<ItemFile>,
#[serde(default, deserialize_with = "deserialize_option_u64ish")]
pub files_count: Option<u64>,
#[serde(default, deserialize_with = "deserialize_option_u64ish")]
pub item_last_updated: Option<u64>,
#[serde(default, deserialize_with = "deserialize_option_u64ish")]
pub item_size: Option<u64>,
#[serde(default)]
pub metadata: ItemMetadata,
#[serde(default)]
pub server: Option<String>,
#[serde(default, deserialize_with = "deserialize_option_u64ish")]
pub uniq: Option<u64>,
#[serde(default)]
pub workable_servers: Vec<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
impl Item {
#[must_use]
pub fn identifier(&self) -> Option<ItemIdentifier> {
self.metadata
.get_text("identifier")
.and_then(|value| ItemIdentifier::new(value).ok())
}
#[must_use]
pub fn file(&self, name: &str) -> Option<&ItemFile> {
self.files.iter().find(|file| file.name == name)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ItemFile {
pub name: String,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub format: Option<String>,
#[serde(default, deserialize_with = "deserialize_option_u64ish")]
pub mtime: Option<u64>,
#[serde(default, deserialize_with = "deserialize_option_u64ish")]
pub size: Option<u64>,
#[serde(default)]
pub md5: Option<String>,
#[serde(default)]
pub crc32: Option<String>,
#[serde(default)]
pub sha1: Option<String>,
#[serde(default)]
pub original: Option<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MetadataWriteResponse {
pub success: bool,
#[serde(default)]
pub task_id: Option<TaskId>,
#[serde(default)]
pub log: Option<Url>,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct SearchResponseHeader {
#[serde(default)]
pub status: i64,
#[serde(default)]
#[serde(rename = "QTime")]
pub q_time: Option<i64>,
#[serde(default)]
pub params: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SearchResultPage {
#[serde(rename = "numFound")]
pub num_found: u64,
pub start: u64,
#[serde(default)]
pub docs: Vec<SearchDocument>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SearchResponse {
#[serde(default)]
#[serde(rename = "responseHeader")]
pub response_header: SearchResponseHeader,
pub response: SearchResultPage,
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SearchDocument(BTreeMap<String, Value>);
impl SearchDocument {
#[must_use]
pub fn get(&self, key: &str) -> Option<&Value> {
self.0.get(key)
}
#[must_use]
pub fn get_text(&self, key: &str) -> Option<&str> {
self.get(key).and_then(Value::as_str)
}
#[must_use]
pub fn identifier(&self) -> Option<ItemIdentifier> {
self.get_text("identifier")
.and_then(|value| ItemIdentifier::new(value).ok())
}
#[must_use]
pub fn title(&self) -> Option<&str> {
self.get_text("title")
}
#[must_use]
pub fn as_map(&self) -> &BTreeMap<String, Value> {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct S3LimitCheck {
pub bucket: String,
pub accesskey: String,
pub over_limit: i64,
#[serde(default)]
pub detail: Option<Value>,
}
#[cfg(test)]
mod tests {
use super::{Item, SearchResponse};
#[test]
fn item_deserializes_realistic_metadata_payloads() {
let item: Item = serde_json::from_value(serde_json::json!({
"created": 1_776_513_537,
"files": [
{
"name": "xfetch.pdf",
"size": 419_170,
"md5": "abc"
}
],
"metadata": {
"identifier": "xfetch",
"title": "XFETCH"
}
}))
.unwrap();
assert_eq!(item.file("xfetch.pdf").unwrap().size, Some(419_170));
assert_eq!(item.identifier().unwrap().as_str(), "xfetch");
}
#[test]
fn search_response_deserializes_advancedsearch_shape() {
let response: SearchResponse = serde_json::from_value(serde_json::json!({
"responseHeader": {
"status": 0,
"QTime": 12,
"params": { "query": "identifier:xfetch" }
},
"response": {
"numFound": 1,
"start": 0,
"docs": [
{
"identifier": "xfetch",
"title": "XFETCH"
}
]
}
}))
.unwrap();
assert_eq!(
response.response.docs[0].identifier().unwrap().as_str(),
"xfetch"
);
assert_eq!(response.response.docs[0].title(), Some("XFETCH"));
assert_eq!(
response.response.docs[0].as_map()["title"],
serde_json::Value::String("XFETCH".to_owned())
);
}
#[test]
fn search_response_deserializes_without_response_header() {
let response: SearchResponse = serde_json::from_value(serde_json::json!({
"response": {
"numFound": 1,
"start": 0,
"docs": [
{
"identifier": "xfetch",
"title": "XFETCH"
}
]
}
}))
.unwrap();
assert_eq!(response.response_header.status, 0);
assert!(response.response_header.params.is_empty());
assert_eq!(
response.response.docs[0].identifier().unwrap().as_str(),
"xfetch"
);
}
}