1use aion_core::{Event, WorkflowStatus};
27
28#[derive(Clone, Debug, Default, Eq, PartialEq)]
30pub struct SubscriptionSelector {
31 pub workflow_type: Option<String>,
33 pub status: Option<WorkflowStatus>,
35}
36
37impl SubscriptionSelector {
38 #[must_use]
41 pub const fn unrestricted() -> Self {
42 Self {
43 workflow_type: None,
44 status: None,
45 }
46 }
47
48 #[must_use]
51 pub fn matches(&self, event: &Event, workflow_type: Option<&str>) -> bool {
52 if let Some(selected_type) = &self.workflow_type {
53 if workflow_type != Some(selected_type.as_str()) {
56 return false;
57 }
58 }
59 if let Some(selected_status) = self.status {
60 if event_status(event) != selected_status {
61 return false;
62 }
63 }
64 true
65 }
66}
67
68const fn event_status(event: &Event) -> WorkflowStatus {
72 match event {
73 Event::WorkflowCompleted { .. } => WorkflowStatus::Completed,
74 Event::WorkflowFailed { .. } => WorkflowStatus::Failed,
75 Event::WorkflowCancelled { .. } => WorkflowStatus::Cancelled,
76 Event::WorkflowTimedOut { .. } => WorkflowStatus::TimedOut,
77 Event::WorkflowContinuedAsNew { .. } => WorkflowStatus::ContinuedAsNew,
78 Event::WorkflowPaused { .. } => WorkflowStatus::Paused,
80 Event::WorkflowStarted { .. }
81 | Event::WorkflowReopened { .. }
83 | Event::WorkflowResumed { .. }
85 | Event::SearchAttributesUpdated { .. }
86 | Event::ActivityScheduled { .. }
87 | Event::ActivityStarted { .. }
88 | Event::ActivityCompleted { .. }
89 | Event::ActivityFailed { .. }
90 | Event::ActivityCancelled { .. }
91 | Event::TimerStarted { .. }
92 | Event::TimerFired { .. }
93 | Event::TimerCancelled { .. }
94 | Event::WithTimeoutCompleted { .. }
95 | Event::SignalReceived { .. }
96 | Event::SignalSent { .. }
97 | Event::ChildWorkflowStarted { .. }
98 | Event::ChildWorkflowCompleted { .. }
99 | Event::ChildWorkflowFailed { .. }
100 | Event::ChildWorkflowCancelled { .. }
101 | Event::ScheduleCreated { .. }
102 | Event::ScheduleUpdated { .. }
103 | Event::SchedulePaused { .. }
104 | Event::ScheduleResumed { .. }
105 | Event::ScheduleDeleted { .. }
106 | Event::ScheduleTriggered { .. } => WorkflowStatus::Running,
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use aion_core::{Event, EventEnvelope, Payload, WorkflowId, WorkflowStatus};
113
114 use super::SubscriptionSelector;
115
116 fn envelope(seq: u64) -> EventEnvelope {
117 EventEnvelope {
118 seq,
119 recorded_at: chrono::Utc::now(),
120 workflow_id: WorkflowId::new(uuid::Uuid::from_u128(1)),
121 }
122 }
123
124 fn payload() -> Result<Payload, aion_core::PayloadError> {
125 Payload::from_json(&serde_json::json!({ "label": "x" }))
126 }
127
128 fn signal(seq: u64) -> Result<Event, aion_core::PayloadError> {
129 Ok(Event::SignalReceived {
130 envelope: envelope(seq),
131 name: "ship".to_owned(),
132 payload: payload()?,
133 })
134 }
135
136 fn started(seq: u64) -> Result<Event, aion_core::PayloadError> {
137 Ok(Event::WorkflowStarted {
138 envelope: envelope(seq),
139 workflow_type: "checkout".to_owned(),
140 input: payload()?,
141 run_id: aion_core::RunId::new(uuid::Uuid::from_u128(1)),
142 parent_run_id: None,
143 package_version: aion_core::PackageVersion::new("a".repeat(64)),
144 })
145 }
146
147 fn completed(seq: u64) -> Result<Event, aion_core::PayloadError> {
148 Ok(Event::WorkflowCompleted {
149 envelope: envelope(seq),
150 result: payload()?,
151 })
152 }
153
154 fn failed(seq: u64) -> Event {
155 Event::WorkflowFailed {
156 envelope: envelope(seq),
157 error: aion_core::WorkflowError {
158 message: "boom".to_owned(),
159 details: None,
160 },
161 }
162 }
163
164 #[test]
165 fn unrestricted_selector_matches_everything() -> Result<(), Box<dyn std::error::Error>> {
166 let selector = SubscriptionSelector::unrestricted();
167
168 assert!(selector.matches(&signal(1)?, None));
169 assert!(selector.matches(&completed(2)?, Some("checkout")));
170 Ok(())
171 }
172
173 #[test]
174 fn type_selector_matches_only_the_recorded_type() -> Result<(), Box<dyn std::error::Error>> {
175 let selector = SubscriptionSelector {
176 workflow_type: Some("checkout".to_owned()),
177 status: None,
178 };
179
180 assert!(selector.matches(&signal(1)?, Some("checkout")));
181 assert!(!selector.matches(&signal(1)?, Some("fulfillment")));
182 assert!(
183 !selector.matches(&signal(1)?, None),
184 "a workflow with no recorded type never matches a type selector"
185 );
186 Ok(())
187 }
188
189 #[test]
190 fn status_selector_matches_per_event_kind() -> Result<(), Box<dyn std::error::Error>> {
191 let running = SubscriptionSelector {
192 workflow_type: None,
193 status: Some(WorkflowStatus::Running),
194 };
195 let completed_only = SubscriptionSelector {
196 workflow_type: None,
197 status: Some(WorkflowStatus::Completed),
198 };
199 let failed_only = SubscriptionSelector {
200 workflow_type: None,
201 status: Some(WorkflowStatus::Failed),
202 };
203
204 assert!(running.matches(&started(1)?, Some("checkout")));
206 assert!(running.matches(&signal(2)?, Some("checkout")));
207 assert!(!running.matches(&completed(3)?, Some("checkout")));
208
209 assert!(completed_only.matches(&completed(3)?, Some("checkout")));
211 assert!(!completed_only.matches(&failed(3), Some("checkout")));
212 assert!(!completed_only.matches(&signal(2)?, Some("checkout")));
213 assert!(failed_only.matches(&failed(3), Some("checkout")));
214 assert!(!failed_only.matches(&completed(3)?, Some("checkout")));
215 Ok(())
216 }
217
218 fn timed_out(seq: u64) -> Event {
219 Event::WorkflowTimedOut {
220 envelope: envelope(seq),
221 timeout: "workflow".to_owned(),
222 }
223 }
224
225 #[test]
226 fn status_selector_projects_workflow_timed_out_to_timed_out()
227 -> Result<(), Box<dyn std::error::Error>> {
228 let timed_out_only = SubscriptionSelector {
233 workflow_type: None,
234 status: Some(WorkflowStatus::TimedOut),
235 };
236 let running = SubscriptionSelector {
237 workflow_type: None,
238 status: Some(WorkflowStatus::Running),
239 };
240 let failed_only = SubscriptionSelector {
241 workflow_type: None,
242 status: Some(WorkflowStatus::Failed),
243 };
244
245 assert!(timed_out_only.matches(&timed_out(4), Some("checkout")));
246 assert!(!timed_out_only.matches(&completed(3)?, Some("checkout")));
247 assert!(!running.matches(&timed_out(4), Some("checkout")));
248 assert!(!failed_only.matches(&timed_out(4), Some("checkout")));
249 Ok(())
250 }
251
252 #[test]
253 fn combined_selectors_and_together() -> Result<(), Box<dyn std::error::Error>> {
254 let selector = SubscriptionSelector {
255 workflow_type: Some("checkout".to_owned()),
256 status: Some(WorkflowStatus::Completed),
257 };
258
259 assert!(selector.matches(&completed(3)?, Some("checkout")));
260 assert!(
261 !selector.matches(&completed(3)?, Some("fulfillment")),
262 "matching status with mismatched type must not pass"
263 );
264 assert!(
265 !selector.matches(&signal(2)?, Some("checkout")),
266 "matching type with mismatched status must not pass"
267 );
268 Ok(())
269 }
270}