use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::client::Client;
use crate::error::ApiError;
use crate::models::Ref;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Operation {
Download,
Upload,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ObjectSpec {
pub oid: String,
pub size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BatchRequest {
pub operation: Operation,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub transfers: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#ref: Option<Ref>,
pub objects: Vec<ObjectSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash_algo: Option<String>,
}
impl BatchRequest {
pub fn new(operation: Operation, objects: Vec<ObjectSpec>) -> Self {
Self {
operation,
transfers: Vec::new(),
r#ref: None,
objects,
hash_algo: None,
}
}
pub fn with_transfers(mut self, transfers: impl IntoIterator<Item = String>) -> Self {
self.transfers = transfers.into_iter().collect();
self
}
pub fn with_ref(mut self, r: Ref) -> Self {
self.r#ref = Some(r);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BatchResponse {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transfer: Option<String>,
pub objects: Vec<ObjectResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash_algo: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ObjectResult {
pub oid: String,
#[serde(default)]
pub size: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authenticated: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actions: Option<Actions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<ObjectError>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ObjectError {
pub code: u32,
pub message: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct Actions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub download: Option<Action>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upload: Option<Action>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verify: Option<Action>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Action {
pub href: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub header: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_in: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
}
impl Client {
pub async fn batch(&self, req: &BatchRequest) -> Result<BatchResponse, ApiError> {
self.post_json("objects/batch", req).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn operation_serializes_lowercase() {
let s = serde_json::to_string(&Operation::Download).unwrap();
assert_eq!(s, "\"download\"");
}
#[test]
fn request_skips_empty_optional_fields() {
let req = BatchRequest::new(
Operation::Download,
vec![ObjectSpec { oid: "abc".into(), size: 10 }],
);
let v = serde_json::to_value(&req).unwrap();
assert!(v.get("transfers").is_none());
assert!(v.get("ref").is_none());
assert!(v.get("hash_algo").is_none());
}
#[test]
fn parses_canonical_download_response() {
let body = r#"{
"transfer": "basic",
"objects": [{
"oid": "1111111",
"size": 123,
"authenticated": true,
"actions": {
"download": {
"href": "https://some-download.com",
"header": { "Key": "value" },
"expires_at": "2016-11-10T15:29:07Z"
}
}
}],
"hash_algo": "sha256"
}"#;
let resp: BatchResponse = serde_json::from_str(body).unwrap();
assert_eq!(resp.transfer.as_deref(), Some("basic"));
let obj = &resp.objects[0];
assert_eq!(obj.authenticated, Some(true));
let action = obj.actions.as_ref().unwrap().download.as_ref().unwrap();
assert_eq!(action.href, "https://some-download.com");
assert_eq!(action.header.get("Key").unwrap(), "value");
assert!(action.expires_in.is_none());
}
#[test]
fn parses_per_object_error() {
let body = r#"{
"transfer": "basic",
"objects": [{
"oid": "1111111", "size": 123,
"error": { "code": 404, "message": "Object does not exist" }
}]
}"#;
let resp: BatchResponse = serde_json::from_str(body).unwrap();
let err = resp.objects[0].error.as_ref().unwrap();
assert_eq!(err.code, 404);
assert_eq!(err.message, "Object does not exist");
}
#[test]
fn parses_upload_already_present_no_actions() {
let body = r#"{
"objects": [{ "oid": "1111111", "size": 123 }]
}"#;
let resp: BatchResponse = serde_json::from_str(body).unwrap();
let obj = &resp.objects[0];
assert!(obj.actions.is_none());
assert!(obj.error.is_none());
}
}