treetop_client/types/
version.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
12pub struct PolicyVersion {
13 pub hash: String,
15 pub loaded_at: String,
17 #[serde(deserialize_with = "deserialize_label_set")]
20 pub label_set: Option<String>,
21 pub generation: u64,
24}
25
26fn deserialize_label_set<'de, D: serde::Deserializer<'de>>(
27 deserializer: D,
28) -> Result<Option<String>, D::Error> {
29 Option::<String>::deserialize(deserializer)
30}
31
32impl std::fmt::Display for PolicyVersion {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 write!(f, "{} (loaded {})", self.hash, self.loaded_at)
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
40pub struct Core {
41 pub version: String,
43 pub cedar: String,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49pub struct SchemaVersion {
50 pub hash: String,
52 pub loaded_at: String,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
58pub struct VersionInfo {
59 pub version: String,
61 pub core: Core,
63 pub policies: PolicyVersion,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub schema: Option<SchemaVersion>,
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn version_info_roundtrip() {
76 let json = serde_json::json!({
77 "version": "0.1.0",
78 "core": {
79 "version": "0.3.0",
80 "cedar": "0.11.0"
81 },
82 "policies": {
83 "hash": "abc123",
84 "loaded_at": "2025-01-01T00:00:00Z",
85 "label_set": "labels-v2",
86 "generation": 7}
87 });
88 let info: VersionInfo = serde_json::from_value(json.clone()).unwrap();
89 assert_eq!(info.version, "0.1.0");
90 assert_eq!(info.core.cedar, "0.11.0");
91 assert_eq!(info.policies.hash, "abc123");
92 assert_eq!(info.policies.label_set.as_deref(), Some("labels-v2"));
93 assert_eq!(info.policies.generation, 7);
94 assert!(info.schema.is_none());
95
96 let reserialized = serde_json::to_value(&info).unwrap();
97 assert_eq!(json, reserialized);
98 }
99
100 #[test]
101 fn version_info_with_schema() {
102 let json = serde_json::json!({
103 "version": "0.1.0",
104 "core": {
105 "version": "0.3.0",
106 "cedar": "0.11.0"
107 },
108 "policies": {
109 "hash": "abc123",
110 "loaded_at": "2025-01-01T00:00:00Z", "label_set": null, "generation": 0},
111 "schema": {
112 "hash": "schema123",
113 "loaded_at": "2025-01-01T00:00:01Z"}
114 });
115
116 let info: VersionInfo = serde_json::from_value(json).unwrap();
117 assert_eq!(
118 info.schema.as_ref().map(|v| v.hash.as_str()),
119 Some("schema123")
120 );
121 assert_eq!(info.policies.label_set, None);
122 assert_eq!(info.policies.generation, 0);
123 }
124
125 #[test]
126 fn policy_version_requires_every_current_field() {
127 let complete =
128 serde_json::json!({"hash":"h","loaded_at":"t","label_set":null,"generation":0});
129 for field in ["hash", "loaded_at", "label_set", "generation"] {
130 let mut incomplete = complete.clone();
131 incomplete.as_object_mut().unwrap().remove(field);
132 assert!(
133 serde_json::from_value::<PolicyVersion>(incomplete).is_err(),
134 "{field}"
135 );
136 }
137 assert!(serde_json::from_value::<PolicyVersion>(complete).is_ok());
138 }
139
140 #[test]
141 fn generation_accepts_only_unsigned_integers() {
142 for generation in [
143 serde_json::json!(-1),
144 serde_json::json!(true),
145 serde_json::json!(1.5),
146 serde_json::json!("1"),
147 serde_json::json!(null),
148 ] {
149 let value = serde_json::json!({
150 "hash": "hash", "loaded_at": "2026-09-05T00:00:00Z",
151 "label_set": null, "generation": generation});
152 assert!(serde_json::from_value::<PolicyVersion>(value).is_err());
153 }
154 let value = serde_json::json!({
155 "hash": "hash", "loaded_at": "2026-09-05T00:00:00Z",
156 "label_set": null, "generation": u64::MAX});
157 let version: PolicyVersion = serde_json::from_value(value.clone()).unwrap();
158 assert_eq!(version.generation, u64::MAX);
159 assert_eq!(serde_json::to_value(version).unwrap(), value);
160 }
161}