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 dataflow_rs::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, ServiceErrorBuilder};
228pub use engine::functions::{
229 AsyncFunctionHandler, BUILTIN_FUNCTION_NAMES, BoxedFunctionHandler, BuiltinKind, EnrichConfig,
230 FilterConfig, FunctionConfig, HttpCallConfig, HttpMethod, LogConfig, MapConfig, MapMapping,
231 PublishKafkaConfig, Template, TemplateCompiler, ValidationConfig, ValidationRule,
232 builtin_function_kind, is_builtin_function,
233};
234pub use engine::message::{AuditTrail, Change, Message, MessageBuilder};
235pub use engine::observer::{ExecutionObserver, TaskEvent};
236pub use engine::task_context::TaskContext;
237pub use engine::task_outcome::{HALT_STATUS_CODE, TaskOutcome};
238pub use engine::trace::{AuditTrailScope, ExecutionStep, ExecutionTrace, StepResult, TraceOptions};
239pub use engine::{ConnectorRef, Engine, EngineBuilder, Rollout, Task, Workflow, WorkflowStatus};
240
241/// The [`datalogic_rs`] JSONLogic engine, re-exported.
242///
243/// [`Engine::datalogic`] and [`TaskContext::datalogic`] both return
244/// `&Arc<datalogic_rs::Engine>`, [`TaskContext::eval`] takes a `&datalogic_rs::Logic`
245/// directly, and [`TemplateCompiler::engine`] returns a `&datalogic_rs::Engine` —
246/// so any crate implementing [`AsyncFunctionHandler`] with ad-hoc evaluation, or
247/// compiling a [`Template`] field itself, has to name these types. Reaching them
248/// through here locks their major version to whatever `dataflow-rs` depends on;
249/// prefer it over an independent `datalogic-rs` dependency, which can skew.
250///
251/// Two things to know:
252///
253/// - `datalogic_rs::Engine` and [`crate::Engine`] are both called `Engine`.
254/// This crate imports the former under an alias
255/// (`use datalogic_rs::Engine as DatalogicEngine;`); you will want to do the
256/// same.
257/// - The operator surface is feature-dependent, and the gate is compile-time —
258/// `datalogic-rs` `#[cfg]`s the opcodes themselves, so there is no runtime
259/// equivalent. `dataflow-rs` forwards each family as a cargo feature of the
260/// same name, all **off by default**:
261///
262/// | feature | operators |
263/// |---|---|
264/// | `ext-string` | `length`, `starts_with`, `ends_with`, `upper`, `lower`, `trim`, `split` |
265/// | `ext-array` | `sort`, `slice` |
266/// | `ext-math` | `abs`, `ceil`, `floor` |
267/// | `ext-control` | `exists`, `??`, `switch` (alias `match`), `type` |
268/// | `error-handling` | `try`, `throw` |
269/// | `datetime` | `datetime`, `timestamp`, `parse_date`, `format_date`, `date_diff`, `now` |
270/// | `all-operators` | all of the above |
271///
272/// `error-handling` names the JSONLogic `try`/`throw` operators. It is
273/// unrelated to this crate's own error handling ([`DataflowError`],
274/// `continue_on_error`, [`Message::errors`]), which is unconditional.
275///
276/// Enabling a family is **not** a no-op for existing workflows. This crate
277/// always runs `datalogic-rs` in templating mode, where an unrecognised
278/// operator name is not an error — the object echoes back verbatim. So a
279/// `{"length": …}` value that used to pass through a `map` mapping as literal
280/// data starts *evaluating* once `ext-string` is on. `datetime` goes further
281/// and changes core operators: with it enabled, `==` and the ordering
282/// operators parse plain date-shaped strings as instants, so
283/// `{"==": ["2024-01-15T00:00:00Z", "2024-01-15T01:00:00+01:00"]}` is `true`
284/// rather than `false`. Enable only the families your rules use.
285///
286/// A direct `datalogic-rs` dependency with the feature enabled still works
287/// via cargo's additive unification, but prefer the feature here — it cannot
288/// skew from the version this crate depends on.
289pub use datalogic_rs;
290
291/// The [`datavalue`] value-type crate, re-exported.
292///
293/// [`Message::context`], [`Message::data`], [`TaskContext::get`] /
294/// [`TaskContext::set`], [`Change::old_value`] and the [`engine::utils`] path
295/// helpers are all expressed in terms of `datavalue::OwnedDataValue`, so it is
296/// unavoidable for handler authors.
297///
298/// Note the crate is published as `datavalue-rs` and used here under the name
299/// `datavalue` via a `package =` rename, which makes the correct `Cargo.toml`
300/// line hard to guess — reach it as `dataflow_rs::datavalue` instead. Same
301/// major-version-skew argument as [`datalogic_rs`].
302pub use datavalue;
303
304/// Type alias for `Workflow` — a Rule represents an IF-THEN unit: IF condition THEN execute actions.
305pub type Rule = Workflow;
306
307/// Type alias for `Task` — an Action is an individual processing step within a rule.
308pub type Action = Task;
309
310/// Type alias for `Engine` — the RulesEngine evaluates rules and executes their actions.
311pub type RulesEngine = Engine;
312
313/// Compiles the Rust snippets in `README.md` as doctests so the landing-page
314/// examples cannot drift from the API. Compiled only under `cargo test`; this
315/// type does not exist in a normal build and is not part of the public API.
316///
317/// Snippets that are illustrative fragments rather than runnable programs are
318/// tagged `ignore` in the README and are skipped here.
319#[cfg(doctest)]
320#[doc = include_str!("../README.md")]
321pub struct ReadmeDoctests;