Skip to main content

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}