use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeployMeta {
#[serde(default = "crate::schema_version")]
pub version: u32,
pub created_at: u64,
pub file_count: u64,
pub total_size: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub tags: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Default)]
pub struct DeployMetaInput {
pub source: Option<String>,
pub branch: Option<String>,
pub author: Option<String>,
pub message: Option<String>,
pub tag: Option<String>,
pub tags: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HistoryEntry {
pub id: String,
pub at: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub meta: Option<DeployMeta>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeploymentList {
pub current: Option<String>,
pub deployments: Vec<HistoryEntry>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GcReport {
pub manifests_total: usize,
pub manifests_removed: usize,
pub blobs_total: usize,
pub blobs_removed: usize,
pub bytes_reclaimed: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlobMismatch {
pub key: String,
pub expected: String,
pub actual: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlobReadError {
pub key: String,
pub error: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScrubReport {
pub checked: usize,
pub mismatched: Vec<BlobMismatch>,
pub errors: Vec<BlobReadError>,
}
impl ScrubReport {
pub fn is_clean(&self) -> bool {
self.mismatched.is_empty() && self.errors.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deploy_meta_tag_and_tags_round_trip() {
let meta = DeployMeta {
version: 1,
created_at: 42,
file_count: 3,
total_size: 99,
source: Some("abc123".into()),
branch: Some("main".into()),
author: None,
message: Some("ship it".into()),
tag: Some("v1.2.3".into()),
tags: BTreeMap::from([
("env".to_string(), "prod".to_string()),
("ticket".to_string(), "ABC-123".to_string()),
]),
};
let json = serde_json::to_string(&meta).unwrap();
assert_eq!(serde_json::from_str::<DeployMeta>(&json).unwrap(), meta);
}
#[test]
fn empty_tag_and_tags_are_omitted() {
let meta = DeployMeta {
version: 1,
created_at: 1,
file_count: 0,
total_size: 0,
source: None,
branch: None,
author: None,
message: None,
tag: None,
tags: BTreeMap::new(),
};
let json = serde_json::to_string(&meta).unwrap();
assert!(
!json.contains("\"tag\""),
"empty tag/tags must not serialize: {json}"
);
assert!(
!json.contains("\"tags\""),
"empty tags must not serialize: {json}"
);
}
#[test]
fn old_records_without_tag_fields_still_deserialize() {
let json = r#"{"version":1,"created_at":7,"file_count":1,"total_size":5}"#;
let meta: DeployMeta = serde_json::from_str(json).unwrap();
assert_eq!(meta.tag, None);
assert!(meta.tags.is_empty());
}
}