Skip to main content

treetop_client/types/
version.rs

1//! Version and policy version types.
2
3use serde::{Deserialize, Serialize};
4
5/// Identifies the policy and label state used for authorization.
6///
7/// Every authorization response includes a `PolicyVersion` so callers can verify
8/// which policy snapshot was used for evaluation.
9///
10/// Displays as `"{hash} (loaded {loaded_at})"`.
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
12pub struct PolicyVersion {
13    /// SHA-256 hash of the policy source content.
14    pub hash: String,
15    /// ISO 8601 timestamp of when these policies were loaded.
16    pub loaded_at: String,
17    /// Stable label configuration identifier, when supplied by the server.
18    /// Required on the wire; explicit null means no configured label identifier.
19    #[serde(deserialize_with = "deserialize_label_set")]
20    pub label_set: Option<String>,
21    /// Generation within one engine instance; this can restart on replacement.
22    /// Required on the wire; no old-server default is inferred.
23    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/// Version information for the Treetop core library (Cedar engine).
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
40pub struct Core {
41    /// The treetop-core library version.
42    pub version: String,
43    /// The Cedar policy engine version.
44    pub cedar: String,
45}
46
47/// Identifies loaded schema content, separately from an authorization generation.
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49pub struct SchemaVersion {
50    /// SHA-256 hash of the schema source.
51    pub hash: String,
52    /// ISO 8601 timestamp of when the schema was loaded.
53    pub loaded_at: String,
54}
55
56/// Full version information returned by the `/api/v1/version` endpoint.
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
58pub struct VersionInfo {
59    /// The treetop-rest server version.
60    pub version: String,
61    /// Core library and Cedar version details.
62    pub core: Core,
63    /// The policy version currently loaded in the server.
64    pub policies: PolicyVersion,
65    /// The schema version currently loaded in the server, if any.
66    #[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}