use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct PolicyVersion {
pub hash: String,
pub loaded_at: String,
#[serde(deserialize_with = "deserialize_label_set")]
pub label_set: Option<String>,
pub generation: u64,
}
fn deserialize_label_set<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<String>, D::Error> {
Option::<String>::deserialize(deserializer)
}
impl std::fmt::Display for PolicyVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} (loaded {})", self.hash, self.loaded_at)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Core {
pub version: String,
pub cedar: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SchemaVersion {
pub hash: String,
pub loaded_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct VersionInfo {
pub version: String,
pub core: Core,
pub policies: PolicyVersion,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<SchemaVersion>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_info_roundtrip() {
let json = serde_json::json!({
"version": "0.1.0",
"core": {
"version": "0.3.0",
"cedar": "0.11.0"
},
"policies": {
"hash": "abc123",
"loaded_at": "2025-01-01T00:00:00Z",
"label_set": "labels-v2",
"generation": 7}
});
let info: VersionInfo = serde_json::from_value(json.clone()).unwrap();
assert_eq!(info.version, "0.1.0");
assert_eq!(info.core.cedar, "0.11.0");
assert_eq!(info.policies.hash, "abc123");
assert_eq!(info.policies.label_set.as_deref(), Some("labels-v2"));
assert_eq!(info.policies.generation, 7);
assert!(info.schema.is_none());
let reserialized = serde_json::to_value(&info).unwrap();
assert_eq!(json, reserialized);
}
#[test]
fn version_info_with_schema() {
let json = serde_json::json!({
"version": "0.1.0",
"core": {
"version": "0.3.0",
"cedar": "0.11.0"
},
"policies": {
"hash": "abc123",
"loaded_at": "2025-01-01T00:00:00Z", "label_set": null, "generation": 0},
"schema": {
"hash": "schema123",
"loaded_at": "2025-01-01T00:00:01Z"}
});
let info: VersionInfo = serde_json::from_value(json).unwrap();
assert_eq!(
info.schema.as_ref().map(|v| v.hash.as_str()),
Some("schema123")
);
assert_eq!(info.policies.label_set, None);
assert_eq!(info.policies.generation, 0);
}
#[test]
fn policy_version_requires_every_current_field() {
let complete =
serde_json::json!({"hash":"h","loaded_at":"t","label_set":null,"generation":0});
for field in ["hash", "loaded_at", "label_set", "generation"] {
let mut incomplete = complete.clone();
incomplete.as_object_mut().unwrap().remove(field);
assert!(
serde_json::from_value::<PolicyVersion>(incomplete).is_err(),
"{field}"
);
}
assert!(serde_json::from_value::<PolicyVersion>(complete).is_ok());
}
#[test]
fn generation_accepts_only_unsigned_integers() {
for generation in [
serde_json::json!(-1),
serde_json::json!(true),
serde_json::json!(1.5),
serde_json::json!("1"),
serde_json::json!(null),
] {
let value = serde_json::json!({
"hash": "hash", "loaded_at": "2026-09-05T00:00:00Z",
"label_set": null, "generation": generation});
assert!(serde_json::from_value::<PolicyVersion>(value).is_err());
}
let value = serde_json::json!({
"hash": "hash", "loaded_at": "2026-09-05T00:00:00Z",
"label_set": null, "generation": u64::MAX});
let version: PolicyVersion = serde_json::from_value(value.clone()).unwrap();
assert_eq!(version.generation, u64::MAX);
assert_eq!(serde_json::to_value(version).unwrap(), value);
}
}