oas_document/
governance.rs1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct GovernanceSection {
43 pub current_mhr: String,
45
46 pub epoch: u64,
49
50 #[serde(default)]
52 pub transitions: Vec<GovernanceTransition>,
53
54 pub policy: GovernancePolicy,
56}
57
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct GovernanceTransition {
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub from: Option<String>,
68
69 pub to: String,
71
72 pub epoch: u64,
74
75 pub proof: GovernanceTransitionProof,
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct GovernanceTransitionProof {
86 #[serde(rename = "type")]
88 pub proof_type: String,
89
90 pub mhr_threshold_signature: String,
92
93 pub timestamp: String,
95}
96
97pub const ENR_GOVERNANCE_PROOF_TYPE: &str = "EnrGovernanceTransitionProof2025";
99
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct GovernancePolicy {
104 pub transition_requires: String,
106
107 pub signing_key_rotation_requires: String,
109
110 #[serde(default = "default_max_transitions")]
112 pub max_transitions_per_day: u32,
113
114 #[serde(default = "default_timelock")]
116 pub timelock_hours: u32,
117}
118
119fn default_max_transitions() -> u32 {
120 1
121}
122
123fn default_timelock() -> u32 {
124 24
125}
126
127impl GovernancePolicy {
128 pub fn default_policy() -> Self {
130 Self {
131 transition_requires: "mhr_threshold".to_string(),
132 signing_key_rotation_requires: "mhr_threshold".to_string(),
133 max_transitions_per_day: default_max_transitions(),
134 timelock_hours: default_timelock(),
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn governance_section_round_trip() {
145 let section = GovernanceSection {
146 current_mhr: "did:oas:prod:mhr:council-alpha".to_string(),
147 epoch: 3,
148 transitions: vec![
149 GovernanceTransition {
150 from: None,
151 to: "did:oas:prod:mhr:council-alpha".to_string(),
152 epoch: 1,
153 proof: GovernanceTransitionProof {
154 proof_type: ENR_GOVERNANCE_PROOF_TYPE.to_string(),
155 mhr_threshold_signature: "sig-initial".to_string(),
156 timestamp: "2026-01-01T00:00:00Z".to_string(),
157 },
158 },
159 GovernanceTransition {
160 from: Some("did:oas:prod:mhr:council-alpha".to_string()),
161 to: "did:oas:prod:mhr:council-beta".to_string(),
162 epoch: 2,
163 proof: GovernanceTransitionProof {
164 proof_type: ENR_GOVERNANCE_PROOF_TYPE.to_string(),
165 mhr_threshold_signature: "sig-transition-2".to_string(),
166 timestamp: "2026-02-01T00:00:00Z".to_string(),
167 },
168 },
169 ],
170 policy: GovernancePolicy::default_policy(),
171 };
172
173 let json = serde_json::to_string_pretty(§ion);
174 assert!(json.is_ok());
175 let json = json.unwrap_or_default();
176
177 let parsed: Result<GovernanceSection, _> = serde_json::from_str(&json);
178 assert!(parsed.is_ok());
179 let parsed = parsed.unwrap_or_else(|_| section.clone());
180 assert_eq!(parsed.epoch, 3);
181 assert_eq!(parsed.transitions.len(), 2);
182 }
183
184 #[test]
185 fn governance_policy_defaults() {
186 let policy = GovernancePolicy::default_policy();
187 assert_eq!(policy.max_transitions_per_day, 1);
188 assert_eq!(policy.timelock_hours, 24);
189 assert_eq!(policy.transition_requires, "mhr_threshold");
190 }
191
192 #[test]
193 fn governance_proof_type_constant() {
194 assert_eq!(
195 ENR_GOVERNANCE_PROOF_TYPE,
196 "EnrGovernanceTransitionProof2025"
197 );
198 }
199
200 #[test]
201 fn empty_transitions_deserializes() {
202 let json = r#"{
203 "currentMhr": "did:oas:test:mhr:council",
204 "epoch": 1,
205 "policy": {
206 "transitionRequires": "mhr_threshold",
207 "signingKeyRotationRequires": "mhr_threshold"
208 }
209 }"#;
210 let section: Result<GovernanceSection, _> = serde_json::from_str(json);
211 assert!(section.is_ok());
212 let section = section.unwrap_or_else(|_| GovernanceSection {
213 current_mhr: String::new(),
214 epoch: 0,
215 transitions: vec![],
216 policy: GovernancePolicy::default_policy(),
217 });
218 assert!(section.transitions.is_empty());
219 assert_eq!(section.policy.max_transitions_per_day, 1);
220 assert_eq!(section.policy.timelock_hours, 24);
221 }
222}