1use serde::{Deserialize, Serialize};
11
12use crate::approval::{ApprovalId, Autonomy, ClusterKey, Ruling};
13use crate::error::{AxCode, AxError};
14use crate::event::scope::Scope;
15use crate::locator::B3Hash;
16use crate::registry::ResidentId;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31pub struct ApprovalResolved {
32 pub id: ApprovalId,
34 pub verdict: Ruling,
36 pub cluster: ClusterKey,
40}
41
42pub mod autonomy_word {
51 use super::{Autonomy, AxCode, AxError, ResidentId};
52
53 const DELEGATE: &str = "delegate";
55
56 const OWNER: &str = "owner";
58
59 const AT: char = ':';
61
62 #[must_use]
64 pub fn spell(autonomy: &Autonomy) -> String {
65 match autonomy {
66 Autonomy::Owner => OWNER.to_owned(),
67 Autonomy::Delegate(resident) => {
68 format!("{DELEGATE}{AT}{resident}", resident = resident.as_str())
69 }
70 }
71 }
72
73 pub fn read(word: &str) -> Result<Autonomy, AxError> {
82 let unreadable = |detail: &str| {
83 AxError::failure(AxCode::WireMismatch, "read who answers for a scope", detail)
84 .with_recovery(
85 "open this city with the build that wrote its history: this one knows \
86 `owner` and `delegate:<resident>`",
87 )
88 };
89 if word == OWNER {
90 return Ok(Autonomy::Owner);
91 }
92 match word.split_once(AT) {
93 Some((DELEGATE, resident)) => ResidentId::new(resident)
94 .map(Autonomy::Delegate)
95 .ok_or_else(|| unreadable(resident)),
96 Some(_) | None => Err(unreadable(word)),
97 }
98 }
99
100 pub(super) fn serialize<S: serde::Serializer>(
101 autonomy: &Autonomy,
102 serializer: S,
103 ) -> Result<S::Ok, S::Error> {
104 serializer.serialize_str(&spell(autonomy))
105 }
106
107 pub(super) fn deserialize<'de, D: serde::Deserializer<'de>>(
108 deserializer: D,
109 ) -> Result<Autonomy, D::Error> {
110 let word = <String as serde::Deserialize>::deserialize(deserializer)?;
111 read(&word).map_err(serde::de::Error::custom)
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
124pub struct AutonomyChanged {
125 #[serde(default = "city_wide")]
129 #[cfg_attr(feature = "schema", schemars(with = "String"))]
130 pub scope: Scope,
131 #[serde(with = "autonomy_word")]
132 #[cfg_attr(feature = "schema", schemars(with = "String"))]
133 pub autonomy: Autonomy,
134}
135
136fn city_wide() -> Scope {
138 Scope::City
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(rename_all = "snake_case")]
149#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
150pub enum Admittance {
151 Halted,
153 Released,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
164pub struct CityHalted {
165 #[cfg_attr(feature = "schema", schemars(with = "String"))]
166 pub scope: Scope,
167 pub state: Admittance,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
177pub struct GovernedDocumentWritten {
178 pub which: String,
180 pub bytes: usize,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
187 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
188 pub naming: Option<B3Hash>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
201pub struct RulesChanged {
202 #[cfg_attr(feature = "schema", schemars(with = "String"))]
204 pub scope: Scope,
205 pub which: GoverningDocument,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
211 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
212 pub before: Option<B3Hash>,
213 #[cfg_attr(feature = "schema", schemars(with = "String"))]
215 pub after: B3Hash,
216 pub bytes: usize,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
223#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
224pub enum GoverningDocument {
225 #[serde(rename = "RULES.toml")]
227 Rules,
228 #[serde(rename = "CONFIG.toml")]
230 Config,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
242pub struct SpineDocumentWritten {
243 pub building: String,
245 pub which: String,
247 pub bytes: usize,
249}
250
251#[cfg(test)]
252#[allow(
253 clippy::unwrap_used,
254 clippy::expect_used,
255 clippy::panic,
256 clippy::indexing_slicing,
257 reason = "test code"
258)]
259mod tests {
260 use super::*;
261 use crate::address::Address;
262 use crate::approval::ApprovalClass;
263 use crate::event::Payload;
264
265 #[test]
268 fn an_answered_item_writes_the_keys_the_histories_already_hold() {
269 let answered = ApprovalResolved {
270 id: ApprovalId::new("ap-7").unwrap(),
271 verdict: Ruling::Allow,
272 cluster: ClusterKey {
273 class: ApprovalClass::Question,
274 detail: "ask the person".to_owned(),
275 },
276 };
277 let payload = Payload::of(&answered).unwrap();
278 assert_eq!(
279 serde_json::to_string(payload.as_map()).unwrap(),
280 r#"{"cluster":{"class":"question","detail":"ask the person"},"id":"ap-7","verdict":"allow"}"#
281 );
282 assert_eq!(payload.read::<ApprovalResolved>().unwrap(), answered);
283 let denied = ApprovalResolved {
284 verdict: Ruling::Deny,
285 ..answered
286 };
287 assert!(
288 serde_json::to_string(Payload::of(&denied).unwrap().as_map())
289 .unwrap()
290 .contains(r#""verdict":"deny""#)
291 );
292 }
293
294 #[test]
297 fn an_appointment_this_build_cannot_read_is_refused_rather_than_defaulted() {
298 let line = serde_json::json!({"scope": "city", "autonomy": "anybody"});
299 let payload: Payload = serde_json::from_value(line).unwrap();
300 let err = payload.read::<AutonomyChanged>().unwrap_err();
301 assert_eq!(err.code(), &AxCode::WireMismatch);
302 let line = serde_json::json!({"scope": "lab", "state": "halted"});
303 let payload: Payload = serde_json::from_value(line).unwrap();
304 assert!(payload.read::<CityHalted>().is_err());
305 }
306
307 #[test]
308 fn a_verdict_this_build_does_not_know_is_refused_rather_than_defaulted() {
309 let line = serde_json::json!({
310 "id": "ap-7",
311 "verdict": "maybe",
312 "cluster": {"class": "question", "detail": "d"},
313 });
314 let payload: Payload = serde_json::from_value(line).unwrap();
315 assert!(payload.read::<ApprovalResolved>().is_err());
316 }
317
318 #[test]
319 fn the_three_standing_records_keep_their_keys() {
320 let halted = Payload::of(&CityHalted {
321 scope: Scope::Building(Address::parse("lab").unwrap()),
322 state: Admittance::Halted,
323 })
324 .unwrap();
325 assert_eq!(
326 serde_json::to_string(halted.as_map()).unwrap(),
327 r#"{"scope":"building:lab","state":"halted"}"#
328 );
329 let autonomy = Payload::of(&AutonomyChanged {
330 scope: Scope::City,
331 autonomy: Autonomy::Delegate(ResidentId::new("hall/clerk.md").unwrap()),
332 })
333 .unwrap();
334 assert_eq!(
335 serde_json::to_string(autonomy.as_map()).unwrap(),
336 r#"{"autonomy":"delegate:hall/clerk.md","scope":"city"}"#
337 );
338 assert_eq!(
339 autonomy.read::<AutonomyChanged>().unwrap().autonomy,
340 Autonomy::Delegate(ResidentId::new("hall/clerk.md").unwrap())
341 );
342 let written = Payload::of(&GovernedDocumentWritten {
343 which: "MAYOR.md".to_owned(),
344 bytes: 12,
345 naming: None,
346 })
347 .unwrap();
348 assert_eq!(
349 serde_json::to_string(written.as_map()).unwrap(),
350 r#"{"bytes":12,"which":"MAYOR.md"}"#
351 );
352 }
353
354 #[test]
357 fn a_rules_change_carries_the_file_name_and_two_digests_never_the_text() {
358 let opening = RulesChanged {
359 scope: Scope::Building(Address::parse("lab").unwrap()),
360 which: GoverningDocument::Rules,
361 before: None,
362 after: B3Hash::digest(b"new"),
363 bytes: 3,
364 };
365 let payload = Payload::of(&opening).unwrap();
366 let text = serde_json::to_string(payload.as_map()).unwrap();
367 assert_eq!(
368 text,
369 format!(
370 r#"{{"after":"{}","bytes":3,"scope":"building:lab","which":"RULES.toml"}}"#,
371 B3Hash::digest(b"new")
372 )
373 );
374 assert_eq!(payload.read::<RulesChanged>().unwrap(), opening);
375 let moved = RulesChanged {
376 which: GoverningDocument::Config,
377 before: Some(B3Hash::digest(b"old")),
378 ..opening
379 };
380 assert_eq!(
381 Payload::of(&moved).unwrap().read::<RulesChanged>().unwrap(),
382 moved
383 );
384 }
385}