use crate::ids::{BundleId, DeploymentId, PackId, RevisionId};
use crate::version::{SchemaVersion, SemVer};
use chrono::{DateTime, Utc};
use greentic_types::EnvId;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RevisionLifecycle {
Inactive,
Staged,
Warming,
Ready,
Draining,
Failed,
Archived,
}
pub fn is_valid_transition(from: RevisionLifecycle, to: RevisionLifecycle) -> bool {
use RevisionLifecycle::*;
matches!(
(from, to),
(Inactive, Staged)
| (Inactive, Failed)
| (Inactive, Archived)
| (Staged, Warming)
| (Staged, Failed)
| (Staged, Archived)
| (Warming, Ready)
| (Warming, Failed)
| (Warming, Archived)
| (Ready, Draining)
| (Ready, Failed)
| (Ready, Archived)
| (Draining, Inactive)
| (Failed, Staged)
| (Failed, Archived)
)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackListEntry {
pub pack_id: PackId,
pub version: SemVer,
pub digest: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_uri: Option<String>,
}
impl PackListEntry {
pub fn from_lock_primitives(pack_id: PackId, digest: String) -> Self {
Self {
pack_id,
version: SemVer::new(0, 0, 0),
digest,
source_uri: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Revision {
pub schema: SchemaVersion,
pub revision_id: RevisionId,
pub env_id: EnvId,
pub bundle_id: BundleId,
pub deployment_id: DeploymentId,
pub sequence: u64,
pub created_at: DateTime<Utc>,
pub bundle_digest: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bundle_source_uri: Option<String>,
pub pack_list: Vec<PackListEntry>,
pub pack_list_lock_ref: PathBuf,
#[serde(default)]
pub pack_config_refs: Vec<PathBuf>,
pub config_digest: String,
pub signature_sidecar_ref: PathBuf,
pub lifecycle: RevisionLifecycle,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub staged_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub warmed_at: Option<DateTime<Utc>>,
#[serde(default)]
pub drain_seconds: u32,
#[serde(default)]
pub abort_metrics: Vec<String>,
}
impl Revision {
pub fn schema_str() -> &'static str {
SchemaVersion::REVISION_V1
}
pub fn validate(&self) -> Result<(), crate::error::SpecError> {
if self.schema.as_str() != SchemaVersion::REVISION_V1 {
return Err(crate::error::SpecError::SchemaMismatch {
expected: SchemaVersion::REVISION_V1,
actual: self.schema.as_str().to_string(),
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal() -> Revision {
Revision {
schema: SchemaVersion::new(SchemaVersion::REVISION_V1),
revision_id: RevisionId::new(),
env_id: EnvId::try_from("local").unwrap(),
bundle_id: BundleId::new("demo"),
deployment_id: DeploymentId::new(),
sequence: 1,
created_at: Utc::now(),
bundle_digest: "sha256:00".to_string(),
bundle_source_uri: None,
pack_list: Vec::new(),
pack_list_lock_ref: PathBuf::from("pack-list.lock"),
pack_config_refs: Vec::new(),
config_digest: "sha256:00".to_string(),
signature_sidecar_ref: PathBuf::from("rev.sig"),
lifecycle: RevisionLifecycle::Staged,
staged_at: None,
warmed_at: None,
drain_seconds: 0,
abort_metrics: Vec::new(),
}
}
#[test]
fn bundle_source_uri_is_omitted_when_none_and_round_trips_when_some() {
let none = minimal();
let json = serde_json::to_value(&none).unwrap();
assert!(
json.get("bundle_source_uri").is_none(),
"None must omit the key for wire compatibility: {json}"
);
assert_eq!(serde_json::from_value::<Revision>(json).unwrap(), none);
let some = Revision {
bundle_source_uri: Some("oci://ghcr.io/greenticai/bundles/demo@sha256:abc".to_string()),
..minimal()
};
let rt: Revision = serde_json::from_str(&serde_json::to_string(&some).unwrap()).unwrap();
assert_eq!(rt, some);
}
}