aion_server/stream/
selector.rs1use 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::WorkflowStarted { .. }
79 | Event::SearchAttributesUpdated { .. }
80 | Event::ActivityScheduled { .. }
81 | Event::ActivityStarted { .. }
82 | Event::ActivityCompleted { .. }
83 | Event::ActivityFailed { .. }
84 | Event::ActivityCancelled { .. }
85 | Event::TimerStarted { .. }
86 | Event::TimerFired { .. }
87 | Event::TimerCancelled { .. }
88 | Event::WithTimeoutCompleted { .. }
89 | Event::SignalReceived { .. }
90 | Event::SignalSent { .. }
91 | Event::ChildWorkflowStarted { .. }
92 | Event::ChildWorkflowCompleted { .. }
93 | Event::ChildWorkflowFailed { .. }
94 | Event::ChildWorkflowCancelled { .. }
95 | Event::ScheduleCreated { .. }
96 | Event::ScheduleUpdated { .. }
97 | Event::SchedulePaused { .. }
98 | Event::ScheduleResumed { .. }
99 | Event::ScheduleDeleted { .. }
100 | Event::ScheduleTriggered { .. } => WorkflowStatus::Running,
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use aion_core::{Event, EventEnvelope, Payload, WorkflowId, WorkflowStatus};
107
108 use super::SubscriptionSelector;
109
110 fn envelope(seq: u64) -> EventEnvelope {
111 EventEnvelope {
112 seq,
113 recorded_at: chrono::Utc::now(),
114 workflow_id: WorkflowId::new(uuid::Uuid::from_u128(1)),
115 }
116 }
117
118 fn payload() -> Result<Payload, aion_core::PayloadError> {
119 Payload::from_json(&serde_json::json!({ "label": "x" }))
120 }
121
122 fn signal(seq: u64) -> Result<Event, aion_core::PayloadError> {
123 Ok(Event::SignalReceived {
124 envelope: envelope(seq),
125 name: "ship".to_owned(),
126 payload: payload()?,
127 })
128 }
129
130 fn started(seq: u64) -> Result<Event, aion_core::PayloadError> {
131 Ok(Event::WorkflowStarted {
132 envelope: envelope(seq),
133 workflow_type: "checkout".to_owned(),
134 input: payload()?,
135 run_id: aion_core::RunId::new(uuid::Uuid::from_u128(1)),
136 parent_run_id: None,
137 package_version: aion_core::PackageVersion::new("a".repeat(64)),
138 })
139 }
140
141 fn completed(seq: u64) -> Result<Event, aion_core::PayloadError> {
142 Ok(Event::WorkflowCompleted {
143 envelope: envelope(seq),
144 result: payload()?,
145 })
146 }
147
148 fn failed(seq: u64) -> Event {
149 Event::WorkflowFailed {
150 envelope: envelope(seq),
151 error: aion_core::WorkflowError {
152 message: "boom".to_owned(),
153 details: None,
154 },
155 }
156 }
157
158 #[test]
159 fn unrestricted_selector_matches_everything() -> Result<(), Box<dyn std::error::Error>> {
160 let selector = SubscriptionSelector::unrestricted();
161
162 assert!(selector.matches(&signal(1)?, None));
163 assert!(selector.matches(&completed(2)?, Some("checkout")));
164 Ok(())
165 }
166
167 #[test]
168 fn type_selector_matches_only_the_recorded_type() -> Result<(), Box<dyn std::error::Error>> {
169 let selector = SubscriptionSelector {
170 workflow_type: Some("checkout".to_owned()),
171 status: None,
172 };
173
174 assert!(selector.matches(&signal(1)?, Some("checkout")));
175 assert!(!selector.matches(&signal(1)?, Some("fulfillment")));
176 assert!(
177 !selector.matches(&signal(1)?, None),
178 "a workflow with no recorded type never matches a type selector"
179 );
180 Ok(())
181 }
182
183 #[test]
184 fn status_selector_matches_per_event_kind() -> Result<(), Box<dyn std::error::Error>> {
185 let running = SubscriptionSelector {
186 workflow_type: None,
187 status: Some(WorkflowStatus::Running),
188 };
189 let completed_only = SubscriptionSelector {
190 workflow_type: None,
191 status: Some(WorkflowStatus::Completed),
192 };
193 let failed_only = SubscriptionSelector {
194 workflow_type: None,
195 status: Some(WorkflowStatus::Failed),
196 };
197
198 assert!(running.matches(&started(1)?, Some("checkout")));
200 assert!(running.matches(&signal(2)?, Some("checkout")));
201 assert!(!running.matches(&completed(3)?, Some("checkout")));
202
203 assert!(completed_only.matches(&completed(3)?, Some("checkout")));
205 assert!(!completed_only.matches(&failed(3), Some("checkout")));
206 assert!(!completed_only.matches(&signal(2)?, Some("checkout")));
207 assert!(failed_only.matches(&failed(3), Some("checkout")));
208 assert!(!failed_only.matches(&completed(3)?, Some("checkout")));
209 Ok(())
210 }
211
212 #[test]
213 fn combined_selectors_and_together() -> Result<(), Box<dyn std::error::Error>> {
214 let selector = SubscriptionSelector {
215 workflow_type: Some("checkout".to_owned()),
216 status: Some(WorkflowStatus::Completed),
217 };
218
219 assert!(selector.matches(&completed(3)?, Some("checkout")));
220 assert!(
221 !selector.matches(&completed(3)?, Some("fulfillment")),
222 "matching status with mismatched type must not pass"
223 );
224 assert!(
225 !selector.matches(&signal(2)?, Some("checkout")),
226 "matching type with mismatched status must not pass"
227 );
228 Ok(())
229 }
230}