use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use crate::error::StorageError;
pub type StorageResult<T> = std::result::Result<T, StorageError>;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ContentDigest(String);
impl ContentDigest {
pub fn from_bytes(data: &[u8]) -> Self {
use sha2::Digest;
let mut hasher = Sha256::new();
hasher.update(data);
ContentDigest(hex::encode(hasher.finalize()))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn short(&self) -> String {
self.0.chars().take(12).collect()
}
}
impl TryFrom<String> for ContentDigest {
type Error = StorageError;
fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
if s.len() != 64 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(StorageError::InvalidDigest { digest: s });
}
Ok(ContentDigest(s.to_ascii_lowercase()))
}
}
impl std::fmt::Display for ContentDigest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[async_trait]
pub trait CasStore: Send + Sync {
async fn put(&self, data: &[u8]) -> StorageResult<ContentDigest>;
async fn get(&self, digest: &ContentDigest) -> StorageResult<Vec<u8>>;
async fn contains(&self, digest: &ContentDigest) -> StorageResult<bool>;
async fn delete(&self, digest: &ContentDigest) -> StorageResult<()>;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RunId(pub String);
impl RunId {
pub fn new() -> Self {
RunId(uuid::Uuid::new_v4().to_string())
}
}
impl Default for RunId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for RunId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunMetadata {
pub git_sha: Option<String>,
pub agent_name: String,
pub tags: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunEvent {
pub seq: u64,
pub kind: String,
pub payload: serde_json::Value,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunSummary {
pub total_events: u64,
pub final_state_digest: Option<ContentDigest>,
pub duration_ms: u64,
pub success: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum RunStatus {
Running,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunRecord {
pub run_id: RunId,
pub spec_digest: ContentDigest,
pub metadata: RunMetadata,
pub status: RunStatus,
pub summary: Option<RunSummary>,
pub created_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
}
#[async_trait]
pub trait RunLedger: Send + Sync {
async fn create_run(
&self,
spec_digest: &ContentDigest,
metadata: RunMetadata,
) -> StorageResult<RunId>;
async fn append_event(&self, run_id: &RunId, event: RunEvent) -> StorageResult<()>;
async fn complete_run(&self, run_id: &RunId, summary: RunSummary) -> StorageResult<()>;
async fn fail_run(&self, run_id: &RunId, summary: RunSummary) -> StorageResult<()>;
async fn cancel_run(&self, run_id: &RunId, summary: RunSummary) -> StorageResult<()>;
async fn get_run(&self, run_id: &RunId) -> StorageResult<RunRecord>;
async fn get_events(&self, run_id: &RunId) -> StorageResult<Vec<RunEvent>>;
async fn list_runs(&self, spec_digest: Option<&ContentDigest>)
-> StorageResult<Vec<RunRecord>>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseMetadata {
pub version_label: Option<String>,
pub promoted_by: String,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseRecord {
pub name: String,
pub spec_digest: ContentDigest,
pub metadata: ReleaseMetadata,
pub created_at: DateTime<Utc>,
}
#[async_trait]
pub trait ReleaseRegistry: Send + Sync {
async fn promote(
&self,
name: &str,
spec_digest: &ContentDigest,
metadata: ReleaseMetadata,
) -> StorageResult<ReleaseRecord>;
async fn rollback(&self, name: &str) -> StorageResult<ReleaseRecord>;
async fn current(&self, name: &str) -> StorageResult<Option<ReleaseRecord>>;
async fn history(&self, name: &str) -> StorageResult<Vec<ReleaseRecord>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_status_serialization() {
let status = RunStatus::Running;
let json = serde_json::to_string(&status).unwrap();
assert_eq!(json, "\"RUNNING\"");
let status = RunStatus::Completed;
let json = serde_json::to_string(&status).unwrap();
assert_eq!(json, "\"COMPLETED\"");
}
}