dataflow-rs 4.0.0

A lightweight rules engine for building IFTTT-style automation and data processing pipelines in Rust. Define rules with JSONLogic conditions, execute actions, and chain workflows.
Documentation
//! The ops a workflow lowers to, and the side tables they index.
//!
//! The compile walk fills the tables in walk order: setup first, then the
//! body, pre-order. Every field is `pub(crate)`, for the runtime.

use crate::base::{AuthoredPath, StepKind};
use crate::def::{FailurePolicy, RejectionPolicy};
use crate::expr::{Expr, ExprVisitor, Path};
use crate::function::BoundCall;
use crate::function::builtins::Builtin;

/// What a `Cond` op gates.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CondKind {
    Workflow,
    Group,
    Task,
}

/// One instruction. A target is an op index, and `ops.len()` is the end.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Op {
    Cond {
        cond: u32,
        kind: CondKind,
        else_to: u32,
    },
    Call {
        site: u32,
        fail_to: u32,
    },
    FanOut {
        site: u32,
        fan: u32,
        fail_to: u32,
    },
    LoopInit {
        lp: u32,
        exit_to: u32,
    },
    LoopNext {
        lp: u32,
        body_to: u32,
        exit_to: u32,
    },
    Halt,
}

impl Op {
    /// The op's forward target: every target but a `LoopNext`'s `body_to`,
    /// which goes back to the start of its sweep. `None` for a `Halt`.
    pub(crate) fn forward_mut(&mut self) -> Option<&mut u32> {
        match self {
            Self::Cond { else_to: to, .. }
            | Self::Call { fail_to: to, .. }
            | Self::FanOut { fail_to: to, .. }
            | Self::LoopInit { exit_to: to, .. }
            | Self::LoopNext { exit_to: to, .. } => Some(to),
            Self::Halt => None,
        }
    }

    /// [`Self::forward_mut`], read.
    pub(crate) fn forward(mut self) -> Option<u32> {
        self.forward_mut().copied()
    }
}

const _: () = assert!(size_of::<Op>() == 16);

/// A compiled condition: the workflow's, a group's or a task's.
pub(crate) struct CondSite {
    pub(crate) expr: Expr,
    /// The group's or task's id; `None` for the workflow's.
    pub(crate) step: Option<String>,
}

/// One task's call: what it calls and what its outcome does.
pub(crate) struct CallSite {
    /// The task's row in `tasks`.
    pub(crate) task: u32,
    pub(crate) callee: Callee,
    /// What the task's outcome does.
    pub(crate) policy: Policy,
}

/// What a task's outcome does: its three policies, as its call site holds
/// them.
#[derive(Clone, Copy, Debug)]
pub(crate) struct Policy {
    /// What a failure (a 5xx or an `Err`) does.
    pub(crate) on_failure: FailurePolicy,
    /// What a rejection (a 4xx) does.
    pub(crate) on_rejection: RejectionPolicy,
    /// Whether completing the task ends the workflow.
    pub(crate) terminal: bool,
}

/// What a call site calls: a built-in's typed config (the parse and
/// publish ones boxed, so the site stays small), or a host function bound
/// to its config.
pub(crate) enum Callee {
    Builtin(Builtin),
    Host(Box<dyn BoundCall>),
}

impl Callee {
    /// Walks the config's expressions; paths are relative to the config.
    pub(crate) fn visit(&self, v: &mut dyn ExprVisitor) {
        match self {
            Self::Builtin(builtin) => builtin.visit(v),
            Self::Host(bound) => bound.visit(v),
        }
    }
}

/// A task's fan-out.
pub(crate) struct FanSite {
    pub(crate) over: Expr,
    /// `temp_data.<as>`.
    pub(crate) r#as: Path,
    /// `temp_data.<as>_index`.
    pub(crate) index: Path,
    pub(crate) max_concurrency: usize,
    pub(crate) collect: Option<Path>,
    pub(crate) into: Option<Path>,
}

/// A workflow's loop. Each slot is a `temp_data` path.
pub(crate) struct LoopSite {
    pub(crate) init: i64,
    pub(crate) increment: i64,
    pub(crate) max: i64,
    pub(crate) counter: Option<Path>,
    pub(crate) over: Option<Expr>,
    pub(crate) r#as: Option<Path>,
    pub(crate) scratch: Option<Path>,
}

/// One task, as the program lists it: one row per task, never per group.
pub(crate) struct TaskMeta {
    pub(crate) id: String,
    /// The registered name of the function it calls.
    pub(crate) function: String,
    pub(crate) kind: StepKind,
    pub(crate) path: AuthoredPath,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn call_site_fits_in_256_bytes() {
        // A built-in config holding an expression inline is boxed.
        let size = std::mem::size_of::<CallSite>();
        assert!(size <= 256, "a call site is {size} bytes");
    }
}