Skip to main content

dataflow_rs/
lib.rs

1/*!
2# Dataflow-rs
3
4A lightweight rules engine for building IFTTT-style automation and data processing pipelines in Rust.
5
6## Overview
7
8Dataflow-rs provides a high-performance rules engine that follows the **IF → THEN → THAT** model:
9
10- **IF** — Define conditions using JSONLogic expressions (evaluated against `data`, `metadata`, `temp_data`)
11- **THEN** — Execute actions: data transformation, validation, or custom async logic
12- **THAT** — Chain multiple actions and rules with priority ordering
13
14Rules are defined declaratively in JSON and compiled once at startup for zero-overhead evaluation at runtime.
15
16## Key Components
17
18| Rules Engine | Workflow Engine | Description |
19|---|---|---|
20| **RulesEngine** | **Engine** | Central async component that evaluates rules and executes actions |
21| **Rule** | **Workflow** | A condition + actions bundle — IF condition THEN execute actions |
22| **Action** | **Task** | An individual processing step that performs a function on a message |
23
24* **AsyncFunctionHandler**: A trait implemented by action handlers to define custom async processing logic
25* **TaskContext**: Per-call context handed to handlers — typed data accessors, audit-trail-aware setters
26* **TaskOutcome**: Return value of a handler — `Success`, `Status(code)`, `Skip`, or `Halt`
27* **Message**: The data structure that flows through the engine, containing payload, metadata, and processing results
28
29## Built-in Functions
30
31The engine ships with the following pre-registered functions, available to
32any workflow without further setup:
33
34| Category | Function | Purpose |
35|---|---|---|
36| **Parse** | `parse_json` | Deserialize a JSON payload string into `data` |
37| **Parse** | `parse_xml` | Deserialize an XML payload string into `data` |
38| **Transform** | `map` | Assign JSONLogic-derived values to dot-paths within the message |
39| **Validate** | `validation` | Apply JSONLogic rules with custom error messages |
40| **Routing** | `filter` | Skip or halt processing based on a JSONLogic predicate |
41| **Routing** | `log` | Emit a log entry at a configurable level |
42| **Publish** | `publish_json` | Render `data` back out as a JSON payload |
43| **Publish** | `publish_xml` | Render `data` back out as an XML payload |
44
45In addition, dataflow-rs provides **typed config schemas** for three common
46service-layer integrations — `http_call`, `enrich`, and `publish_kafka`.
47These are *not* pre-registered: register an `AsyncFunctionHandler` under the
48matching name and the engine handles config validation and JSONLogic
49pre-compilation for you. See [`HttpCallConfig`], [`EnrichConfig`], and
50[`PublishKafkaConfig`].
51
52Custom functions are registered through `Engine::builder().register(...)`;
53see the **Extending with Custom Functions** section below.
54
55## Usage Example
56
57```rust,no_run
58use dataflow_rs::{Engine, Workflow};
59use dataflow_rs::engine::message::Message;
60use serde_json::json;
61
62#[tokio::main]
63async fn main() -> Result<(), Box<dyn std::error::Error>> {
64    // Define a workflow in JSON
65    let workflow_json = r#"
66    {
67        "id": "premium_order",
68        "name": "Premium Order Processing",
69        "condition": {">=": [{"var": "data.order.total"}, 1000]},
70        "tasks": [
71            {
72                "id": "apply_discount",
73                "name": "Apply Premium Discount",
74                "function": {
75                    "name": "map",
76                    "input": {
77                        "mappings": [
78                            {
79                                "path": "data.order.discount",
80                                "logic": {"*": [{"var": "data.order.total"}, 0.1]}
81                            }
82                        ]
83                    }
84                }
85            }
86        ]
87    }
88    "#;
89
90    // Parse the workflow
91    let workflow = Workflow::from_json(workflow_json)?;
92
93    // Create the workflow engine — builder is the recommended path; built-in
94    // functions are auto-registered.
95    let engine = Engine::builder().with_workflow(workflow).build()?;
96
97    // Create a message to process
98    let mut message = Message::from_value(&json!({
99        "order": {
100            "total": 1500
101        }
102    }));
103
104    // Process the message through the workflow
105    match engine.process_message(&mut message).await {
106        Ok(_) => {
107            println!("Discount: {}", message.data()["order"]["discount"]); // 150
108        }
109        Err(e) => {
110            println!("Error in workflow: {:?}", e);
111        }
112    }
113
114    Ok(())
115}
116```
117
118## Error Handling
119
120The library provides a comprehensive error handling system:
121
122```rust,no_run
123use dataflow_rs::{Engine, Result, DataflowError};
124use dataflow_rs::engine::message::Message;
125use serde_json::json;
126
127#[tokio::main]
128async fn main() -> Result<()> {
129    // ... setup workflows ...
130    let engine = Engine::builder().build()?;
131
132    let mut message = Message::from_value(&json!({}));
133
134    // Process the message, errors will be collected but not halt execution
135    engine.process_message(&mut message).await?;
136
137    // Check if there were any errors during processing
138    if message.has_errors() {
139        for error in message.errors() {
140            println!("Error in workflow: {:?}, task: {:?}: {:?}",
141                     error.workflow_id, error.task_id, error.message);
142        }
143    }
144
145    Ok(())
146}
147```
148
149## Extending with Custom Functions
150
151Implement `AsyncFunctionHandler` with a typed `Input` so the engine deserializes
152your config once at startup; handlers then receive typed input and a
153`TaskContext` that records audit-trail changes automatically.
154
155```rust,no_run
156use dataflow_rs::{
157    AsyncFunctionHandler, Engine, Result, TaskContext, TaskOutcome, Workflow,
158};
159use datavalue::OwnedDataValue;
160use serde::Deserialize;
161use serde_json::json;
162use async_trait::async_trait;
163
164#[derive(Deserialize)]
165struct StatsInput {
166    /// Path inside `data` whose array of numbers to summarize.
167    source: String,
168    /// Path inside `data` to write the result to.
169    target: String,
170}
171
172struct Statistics;
173
174#[async_trait]
175impl AsyncFunctionHandler for Statistics {
176    type Input = StatsInput;
177
178    async fn execute(
179        &self,
180        ctx: &mut TaskContext<'_>,
181        input: &StatsInput,
182    ) -> Result<TaskOutcome> {
183        let count = ctx.data()
184            .get(input.source.as_str())
185            .and_then(|v| v.as_array())
186            .map(|arr| arr.len())
187            .unwrap_or(0);
188
189        ctx.set(
190            &format!("data.{}", input.target),
191            OwnedDataValue::from(&json!({ "count": count })),
192        );
193        Ok(TaskOutcome::Success)
194    }
195}
196
197#[tokio::main]
198async fn main() -> Result<()> {
199    let engine = Engine::builder()
200        .register("statistics", Statistics)
201        // .with_workflow(workflow)
202        .build()?;
203    // ...
204    Ok(())
205}
206```
207
208## Ecosystem
209
210Dataflow-rs is part of a small family of crates that share the same workflow
211and JSONLogic shape:
212
213| Crate | Purpose |
214|---|---|
215| [`dataflow-rs`](https://crates.io/crates/dataflow-rs) | This crate — async workflow engine in Rust |
216| [`@goplasmatic/dataflow-wasm`](https://www.npmjs.com/package/@goplasmatic/dataflow-wasm) | WebAssembly bindings — run workflows in the browser or Node |
217| [`@goplasmatic/dataflow-ui`](https://www.npmjs.com/package/@goplasmatic/dataflow-ui) | React components for visualizing and debugging workflows |
218| [`datalogic-rs`](https://crates.io/crates/datalogic-rs) | The JSONLogic compiler/evaluator used internally |
219
220Source for all four lives under <https://github.com/GoPlasmatic>.
221*/
222
223pub mod engine;
224pub mod prelude;
225
226// Re-export all public APIs for easier access
227pub use engine::error::{DataflowError, ErrorInfo, Result};
228pub use engine::functions::{
229    AsyncFunctionHandler, BoxedFunctionHandler, EnrichConfig, FilterConfig, FunctionConfig,
230    HttpCallConfig, LogConfig, MapConfig, MapMapping, PublishKafkaConfig, ValidationConfig,
231    ValidationRule,
232};
233pub use engine::message::{AuditTrail, Change, Message, MessageBuilder};
234pub use engine::task_context::TaskContext;
235pub use engine::task_outcome::TaskOutcome;
236pub use engine::trace::{ExecutionStep, ExecutionTrace, StepResult};
237pub use engine::{Engine, EngineBuilder, Task, Workflow, WorkflowStatus};
238
239/// Type alias for `Workflow` — a Rule represents an IF-THEN unit: IF condition THEN execute actions.
240pub type Rule = Workflow;
241
242/// Type alias for `Task` — an Action is an individual processing step within a rule.
243pub type Action = Task;
244
245/// Type alias for `Engine` — the RulesEngine evaluates rules and executes their actions.
246pub type RulesEngine = Engine;
247
248/// Compiles the Rust snippets in `README.md` as doctests so the landing-page
249/// examples cannot drift from the API. Compiled only under `cargo test`; this
250/// type does not exist in a normal build and is not part of the public API.
251///
252/// Snippets that are illustrative fragments rather than runnable programs are
253/// tagged `ignore` in the README and are skipped here.
254#[cfg(doctest)]
255#[doc = include_str!("../README.md")]
256pub struct ReadmeDoctests;