1use std::fmt;
10use std::num::NonZeroU64;
11use std::time::SystemTime;
12
13use oxide_batch_core::{
14 DomainError, ExecutionContext, ExitCode, FlowTarget, IdentifierKind, JobExecutionId, NodeId,
15 StepExecution, StepExecutionId,
16};
17
18#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
20pub struct FlowDecisionId(NonZeroU64);
21
22impl FlowDecisionId {
23 pub fn new(value: u64) -> Result<Self, DomainError> {
29 NonZeroU64::new(value)
30 .map(Self)
31 .ok_or(DomainError::ZeroIdentifier {
32 kind: IdentifierKind::FlowDecision,
33 })
34 }
35
36 #[must_use]
38 pub const fn get(self) -> u64 {
39 self.0.get()
40 }
41}
42
43impl fmt::Display for FlowDecisionId {
44 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45 self.get().fmt(formatter)
46 }
47}
48
49#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
51pub struct FlowDecisionSequence(NonZeroU64);
52
53impl FlowDecisionSequence {
54 pub fn new(value: u64) -> Result<Self, DomainError> {
62 NonZeroU64::new(value)
63 .map(Self)
64 .ok_or(DomainError::ZeroIdentifier {
65 kind: IdentifierKind::FlowDecisionSequence,
66 })
67 }
68
69 #[must_use]
71 pub const fn get(self) -> u64 {
72 self.0.get()
73 }
74}
75
76#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
78#[non_exhaustive]
79pub enum FlowTransitionKind {
80 StepExit,
82 Decider,
84 CompletedStepReuse,
86 SplitAggregate,
88}
89
90impl FlowTransitionKind {
91 #[doc(hidden)]
93 #[must_use]
94 pub const fn durable_code(self) -> &'static str {
95 match self {
96 Self::StepExit => "STEP_EXIT",
97 Self::Decider => "DECIDER",
98 Self::CompletedStepReuse => "COMPLETED_STEP_REUSE",
99 Self::SplitAggregate => "SPLIT_AGGREGATE",
100 }
101 }
102
103 #[doc(hidden)]
105 #[must_use]
106 pub fn from_durable_code(value: &str) -> Option<Self> {
107 match value {
108 "STEP_EXIT" => Some(Self::StepExit),
109 "DECIDER" => Some(Self::Decider),
110 "COMPLETED_STEP_REUSE" => Some(Self::CompletedStepReuse),
111 "SPLIT_AGGREGATE" => Some(Self::SplitAggregate),
112 _ => None,
113 }
114 }
115}
116
117#[derive(Clone, Debug, Eq, PartialEq)]
119pub struct FlowDecision {
120 id: FlowDecisionId,
121 job_execution_id: JobExecutionId,
122 sequence: FlowDecisionSequence,
123 source_node_id: NodeId,
124 source_step_execution_id: Option<StepExecutionId>,
125 kind: FlowTransitionKind,
126 observed_outcome: ExitCode,
127 target: FlowTarget,
128 plan_fingerprint: [u8; 32],
129 input_digest: [u8; 32],
130 reused_decision_id: Option<FlowDecisionId>,
131 decided_at: SystemTime,
132}
133
134impl FlowDecision {
135 #[allow(clippy::too_many_arguments)]
137 #[doc(hidden)]
138 #[must_use]
139 pub const fn new(
140 id: FlowDecisionId,
141 job_execution_id: JobExecutionId,
142 sequence: FlowDecisionSequence,
143 source_node_id: NodeId,
144 source_step_execution_id: Option<StepExecutionId>,
145 kind: FlowTransitionKind,
146 observed_outcome: ExitCode,
147 target: FlowTarget,
148 plan_fingerprint: [u8; 32],
149 input_digest: [u8; 32],
150 reused_decision_id: Option<FlowDecisionId>,
151 decided_at: SystemTime,
152 ) -> Self {
153 Self {
154 id,
155 job_execution_id,
156 sequence,
157 source_node_id,
158 source_step_execution_id,
159 kind,
160 observed_outcome,
161 target,
162 plan_fingerprint,
163 input_digest,
164 reused_decision_id,
165 decided_at,
166 }
167 }
168
169 #[must_use]
171 pub const fn id(&self) -> FlowDecisionId {
172 self.id
173 }
174
175 #[must_use]
177 pub const fn job_execution_id(&self) -> JobExecutionId {
178 self.job_execution_id
179 }
180
181 #[must_use]
183 pub const fn sequence(&self) -> FlowDecisionSequence {
184 self.sequence
185 }
186
187 #[must_use]
189 pub const fn source_node_id(&self) -> &NodeId {
190 &self.source_node_id
191 }
192
193 #[must_use]
195 pub const fn source_step_execution_id(&self) -> Option<StepExecutionId> {
196 self.source_step_execution_id
197 }
198
199 #[must_use]
201 pub const fn kind(&self) -> FlowTransitionKind {
202 self.kind
203 }
204
205 #[must_use]
207 pub const fn observed_outcome(&self) -> &ExitCode {
208 &self.observed_outcome
209 }
210
211 #[must_use]
213 pub const fn target(&self) -> &FlowTarget {
214 &self.target
215 }
216
217 #[must_use]
219 pub const fn plan_fingerprint(&self) -> &[u8; 32] {
220 &self.plan_fingerprint
221 }
222
223 #[must_use]
225 pub const fn input_digest(&self) -> &[u8; 32] {
226 &self.input_digest
227 }
228
229 #[must_use]
231 pub const fn reused_decision_id(&self) -> Option<FlowDecisionId> {
232 self.reused_decision_id
233 }
234
235 #[must_use]
237 pub const fn decided_at(&self) -> SystemTime {
238 self.decided_at
239 }
240}
241
242#[derive(Clone, Debug, Eq, PartialEq)]
244pub struct FlowDecisionRequest {
245 job_execution_id: JobExecutionId,
246 sequence: FlowDecisionSequence,
247 source_node_id: NodeId,
248 source_step_execution_id: Option<StepExecutionId>,
249 kind: FlowTransitionKind,
250 observed_outcome: ExitCode,
251 target: FlowTarget,
252 plan_fingerprint: [u8; 32],
253 input_digest: [u8; 32],
254 reused_decision_id: Option<FlowDecisionId>,
255 decided_at: SystemTime,
256}
257
258impl FlowDecisionRequest {
259 #[allow(clippy::too_many_arguments)]
261 #[doc(hidden)]
262 #[must_use]
263 pub const fn new(
264 job_execution_id: JobExecutionId,
265 sequence: FlowDecisionSequence,
266 source_node_id: NodeId,
267 source_step_execution_id: Option<StepExecutionId>,
268 kind: FlowTransitionKind,
269 observed_outcome: ExitCode,
270 target: FlowTarget,
271 plan_fingerprint: [u8; 32],
272 input_digest: [u8; 32],
273 reused_decision_id: Option<FlowDecisionId>,
274 decided_at: SystemTime,
275 ) -> Self {
276 Self {
277 job_execution_id,
278 sequence,
279 source_node_id,
280 source_step_execution_id,
281 kind,
282 observed_outcome,
283 target,
284 plan_fingerprint,
285 input_digest,
286 reused_decision_id,
287 decided_at,
288 }
289 }
290
291 #[must_use]
293 pub const fn job_execution_id(&self) -> JobExecutionId {
294 self.job_execution_id
295 }
296 #[must_use]
298 pub const fn sequence(&self) -> FlowDecisionSequence {
299 self.sequence
300 }
301 #[must_use]
303 pub const fn source_node_id(&self) -> &NodeId {
304 &self.source_node_id
305 }
306 #[must_use]
308 pub const fn source_step_execution_id(&self) -> Option<StepExecutionId> {
309 self.source_step_execution_id
310 }
311 #[must_use]
313 pub const fn kind(&self) -> FlowTransitionKind {
314 self.kind
315 }
316 #[must_use]
318 pub const fn observed_outcome(&self) -> &ExitCode {
319 &self.observed_outcome
320 }
321 #[must_use]
323 pub const fn target(&self) -> &FlowTarget {
324 &self.target
325 }
326 #[must_use]
328 pub const fn plan_fingerprint(&self) -> &[u8; 32] {
329 &self.plan_fingerprint
330 }
331 #[must_use]
333 pub const fn input_digest(&self) -> &[u8; 32] {
334 &self.input_digest
335 }
336 #[must_use]
338 pub const fn reused_decision_id(&self) -> Option<FlowDecisionId> {
339 self.reused_decision_id
340 }
341 #[must_use]
343 pub const fn decided_at(&self) -> SystemTime {
344 self.decided_at
345 }
346
347 #[must_use]
352 pub fn materialize(&self, id: FlowDecisionId) -> FlowDecision {
353 FlowDecision::new(
354 id,
355 self.job_execution_id,
356 self.sequence,
357 self.source_node_id.clone(),
358 self.source_step_execution_id,
359 self.kind,
360 self.observed_outcome.clone(),
361 self.target.clone(),
362 self.plan_fingerprint,
363 self.input_digest,
364 self.reused_decision_id,
365 self.decided_at,
366 )
367 }
368}
369
370#[derive(Clone, Debug, Eq, PartialEq)]
372pub struct FlowStepState {
373 node_id: NodeId,
374 execution: StepExecution,
375 context: Option<ExecutionContext>,
376}
377
378impl FlowStepState {
379 #[must_use]
384 pub const fn new(
385 node_id: NodeId,
386 execution: StepExecution,
387 context: Option<ExecutionContext>,
388 ) -> Self {
389 Self {
390 node_id,
391 execution,
392 context,
393 }
394 }
395
396 #[must_use]
398 pub const fn node_id(&self) -> &NodeId {
399 &self.node_id
400 }
401
402 #[must_use]
404 pub const fn execution(&self) -> &StepExecution {
405 &self.execution
406 }
407
408 #[must_use]
410 pub const fn context(&self) -> Option<&ExecutionContext> {
411 self.context.as_ref()
412 }
413}