Skip to main content

angzarr_client/proto_ext/
pages.rs

1//! Page extension traits for EventPage and CommandPage.
2//!
3//! Provides convenient accessors for sequence, type URL, and payload decoding.
4
5use crate::convert::TYPE_URL_PREFIX;
6use crate::proto::page_header::SequenceType;
7use crate::proto::{
8    AngzarrDeferredSequence, CommandPage, EventPage, ExternalDeferredSequence, MergeStrategy,
9    PageHeader,
10};
11use prost::Name;
12
13/// Extension trait for PageHeader.
14pub trait PageHeaderExt {
15    /// Get the explicit sequence number, if set.
16    /// Returns None for deferred sequences (external or angzarr).
17    fn explicit_sequence(&self) -> Option<u32>;
18
19    /// Check if this is a deferred sequence (not yet stamped).
20    fn is_deferred(&self) -> bool;
21
22    /// Get external deferred info, if present.
23    fn external_deferred(&self) -> Option<&ExternalDeferredSequence>;
24
25    /// Get angzarr deferred info (saga-produced), if present.
26    fn angzarr_deferred(&self) -> Option<&AngzarrDeferredSequence>;
27}
28
29impl PageHeaderExt for PageHeader {
30    fn explicit_sequence(&self) -> Option<u32> {
31        match &self.sequence_type {
32            Some(SequenceType::Sequence(seq)) => Some(*seq),
33            _ => None,
34        }
35    }
36
37    fn is_deferred(&self) -> bool {
38        matches!(
39            &self.sequence_type,
40            Some(SequenceType::ExternalDeferred(_)) | Some(SequenceType::AngzarrDeferred(_))
41        )
42    }
43
44    fn external_deferred(&self) -> Option<&ExternalDeferredSequence> {
45        match &self.sequence_type {
46            Some(SequenceType::ExternalDeferred(ext)) => Some(ext),
47            _ => None,
48        }
49    }
50
51    fn angzarr_deferred(&self) -> Option<&AngzarrDeferredSequence> {
52        match &self.sequence_type {
53            Some(SequenceType::AngzarrDeferred(ang)) => Some(ang),
54            _ => None,
55        }
56    }
57}
58
59/// Extension trait for EventPage proto type.
60///
61/// Provides convenient accessors for sequence, type URL, and payload decoding.
62pub trait EventPageExt {
63    /// Get the sequence number from this page.
64    /// Returns 0 for deferred sequences (not yet stamped).
65    fn sequence_num(&self) -> u32;
66
67    /// Get the page header, if present.
68    fn header(&self) -> Option<&PageHeader>;
69
70    /// Check if this page has a deferred sequence.
71    fn is_deferred(&self) -> bool;
72
73    /// Get the type URL of the event, if present.
74    fn type_url(&self) -> Option<&str>;
75
76    /// Get the raw payload bytes, if present.
77    fn payload(&self) -> Option<&[u8]>;
78
79    /// Type-safe decode using prost::Name reflection.
80    ///
81    /// Returns None if the event is missing, type URL doesn't match exactly,
82    /// or decoding fails. The expected type URL is derived from M::full_name().
83    fn decode_typed<M: prost::Message + Default + Name>(&self) -> Option<M>;
84}
85
86impl EventPageExt for EventPage {
87    fn sequence_num(&self) -> u32 {
88        self.header
89            .as_ref()
90            .and_then(|h| h.explicit_sequence())
91            .unwrap_or(0)
92    }
93
94    fn header(&self) -> Option<&PageHeader> {
95        self.header.as_ref()
96    }
97
98    fn is_deferred(&self) -> bool {
99        self.header
100            .as_ref()
101            .map(|h| h.is_deferred())
102            .unwrap_or(false)
103    }
104
105    fn type_url(&self) -> Option<&str> {
106        match &self.payload {
107            Some(crate::proto::event_page::Payload::Event(e)) => Some(e.type_url.as_str()),
108            _ => None,
109        }
110    }
111
112    fn payload(&self) -> Option<&[u8]> {
113        match &self.payload {
114            Some(crate::proto::event_page::Payload::Event(e)) => Some(e.value.as_slice()),
115            _ => None,
116        }
117    }
118
119    fn decode_typed<M: prost::Message + Default + Name>(&self) -> Option<M> {
120        let event = match &self.payload {
121            Some(crate::proto::event_page::Payload::Event(e)) => e,
122            _ => return None,
123        };
124        let expected = format!("{}{}", TYPE_URL_PREFIX, M::full_name());
125        if event.type_url != expected {
126            return None;
127        }
128        M::decode(event.value.as_slice()).ok()
129    }
130}
131
132/// Extension trait for CommandPage proto type.
133///
134/// Provides convenient accessors for sequence, type URL, and payload decoding.
135pub trait CommandPageExt {
136    /// Get the sequence number from this page.
137    /// Returns 0 for deferred sequences (not yet stamped).
138    fn sequence_num(&self) -> u32;
139
140    /// Get the page header, if present.
141    fn header(&self) -> Option<&PageHeader>;
142
143    /// Check if this page has a deferred sequence.
144    fn is_deferred(&self) -> bool;
145
146    /// Get the type URL of the command, if present.
147    fn type_url(&self) -> Option<&str>;
148
149    /// Get the raw payload bytes, if present.
150    fn payload(&self) -> Option<&[u8]>;
151
152    /// Type-safe decode using prost::Name reflection.
153    ///
154    /// Returns None if the command is missing, type URL doesn't match exactly,
155    /// or decoding fails. The expected type URL is derived from M::full_name().
156    fn decode_typed<M: prost::Message + Default + Name>(&self) -> Option<M>;
157
158    /// Get the merge strategy for this command.
159    ///
160    /// Returns the MergeStrategy enum value. Defaults to Commutative (0) if unset.
161    fn merge_strategy(&self) -> MergeStrategy;
162}
163
164impl CommandPageExt for CommandPage {
165    fn sequence_num(&self) -> u32 {
166        self.header
167            .as_ref()
168            .and_then(|h| h.explicit_sequence())
169            .unwrap_or(0)
170    }
171
172    fn header(&self) -> Option<&PageHeader> {
173        self.header.as_ref()
174    }
175
176    fn is_deferred(&self) -> bool {
177        self.header
178            .as_ref()
179            .map(|h| h.is_deferred())
180            .unwrap_or(false)
181    }
182
183    fn type_url(&self) -> Option<&str> {
184        match &self.payload {
185            Some(crate::proto::command_page::Payload::Command(c)) => Some(c.type_url.as_str()),
186            _ => None,
187        }
188    }
189
190    fn payload(&self) -> Option<&[u8]> {
191        match &self.payload {
192            Some(crate::proto::command_page::Payload::Command(c)) => Some(c.value.as_slice()),
193            _ => None,
194        }
195    }
196
197    fn decode_typed<M: prost::Message + Default + Name>(&self) -> Option<M> {
198        let command = match &self.payload {
199            Some(crate::proto::command_page::Payload::Command(c)) => c,
200            _ => return None,
201        };
202        let expected = format!("{}{}", TYPE_URL_PREFIX, M::full_name());
203        if command.type_url != expected {
204            return None;
205        }
206        M::decode(command.value.as_slice()).ok()
207    }
208
209    fn merge_strategy(&self) -> MergeStrategy {
210        MergeStrategy::try_from(self.merge_strategy).unwrap_or(MergeStrategy::MergeCommutative)
211    }
212}
213
214/// Extension trait for AngzarrDeferredSequence.
215///
216/// Provides idempotency key generation for saga-produced commands/facts.
217pub trait AngzarrDeferredSequenceExt {
218    /// Generate the composite idempotency key for logging and display.
219    ///
220    /// Format: `{source.edition}:{source.domain}:{source.root_hex}:{source_seq}`
221    ///
222    /// Example: `angzarr:order:550e8400e29b41d4a716446655440000:7`
223    fn idempotency_key(&self) -> String;
224}
225
226impl AngzarrDeferredSequenceExt for AngzarrDeferredSequence {
227    fn idempotency_key(&self) -> String {
228        use super::cover::CoverExt;
229        let source = self.source.as_ref().expect("source required");
230        format!(
231            "{}:{}:{}:{}",
232            source.edition(),
233            source.domain,
234            source.root_id_hex().unwrap_or_default(),
235            self.source_seq
236        )
237    }
238}