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::ActivityAdvisoryExhausted { .. }
93 | Event::ActivityCancelled { .. }
94 | Event::TimerStarted { .. }
95 | Event::TimerFired { .. }
96 | Event::TimerCancelled { .. }
97 | Event::WithTimeoutCompleted { .. }
98 | Event::SignalReceived { .. }
99 | Event::SignalSent { .. }
100 | Event::ChildWorkflowStarted { .. }
101 | Event::ChildWorkflowCompleted { .. }
102 | Event::ChildWorkflowFailed { .. }
103 | Event::ChildWorkflowCancelled { .. }
104 | Event::ScheduleCreated { .. }
105 | Event::ScheduleUpdated { .. }
106 | Event::SchedulePaused { .. }
107 | Event::ScheduleResumed { .. }
108 | Event::ScheduleDeleted { .. }
109 | Event::ScheduleTriggered { .. } => WorkflowStatus::Running,
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use aion_core::{Event, EventEnvelope, Payload, WorkflowId, WorkflowStatus};
116
117 use super::SubscriptionSelector;
118
119 fn envelope(seq: u64) -> EventEnvelope {
120 EventEnvelope {
121 seq,
122 recorded_at: chrono::Utc::now(),
123 workflow_id: WorkflowId::new(uuid::Uuid::from_u128(1)),
124 }
125 }
126
127 fn payload() -> Result<Payload, aion_core::PayloadError> {
128 Payload::from_json(&serde_json::json!({ "label": "x" }))
129 }
130
131 fn signal(seq: u64) -> Result<Event, aion_core::PayloadError> {
132 Ok(Event::SignalReceived {
133 envelope: envelope(seq),
134 name: "ship".to_owned(),
135 payload: payload()?,
136 })
137 }
138
139 fn started(seq: u64) -> Result<Event, aion_core::PayloadError> {
140 Ok(Event::WorkflowStarted {
141 envelope: envelope(seq),
142 workflow_type: "checkout".to_owned(),
143 input: payload()?,
144 run_id: aion_core::RunId::new(uuid::Uuid::from_u128(1)),
145 parent_run_id: None,
146 package_version: aion_core::PackageVersion::new("a".repeat(64)),
147 })
148 }
149
150 fn completed(seq: u64) -> Result<Event, aion_core::PayloadError> {
151 Ok(Event::WorkflowCompleted {
152 envelope: envelope(seq),
153 result: payload()?,
154 })
155 }
156
157 fn failed(seq: u64) -> Event {
158 Event::WorkflowFailed {
159 envelope: envelope(seq),
160 error: aion_core::WorkflowError {
161 message: "boom".to_owned(),
162 details: None,
163 },
164 }
165 }
166
167 #[test]
168 fn unrestricted_selector_matches_everything() -> Result<(), Box<dyn std::error::Error>> {
169 let selector = SubscriptionSelector::unrestricted();
170
171 assert!(selector.matches(&signal(1)?, None));
172 assert!(selector.matches(&completed(2)?, Some("checkout")));
173 Ok(())
174 }
175
176 #[test]
177 fn type_selector_matches_only_the_recorded_type() -> Result<(), Box<dyn std::error::Error>> {
178 let selector = SubscriptionSelector {
179 workflow_type: Some("checkout".to_owned()),
180 status: None,
181 };
182
183 assert!(selector.matches(&signal(1)?, Some("checkout")));
184 assert!(!selector.matches(&signal(1)?, Some("fulfillment")));
185 assert!(
186 !selector.matches(&signal(1)?, None),
187 "a workflow with no recorded type never matches a type selector"
188 );
189 Ok(())
190 }
191
192 #[test]
193 fn status_selector_matches_per_event_kind() -> Result<(), Box<dyn std::error::Error>> {
194 let running = SubscriptionSelector {
195 workflow_type: None,
196 status: Some(WorkflowStatus::Running),
197 };
198 let completed_only = SubscriptionSelector {
199 workflow_type: None,
200 status: Some(WorkflowStatus::Completed),
201 };
202 let failed_only = SubscriptionSelector {
203 workflow_type: None,
204 status: Some(WorkflowStatus::Failed),
205 };
206
207 assert!(running.matches(&started(1)?, Some("checkout")));
209 assert!(running.matches(&signal(2)?, Some("checkout")));
210 assert!(!running.matches(&completed(3)?, Some("checkout")));
211
212 assert!(completed_only.matches(&completed(3)?, Some("checkout")));
214 assert!(!completed_only.matches(&failed(3), Some("checkout")));
215 assert!(!completed_only.matches(&signal(2)?, Some("checkout")));
216 assert!(failed_only.matches(&failed(3), Some("checkout")));
217 assert!(!failed_only.matches(&completed(3)?, Some("checkout")));
218 Ok(())
219 }
220
221 fn timed_out(seq: u64) -> Event {
222 Event::WorkflowTimedOut {
223 envelope: envelope(seq),
224 timeout: "workflow".to_owned(),
225 }
226 }
227
228 #[test]
229 fn status_selector_projects_workflow_timed_out_to_timed_out()
230 -> Result<(), Box<dyn std::error::Error>> {
231 let timed_out_only = SubscriptionSelector {
236 workflow_type: None,
237 status: Some(WorkflowStatus::TimedOut),
238 };
239 let running = SubscriptionSelector {
240 workflow_type: None,
241 status: Some(WorkflowStatus::Running),
242 };
243 let failed_only = SubscriptionSelector {
244 workflow_type: None,
245 status: Some(WorkflowStatus::Failed),
246 };
247
248 assert!(timed_out_only.matches(&timed_out(4), Some("checkout")));
249 assert!(!timed_out_only.matches(&completed(3)?, Some("checkout")));
250 assert!(!running.matches(&timed_out(4), Some("checkout")));
251 assert!(!failed_only.matches(&timed_out(4), Some("checkout")));
252 Ok(())
253 }
254
255 #[test]
256 fn combined_selectors_and_together() -> Result<(), Box<dyn std::error::Error>> {
257 let selector = SubscriptionSelector {
258 workflow_type: Some("checkout".to_owned()),
259 status: Some(WorkflowStatus::Completed),
260 };
261
262 assert!(selector.matches(&completed(3)?, Some("checkout")));
263 assert!(
264 !selector.matches(&completed(3)?, Some("fulfillment")),
265 "matching status with mismatched type must not pass"
266 );
267 assert!(
268 !selector.matches(&signal(2)?, Some("checkout")),
269 "matching type with mismatched status must not pass"
270 );
271 Ok(())
272 }
273}