dataflow_rs/def/types.rs
1//! The definition types: plain data, with raw expressions.
2
3use super::Rollout;
4use crate::base::{AuthoredPath, StepKind};
5use chrono::{DateTime, Utc};
6use serde_json::{Map, Value};
7
8/// The value [`parse`](super::parse) reads for an absent key, which
9/// `Serialize` leaves out. A tool that shows a definition reads them here
10/// rather than keeping its own copy.
11pub mod defaults {
12 /// A workflow's `priority`.
13 pub const PRIORITY: u32 = 0;
14 /// A workflow's `version`.
15 pub const VERSION: u32 = 1;
16 /// A workflow's `channel`.
17 pub const CHANNEL: &str = "default";
18 /// A loop's `init`.
19 pub const INIT: i64 = 0;
20 /// A loop's `increment`.
21 pub const INCREMENT: i64 = 1;
22 /// A `for_each`'s `max_concurrency`.
23 pub const MAX_CONCURRENCY: usize = 1;
24}
25
26/// Raw JSONLogic as authored. Only a compiler turns it into an
27/// [`Expr`](crate::Expr).
28#[derive(Clone, Debug, PartialEq)]
29#[non_exhaustive]
30pub struct Logic(pub Value);
31
32impl Logic {
33 /// Wraps an authored expression.
34 pub fn new(value: Value) -> Self {
35 Self(value)
36 }
37}
38
39/// The keys every node shares: a workflow, a task and a group. `name` is
40/// optional everywhere, which relaxes v3.
41#[derive(Clone, Debug, Default, PartialEq)]
42#[non_exhaustive]
43pub struct NodeHeader {
44 /// A human-readable name.
45 pub(crate) name: Option<String>,
46 /// What the node is for.
47 pub(crate) description: Option<String>,
48 /// Host and UI data, kept verbatim and never read: the authored `ext`
49 /// object, then the unknown keys the compat reader moved there.
50 pub(crate) ext: Map<String, Value>,
51}
52
53impl NodeHeader {
54 /// The human-readable name, if authored.
55 #[must_use]
56 pub fn name(&self) -> Option<&str> {
57 self.name.as_deref()
58 }
59
60 /// What the node is for, if authored.
61 #[must_use]
62 pub fn description(&self) -> Option<&str> {
63 self.description.as_deref()
64 }
65
66 /// Host and UI data, kept verbatim and never read: the authored `ext`
67 /// object, then the unknown keys the compat reader moved there.
68 #[must_use]
69 pub fn ext(&self) -> &Map<String, Value> {
70 &self.ext
71 }
72}
73
74/// A workflow definition: a condition and a tree of steps.
75///
76/// Only [`parse`](super::parse) makes one, and nothing changes one after:
77/// every field is read through its accessor, so a definition a compiler sees
78/// has passed every rule `parse` checks, and the interpreter can trust them.
79///
80/// ```
81/// use dataflow_rs::def::{ParseOptions, StepDef, parse};
82/// use serde_json::json;
83///
84/// let json = json!({"id": "w", "priority": 5, "tasks": [
85/// {"id": "t", "function": {"name": "log", "input": {"message": "hi"}}}
86/// ]});
87/// let def = parse(&json, ParseOptions::default()).def.unwrap();
88/// assert_eq!((def.id(), def.priority()), ("w", 5));
89/// let StepDef::Task(task) = &def.tasks()[0] else { unreachable!() };
90/// assert_eq!(task.function().name(), "log");
91/// ```
92///
93/// A field cannot be assigned:
94///
95/// ```compile_fail
96/// # use dataflow_rs::def::{ParseOptions, parse};
97/// # use serde_json::json;
98/// let json = json!({"id": "w", "tasks": []});
99/// let mut def = parse(&json, ParseOptions::default()).def.unwrap();
100/// def.priority = 5;
101/// ```
102#[derive(Clone, Debug, PartialEq)]
103#[non_exhaustive]
104pub struct WorkflowDef {
105 /// The workflow's id: a non-empty string.
106 pub(crate) id: String,
107 /// The name, description and `ext`.
108 pub(crate) header: NodeHeader,
109 /// The run order: lower runs first, and equal priorities keep their
110 /// order. Default 0.
111 pub(crate) priority: u32,
112 /// Whether a message enters the workflow. Absent means true.
113 pub(crate) condition: Option<Logic>,
114 /// The steps, as authored: tasks and groups.
115 pub(crate) tasks: Vec<StepDef>,
116 /// What a task's failure that reaches the workflow does. Default `fail`.
117 pub(crate) on_failure: WorkflowFailurePolicy,
118 /// The routing channel. Default `"default"`, as in v3.
119 pub(crate) channel: String,
120 /// The lifecycle status. Default `active`.
121 pub(crate) status: Status,
122 /// The definition's version. Default 1.
123 pub(crate) version: u32,
124 /// The workflow's slice of the traffic on its channel. `None` runs for
125 /// every message.
126 pub(crate) rollout: Option<Rollout>,
127 /// The bounded loop over the task list, if any.
128 pub(crate) r#loop: Option<LoopDef>,
129 /// Tags for the host's filtering.
130 pub(crate) tags: Vec<String>,
131 /// When the definition was created. Kept for stored definitions, never
132 /// read.
133 pub(crate) created_at: Option<DateTime<Utc>>,
134 /// When the definition was last updated. Kept, never read.
135 pub(crate) updated_at: Option<DateTime<Utc>>,
136}
137
138impl WorkflowDef {
139 /// The workflow's id: a non-empty string.
140 #[must_use]
141 pub fn id(&self) -> &str {
142 &self.id
143 }
144
145 /// The name, description and `ext`.
146 #[must_use]
147 pub fn header(&self) -> &NodeHeader {
148 &self.header
149 }
150
151 /// The run order: lower runs first, and equal priorities keep their
152 /// order. Default 0.
153 #[must_use]
154 pub fn priority(&self) -> u32 {
155 self.priority
156 }
157
158 /// Whether a message enters the workflow. Absent means true.
159 #[must_use]
160 pub fn condition(&self) -> Option<&Logic> {
161 self.condition.as_ref()
162 }
163
164 /// The steps, as authored: tasks and groups.
165 #[must_use]
166 pub fn tasks(&self) -> &[StepDef] {
167 &self.tasks
168 }
169
170 /// What a task's failure that reaches the workflow does. Default `fail`.
171 #[must_use]
172 pub fn on_failure(&self) -> WorkflowFailurePolicy {
173 self.on_failure
174 }
175
176 /// The routing channel. Default `"default"`, as in v3.
177 #[must_use]
178 pub fn channel(&self) -> &str {
179 &self.channel
180 }
181
182 /// The lifecycle status. Default `active`.
183 #[must_use]
184 pub fn status(&self) -> Status {
185 self.status
186 }
187
188 /// The definition's version. Default 1.
189 #[must_use]
190 pub fn version(&self) -> u32 {
191 self.version
192 }
193
194 /// The workflow's slice of the traffic on its channel. `None` runs for
195 /// every message.
196 #[must_use]
197 pub fn rollout(&self) -> Option<&Rollout> {
198 self.rollout.as_ref()
199 }
200
201 /// The bounded loop over the task list, if any.
202 #[must_use]
203 pub fn r#loop(&self) -> Option<&LoopDef> {
204 self.r#loop.as_ref()
205 }
206
207 /// Tags for the host's filtering.
208 #[must_use]
209 pub fn tags(&self) -> &[String] {
210 &self.tags
211 }
212
213 /// When the definition was created. Kept for stored definitions, never
214 /// read.
215 #[must_use]
216 pub fn created_at(&self) -> Option<DateTime<Utc>> {
217 self.created_at
218 }
219
220 /// When the definition was last updated. Kept, never read.
221 #[must_use]
222 pub fn updated_at(&self) -> Option<DateTime<Utc>> {
223 self.updated_at
224 }
225
226 /// Every step with its authored path and kind, pre-order: `loop.setup`
227 /// first, then `tasks`, each group before its members.
228 ///
229 /// A task's kind is [`StepKind::Task`] and a group's [`StepKind::Group`];
230 /// every step under `loop.setup`, group or task, is [`StepKind::Setup`].
231 pub fn walk(&self) -> impl Iterator<Item = (AuthoredPath, StepKind, &StepDef)> {
232 let mut out = Vec::new();
233 if let Some(config) = &self.r#loop {
234 let mut at = AuthoredPath::root().join("loop").join("setup");
235 collect(&config.setup, &mut at, true, &mut out);
236 }
237 let mut at = AuthoredPath::root().join("tasks");
238 collect(&self.tasks, &mut at, false, &mut out);
239 out.into_iter()
240 }
241}
242
243/// Appends `steps` and their members to `out`, pre-order.
244fn collect<'d>(
245 steps: &'d [StepDef],
246 at: &mut AuthoredPath,
247 setup: bool,
248 out: &mut Vec<(AuthoredPath, StepKind, &'d StepDef)>,
249) {
250 for (i, step) in steps.iter().enumerate() {
251 at.push(i);
252 out.push((at.clone(), step.kind(setup), step));
253 if let StepDef::Group(group) = step {
254 at.push("tasks");
255 collect(&group.tasks, at, setup, out);
256 at.pop();
257 }
258 at.pop();
259 }
260}
261
262/// One element of a step list. An element carrying a `tasks` key is a group.
263///
264/// Not `#[non_exhaustive]`: the step grammar is closed, and a match on it is
265/// a decision about every kind of step.
266#[derive(Clone, Debug, PartialEq)]
267#[allow(
268 clippy::large_enum_variant,
269 reason = "a definition is parse-time data, never touched per message; a task \
270 is larger than a group because its JSON values are insertion-ordered \
271 maps (serde_json's `preserve_order`), and boxing it would cost every \
272 host that builds or matches a step a `Box` for no runtime gain"
273)]
274pub enum StepDef {
275 /// A task: one function call.
276 Task(TaskDef),
277 /// A group: a span of steps sharing one condition.
278 Group(GroupDef),
279}
280
281impl StepDef {
282 /// The step's id. Tasks and groups share one namespace.
283 pub fn id(&self) -> &str {
284 match self {
285 Self::Task(task) => &task.id,
286 Self::Group(group) => &group.id,
287 }
288 }
289
290 /// The kind a walk stamps on the step: [`StepKind::Setup`] for any step
291 /// in a setup list, `setup`, and otherwise its own.
292 pub(crate) fn kind(&self, setup: bool) -> StepKind {
293 match self {
294 _ if setup => StepKind::Setup,
295 Self::Task(_) => StepKind::Task,
296 Self::Group(_) => StepKind::Group,
297 }
298 }
299}
300
301/// A task: one function call, with its outcome policies.
302#[derive(Clone, Debug, PartialEq)]
303#[non_exhaustive]
304pub struct TaskDef {
305 /// The step id, unique across the workflow's steps and setup.
306 pub(crate) id: String,
307 /// The name, description and `ext`.
308 pub(crate) header: NodeHeader,
309 /// Whether the task runs. Absent means true.
310 pub(crate) condition: Option<Logic>,
311 /// The function the task runs, and its raw input.
312 pub(crate) function: FunctionRef,
313 /// What a failure (a 5xx or an `Err`) does. Default `fail`.
314 pub(crate) on_failure: FailurePolicy,
315 /// What a rejection (a 4xx) does. Default `continue`.
316 pub(crate) on_rejection: RejectionPolicy,
317 /// Whether running the task ends the workflow, applied after the
318 /// policies. Default false.
319 pub(crate) terminal: bool,
320 /// Run the function once per element of an array. Refused on a built-in.
321 pub(crate) for_each: Option<ForEachDef>,
322}
323
324impl TaskDef {
325 /// The step id, unique across the workflow's steps and setup.
326 #[must_use]
327 pub fn id(&self) -> &str {
328 &self.id
329 }
330
331 /// The name, description and `ext`.
332 #[must_use]
333 pub fn header(&self) -> &NodeHeader {
334 &self.header
335 }
336
337 /// Whether the task runs. Absent means true.
338 #[must_use]
339 pub fn condition(&self) -> Option<&Logic> {
340 self.condition.as_ref()
341 }
342
343 /// The function the task runs, and its raw input.
344 #[must_use]
345 pub fn function(&self) -> &FunctionRef {
346 &self.function
347 }
348
349 /// What a failure (a 5xx or an `Err`) does. Default `fail`.
350 #[must_use]
351 pub fn on_failure(&self) -> FailurePolicy {
352 self.on_failure
353 }
354
355 /// What a rejection (a 4xx) does. Default `continue`.
356 #[must_use]
357 pub fn on_rejection(&self) -> RejectionPolicy {
358 self.on_rejection
359 }
360
361 /// Whether running the task ends the workflow, applied after the
362 /// policies. Default false.
363 #[must_use]
364 pub fn terminal(&self) -> bool {
365 self.terminal
366 }
367
368 /// The task's fan-out, if any: its function runs once per element.
369 #[must_use]
370 pub fn for_each(&self) -> Option<&ForEachDef> {
371 self.for_each.as_ref()
372 }
373}
374
375/// A group: a span of steps sharing one condition, evaluated once on entry.
376#[derive(Clone, Debug, PartialEq)]
377#[non_exhaustive]
378pub struct GroupDef {
379 /// The step id, unique across the workflow's steps and setup.
380 pub(crate) id: String,
381 /// The name, description and `ext`.
382 pub(crate) header: NodeHeader,
383 /// Whether the span runs. Absent means true.
384 pub(crate) condition: Option<Logic>,
385 /// Whether reaching the end of the group ends the workflow.
386 pub(crate) terminal: bool,
387 /// The members: never empty, nested at most
388 /// [`MAX_GROUP_DEPTH`](super::MAX_GROUP_DEPTH) deep.
389 pub(crate) tasks: Vec<StepDef>,
390}
391
392impl GroupDef {
393 /// The step id, unique across the workflow's steps and setup.
394 #[must_use]
395 pub fn id(&self) -> &str {
396 &self.id
397 }
398
399 /// The name, description and `ext`.
400 #[must_use]
401 pub fn header(&self) -> &NodeHeader {
402 &self.header
403 }
404
405 /// Whether the span runs. Absent means true.
406 #[must_use]
407 pub fn condition(&self) -> Option<&Logic> {
408 self.condition.as_ref()
409 }
410
411 /// Whether reaching the end of the group ends the workflow.
412 #[must_use]
413 pub fn terminal(&self) -> bool {
414 self.terminal
415 }
416
417 /// The members: never empty, nested at most
418 /// [`MAX_GROUP_DEPTH`](super::MAX_GROUP_DEPTH) deep.
419 #[must_use]
420 pub fn tasks(&self) -> &[StepDef] {
421 &self.tasks
422 }
423}
424
425/// A workflow's bounded loop: the task list runs once per sweep while
426/// `counter < max`.
427#[derive(Clone, Debug, PartialEq)]
428#[non_exhaustive]
429pub struct LoopDef {
430 /// The `temp_data` slot the engine writes the counter to, if any.
431 pub(crate) counter: Option<String>,
432 /// The first counter value. Default 0.
433 pub(crate) init: i64,
434 /// Added to the counter after each sweep: at least 1. Default 1.
435 pub(crate) increment: i64,
436 /// The exclusive upper bound, above `init`. Required.
437 pub(crate) max: i64,
438 /// Steps run once, before the first sweep, with no counter.
439 pub(crate) setup: Vec<StepDef>,
440 /// The array iterated, evaluated once after setup.
441 pub(crate) over: Option<Logic>,
442 /// The `temp_data` slot holding `over[counter]`. Requires `over`.
443 pub(crate) r#as: Option<String>,
444 /// The `temp_data` slot reset to `{}` before every sweep.
445 pub(crate) scratch: Option<String>,
446}
447
448impl LoopDef {
449 /// The `temp_data` slot the engine writes the counter to, if any.
450 #[must_use]
451 pub fn counter(&self) -> Option<&str> {
452 self.counter.as_deref()
453 }
454
455 /// The first counter value. Default 0.
456 #[must_use]
457 pub fn init(&self) -> i64 {
458 self.init
459 }
460
461 /// Added to the counter after each sweep: at least 1. Default 1.
462 #[must_use]
463 pub fn increment(&self) -> i64 {
464 self.increment
465 }
466
467 /// The exclusive upper bound, above `init`.
468 #[must_use]
469 pub fn max(&self) -> i64 {
470 self.max
471 }
472
473 /// Steps run once, before the first sweep, with no counter.
474 #[must_use]
475 pub fn setup(&self) -> &[StepDef] {
476 &self.setup
477 }
478
479 /// The array iterated, evaluated once after setup.
480 #[must_use]
481 pub fn over(&self) -> Option<&Logic> {
482 self.over.as_ref()
483 }
484
485 /// The `temp_data` slot holding `over[counter]`. Only with `over`.
486 #[must_use]
487 pub fn r#as(&self) -> Option<&str> {
488 self.r#as.as_deref()
489 }
490
491 /// The `temp_data` slot reset to `{}` before every sweep.
492 #[must_use]
493 pub fn scratch(&self) -> Option<&str> {
494 self.scratch.as_deref()
495 }
496}
497
498/// A task's fan-out: its function runs once per element of an array.
499#[derive(Clone, Debug, PartialEq)]
500#[non_exhaustive]
501pub struct ForEachDef {
502 /// The array, evaluated once after the task's condition.
503 pub(crate) over: Logic,
504 /// The `temp_data` slot holding the element; its index is at
505 /// `temp_data.<as>_index`.
506 pub(crate) r#as: String,
507 /// Calls in flight at once: timing only, never results. Default 1.
508 pub(crate) max_concurrency: usize,
509 /// The context path each call writes its result to. Requires `into`.
510 pub(crate) collect: Option<String>,
511 /// The context path receiving the results in element order. Requires
512 /// `collect`.
513 pub(crate) into: Option<String>,
514}
515
516impl ForEachDef {
517 /// The array, evaluated once after the task's condition.
518 #[must_use]
519 pub fn over(&self) -> &Logic {
520 &self.over
521 }
522
523 /// The `temp_data` slot holding the element; its index is at
524 /// `temp_data.<as>_index`.
525 #[must_use]
526 pub fn r#as(&self) -> &str {
527 &self.r#as
528 }
529
530 /// Calls in flight at once: timing only, never results. Default 1.
531 #[must_use]
532 pub fn max_concurrency(&self) -> usize {
533 self.max_concurrency
534 }
535
536 /// The context path each call writes its result to. Only with `into`.
537 #[must_use]
538 pub fn collect(&self) -> Option<&str> {
539 self.collect.as_deref()
540 }
541
542 /// The context path receiving the results in element order. Only with
543 /// `collect`.
544 #[must_use]
545 pub fn into(&self) -> Option<&str> {
546 self.into.as_deref()
547 }
548}
549
550/// The function a task runs.
551#[derive(Clone, Debug, PartialEq)]
552#[non_exhaustive]
553pub struct FunctionRef {
554 /// The registered name.
555 pub(crate) name: String,
556 /// The config, raw until a compiler reads it. Default `{}`.
557 pub(crate) input: Value,
558}
559
560impl FunctionRef {
561 /// The registered name.
562 #[must_use]
563 pub fn name(&self) -> &str {
564 &self.name
565 }
566
567 /// The config, raw until a compiler reads it. Default `{}`.
568 #[must_use]
569 pub fn input(&self) -> &Value {
570 &self.input
571 }
572}
573
574/// A spelling table for an enum read from a string.
575pub(super) trait Spelling: Copy + Default + 'static {
576 /// Every variant, in the order the messages list them.
577 const ALL: &'static [Self];
578
579 /// The variant's authored spelling.
580 fn spelling(self) -> &'static str;
581
582 /// The variant spelled `text`, if any.
583 fn from_spelling(text: &str) -> Option<Self> {
584 Self::ALL.iter().copied().find(|v| v.spelling() == text)
585 }
586}
587
588/// A workflow's lifecycle status.
589#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
590#[non_exhaustive]
591pub enum Status {
592 /// Runs. The default.
593 #[default]
594 Active,
595 /// Kept, not run.
596 Paused,
597 /// Retired.
598 Archived,
599}
600
601impl Status {
602 /// The authored spelling: `"active"`, `"paused"` or `"archived"`.
603 pub fn as_str(self) -> &'static str {
604 match self {
605 Self::Active => "active",
606 Self::Paused => "paused",
607 Self::Archived => "archived",
608 }
609 }
610}
611
612impl Spelling for Status {
613 const ALL: &'static [Self] = &[Self::Active, Self::Paused, Self::Archived];
614 fn spelling(self) -> &'static str {
615 self.as_str()
616 }
617}
618
619/// What a task's failure (a 5xx or an `Err`) does.
620#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
621#[non_exhaustive]
622pub enum FailurePolicy {
623 /// The failure passes to the workflow's
624 /// [`on_failure`](WorkflowDef::on_failure). The default.
625 #[default]
626 Fail,
627 /// The workflow stops, and the run goes on.
628 Halt,
629 /// The failure is recorded and the next task runs.
630 Continue,
631}
632
633impl FailurePolicy {
634 /// The authored spelling: `"fail"`, `"halt"` or `"continue"`.
635 pub fn as_str(self) -> &'static str {
636 match self {
637 Self::Fail => "fail",
638 Self::Halt => "halt",
639 Self::Continue => "continue",
640 }
641 }
642}
643
644impl Spelling for FailurePolicy {
645 const ALL: &'static [Self] = &[Self::Fail, Self::Halt, Self::Continue];
646 fn spelling(self) -> &'static str {
647 self.as_str()
648 }
649}
650
651/// What a task's rejection (a 4xx) does.
652#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
653#[non_exhaustive]
654pub enum RejectionPolicy {
655 /// The rejection is recorded and the next task runs. The default.
656 #[default]
657 Continue,
658 /// The workflow stops, and the run goes on.
659 Halt,
660}
661
662impl RejectionPolicy {
663 /// The authored spelling: `"continue"` or `"halt"`.
664 pub fn as_str(self) -> &'static str {
665 match self {
666 Self::Continue => "continue",
667 Self::Halt => "halt",
668 }
669 }
670}
671
672impl Spelling for RejectionPolicy {
673 const ALL: &'static [Self] = &[Self::Continue, Self::Halt];
674 fn spelling(self) -> &'static str {
675 self.as_str()
676 }
677}
678
679/// What a failure that reaches the workflow does.
680#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
681#[non_exhaustive]
682pub enum WorkflowFailurePolicy {
683 /// The run stops. The default.
684 #[default]
685 Fail,
686 /// The next sweep, or the next workflow, runs.
687 Continue,
688}
689
690impl WorkflowFailurePolicy {
691 /// The authored spelling: `"fail"` or `"continue"`.
692 pub fn as_str(self) -> &'static str {
693 match self {
694 Self::Fail => "fail",
695 Self::Continue => "continue",
696 }
697 }
698}
699
700impl Spelling for WorkflowFailurePolicy {
701 const ALL: &'static [Self] = &[Self::Fail, Self::Continue];
702 fn spelling(self) -> &'static str {
703 self.as_str()
704 }
705}