angzarr_client/proto_ext/cover.rs
1//! Cover extension trait and implementations.
2//!
3//! Provides convenient accessors for domain, correlation_id, and root_id
4//! from Cover-bearing types.
5
6use crate::proto::{CommandBook, Cover, Edition, EventBook, Query};
7
8use super::constants::{DEFAULT_EDITION, UNKNOWN_DOMAIN};
9
10/// Extension trait for types with an optional Cover.
11///
12/// Provides convenient accessors for domain, correlation_id, and root_id
13/// without verbose `.cover.as_ref().map(...)` chains.
14pub trait CoverExt {
15 /// Get the cover, if present.
16 fn cover(&self) -> Option<&Cover>;
17
18 /// Get the domain from the cover, or [`UNKNOWN_DOMAIN`] if missing.
19 fn domain(&self) -> &str {
20 self.cover()
21 .map(|c| c.domain.as_str())
22 .unwrap_or(UNKNOWN_DOMAIN)
23 }
24
25 /// Get the correlation_id from the cover, or empty string if missing.
26 fn correlation_id(&self) -> &str {
27 self.cover()
28 .map(|c| c.correlation_id.as_str())
29 .unwrap_or("")
30 }
31
32 /// Get the root UUID as a hex-encoded string, if present.
33 fn root_id_hex(&self) -> Option<String> {
34 self.cover()
35 .and_then(|c| c.root.as_ref())
36 .map(|u| hex::encode(&u.value))
37 }
38
39 /// Get the root UUID, if present.
40 fn root_uuid(&self) -> Option<uuid::Uuid> {
41 self.cover()
42 .and_then(|c| c.root.as_ref())
43 .and_then(|u| uuid::Uuid::from_slice(&u.value).ok())
44 }
45
46 /// Check if correlation_id is present and non-empty.
47 fn has_correlation_id(&self) -> bool {
48 !self.correlation_id().is_empty()
49 }
50
51 /// Get the edition name from the cover.
52 ///
53 /// Returns the explicit edition name if set and non-empty, otherwise
54 /// defaults to the canonical timeline name (`"angzarr"`).
55 fn edition(&self) -> &str {
56 self.cover()
57 .and_then(|c| c.edition.as_ref())
58 .map(|e| e.name.as_str())
59 .filter(|e| !e.is_empty())
60 .unwrap_or(DEFAULT_EDITION)
61 }
62
63 /// Get the Edition struct from the cover, if present.
64 fn edition_struct(&self) -> Option<&crate::proto::Edition> {
65 self.cover().and_then(|c| c.edition.as_ref())
66 }
67
68 /// Get the edition name as an Option, without defaulting.
69 ///
70 /// Returns `Some(&str)` if edition is set and non-empty, `None` otherwise.
71 fn edition_opt(&self) -> Option<&str> {
72 self.cover()
73 .and_then(|c| c.edition.as_ref())
74 .map(|e| e.name.as_str())
75 .filter(|n| !n.is_empty())
76 }
77
78 /// Compute the bus routing key: `"{domain}"`.
79 ///
80 /// The routing key is a transport concern used for bus subscription matching.
81 /// Edition filtering is handled at the handler level, not the bus level.
82 fn routing_key(&self) -> String {
83 self.domain().to_string()
84 }
85
86 /// Generate a cache key for this entity based on edition + domain + root.
87 ///
88 /// Used for caching aggregate state during saga retry to avoid redundant fetches.
89 /// Includes edition to prevent collision between aggregates in different timelines.
90 fn cache_key(&self) -> String {
91 let edition = self.edition();
92 let domain = self.domain();
93 let root = self.root_id_hex().unwrap_or_default();
94 format!("{edition}:{domain}:{root}")
95 }
96}
97
98impl CoverExt for EventBook {
99 fn cover(&self) -> Option<&Cover> {
100 self.cover.as_ref()
101 }
102}
103
104impl CoverExt for CommandBook {
105 fn cover(&self) -> Option<&Cover> {
106 self.cover.as_ref()
107 }
108}
109
110impl CoverExt for Query {
111 fn cover(&self) -> Option<&Cover> {
112 self.cover.as_ref()
113 }
114}
115
116impl CoverExt for Cover {
117 fn cover(&self) -> Option<&Cover> {
118 Some(self)
119 }
120}
121
122impl Cover {
123 /// Stamp edition onto this cover if not already set.
124 ///
125 /// Sets the edition to the given name with no divergences if the cover's
126 /// edition is None or has an empty name. Used by sagas and process managers
127 /// to propagate source edition to outgoing covers and commands.
128 pub fn stamp_edition_if_empty(&mut self, edition: &str) {
129 if self.edition.as_ref().is_none_or(|e| e.name.is_empty()) {
130 self.edition = Some(Edition {
131 name: edition.to_string(),
132 divergences: vec![],
133 });
134 }
135 }
136}