Skip to main content

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}