use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplicationConfiguration {
#[serde(default)]
pub role: String,
pub rules: Vec<ReplicationRule>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReplicationRule {
pub id: String,
pub priority: i32,
pub status: ReplicationRuleStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<std::collections::HashMap<String, String>>,
pub destination: ReplicationDestination,
#[serde(skip_serializing_if = "Option::is_none")]
pub delete_marker_replication: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub existing_object_replication: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delete_replication: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReplicationRuleStatus {
Enabled,
Disabled,
}
impl fmt::Display for ReplicationRuleStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReplicationRuleStatus::Enabled => write!(f, "Enabled"),
ReplicationRuleStatus::Disabled => write!(f, "Disabled"),
}
}
}
impl std::str::FromStr for ReplicationRuleStatus {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"enabled" => Ok(ReplicationRuleStatus::Enabled),
"disabled" => Ok(ReplicationRuleStatus::Disabled),
_ => Err(format!("Invalid replication rule status: {s}")),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReplicationDestination {
pub bucket_arn: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub storage_class: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BucketTarget {
#[serde(rename = "sourcebucket", default)]
pub source_bucket: String,
#[serde(default)]
pub endpoint: String,
#[serde(default)]
pub credentials: Option<BucketTargetCredentials>,
#[serde(rename = "targetbucket", default)]
pub target_bucket: String,
#[serde(default)]
pub secure: bool,
#[serde(default)]
pub path: String,
#[serde(default)]
pub api: String,
#[serde(default)]
pub arn: String,
#[serde(rename = "type", default)]
pub target_type: String,
#[serde(default)]
pub region: String,
#[serde(alias = "bandwidth", default)]
pub bandwidth_limit: i64,
#[serde(rename = "replicationSync", default)]
pub replication_sync: bool,
#[serde(default)]
pub storage_class: String,
#[serde(rename = "healthCheckDuration", default)]
pub health_check_duration: u64,
#[serde(rename = "disableProxy", default)]
pub disable_proxy: bool,
#[serde(rename = "isOnline", default)]
pub online: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BucketTargetCredentials {
#[serde(rename = "accessKey")]
pub access_key: String,
#[serde(rename = "secretKey")]
pub secret_key: String,
}
impl fmt::Display for ReplicationRule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} (priority={}, {})",
self.id, self.priority, self.status
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_replication_rule_status_display() {
assert_eq!(ReplicationRuleStatus::Enabled.to_string(), "Enabled");
assert_eq!(ReplicationRuleStatus::Disabled.to_string(), "Disabled");
}
#[test]
fn test_replication_rule_status_from_str() {
assert_eq!(
"enabled".parse::<ReplicationRuleStatus>().unwrap(),
ReplicationRuleStatus::Enabled
);
assert!("invalid".parse::<ReplicationRuleStatus>().is_err());
}
#[test]
fn test_replication_configuration_serialization() {
let config = ReplicationConfiguration {
role: "arn:aws:iam::123456789:role/replication".to_string(),
rules: vec![ReplicationRule {
id: "rule-1".to_string(),
priority: 1,
status: ReplicationRuleStatus::Enabled,
prefix: Some("data/".to_string()),
tags: None,
destination: ReplicationDestination {
bucket_arn: "arn:aws:s3:::dest-bucket".to_string(),
storage_class: None,
},
delete_marker_replication: Some(true),
existing_object_replication: Some(true),
delete_replication: None,
}],
};
let json = serde_json::to_string_pretty(&config).unwrap();
let decoded: ReplicationConfiguration = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.rules.len(), 1);
assert_eq!(decoded.rules[0].id, "rule-1");
assert_eq!(decoded.rules[0].priority, 1);
}
#[test]
fn test_bucket_target_serialization() {
let target = BucketTarget {
source_bucket: "my-bucket".to_string(),
endpoint: "http://remote:9000".to_string(),
credentials: Some(BucketTargetCredentials {
access_key: "admin".to_string(),
secret_key: "secret".to_string(),
}),
target_bucket: "dest-bucket".to_string(),
secure: false,
target_type: "replication".to_string(),
region: "us-east-1".to_string(),
replication_sync: true,
..Default::default()
};
let json = serde_json::to_string(&target).unwrap();
assert!(json.contains("sourcebucket"));
assert!(json.contains("targetbucket"));
assert!(json.contains("replicationSync"));
let decoded: BucketTarget = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.source_bucket, "my-bucket");
assert_eq!(decoded.target_bucket, "dest-bucket");
assert!(decoded.replication_sync);
}
#[test]
fn test_bucket_target_deserialization_from_backend() {
let json = r#"{"sourcebucket":"src","endpoint":"http://host:9000","credentials":{"accessKey":"ak","secretKey":"sk"},"targetbucket":"dst","secure":false,"path":"","api":"","arn":"arn:rustfs:replication::id:dst","type":"replication","region":"","bandwidth":0,"replicationSync":false,"storage_class":"","healthCheckDuration":0,"disableProxy":false,"isOnline":true}"#;
let target: BucketTarget = serde_json::from_str(json).unwrap();
assert_eq!(target.source_bucket, "src");
assert_eq!(target.target_bucket, "dst");
assert!(target.online);
assert_eq!(target.target_type, "replication");
}
}