pub struct EngineBuilder { /* private fields */ }Expand description
Builder for Engine. The recommended construction path — chain
register("name", handler) and with_workflow(workflow) calls, then
build() to produce a Result<Engine>. Empty registration is fine; an
engine with no custom handlers still resolves the built-in functions.
register takes any AsyncFunctionHandler and boxes it internally; the
Box<dyn DynAsyncFunctionHandler + Send + Sync> plumbing stays out of
user code.
use dataflow_rs::{Engine, Workflow};
let engine = Engine::builder()
.with_workflow(workflow)
// .register("my_handler", MyHandler)
.build()
.unwrap();Implementations§
Source§impl EngineBuilder
impl EngineBuilder
Sourcepub fn new() -> Self
pub fn new() -> Self
Create an empty builder. Equivalent to EngineBuilder::default.
Sourcepub fn register<F>(self, name: impl Into<String>, handler: F) -> Selfwhere
F: AsyncFunctionHandler,
pub fn register<F>(self, name: impl Into<String>, handler: F) -> Selfwhere
F: AsyncFunctionHandler,
Register a custom async handler under name. Accepts any
AsyncFunctionHandler; boxing happens internally via the engine’s
blanket impl.
Sourcepub fn register_boxed(
self,
name: impl Into<String>,
handler: BoxedFunctionHandler,
) -> Self
pub fn register_boxed( self, name: impl Into<String>, handler: BoxedFunctionHandler, ) -> Self
Register a pre-boxed handler. Useful when handlers are constructed dynamically (e.g. plugin registries) and the concrete type isn’t known at the call site.
Sourcepub fn with_workflow(self, workflow: Workflow) -> Self
pub fn with_workflow(self, workflow: Workflow) -> Self
Add a single workflow. Subsequent calls append.
Sourcepub fn with_workflows<I>(self, workflows: I) -> Selfwhere
I: IntoIterator<Item = Workflow>,
pub fn with_workflows<I>(self, workflows: I) -> Selfwhere
I: IntoIterator<Item = Workflow>,
Append every workflow in workflows. Accepts anything iterable —
Vec<Workflow>, an array, an iterator. Existing workflows on the
builder are kept; subsequent registers/workflows still chain.
Sourcepub fn with_handlers(
self,
handlers: HashMap<String, BoxedFunctionHandler>,
) -> Self
pub fn with_handlers( self, handlers: HashMap<String, BoxedFunctionHandler>, ) -> Self
Insert every handler in handlers, keeping any already registered.
Same extend-not-replace semantics as EngineBuilder::with_workflows.
Exists because register is per-name, which pushed an embedder that
builds a whole HashMap<String, BoxedFunctionHandler> in one place onto
Engine::new and off the builder entirely — and therefore out of reach
of EngineBuilder::with_observer.
Sourcepub fn with_observer(self, observer: Arc<dyn ExecutionObserver>) -> Self
pub fn with_observer(self, observer: Arc<dyn ExecutionObserver>) -> Self
Attach a per-task ExecutionObserver. Later calls replace the previous
one.
This is the only way to time the sync built-ins, which are dispatched inside the executor and never reach the function registry. With no observer attached the instrumentation — including its clock reads — stays out of the dispatch path entirely.
Sourcepub fn with_error_context_path(self, path: impl Into<String>) -> Self
pub fn with_error_context_path(self, path: impl Into<String>) -> Self
Mirror per-task failure codes into the message context at path, so a
downstream condition or map can branch on why a task failed.
Off unless called: with no path configured nothing is written and the
mechanism costs one Option check on a path that only runs after a task
has already failed.
One record is appended per error a task contributes to
Message::errors:
{ "workflow_id": "place_order", "task_id": "charge_payment",
"code": "TIMEOUT_ERROR", "status": 500 }so a later task can gate on the reason:
{ "in": [ { "var": "metadata.errors.0.code" },
["TIMEOUT_ERROR", "IO_ERROR"] ] }Coverage matches errors() exactly — a handler returning Err, a task
returning a 5xx outcome, the validation built-in’s per-rule failures, and
anything a handler adds through
TaskContext::add_error
all appear. The workflow-level WORKFLOW_ERROR wrapper does not: it
re-reports the same underlying failure, so mirroring it would double-count.
status is the task’s own status — 500 when the handler returned Err,
otherwise the status the outcome carried (400 for validation). That is
the distinction metadata.progress cannot make, since its failure arm
hard-codes 500.
The error message and the operator-only detail are deliberately not
recorded: the context is serialized back to callers, and detail is
documented as unsafe to hand to an untrusted one. Read those from
message.errors() host-side.
path must start with data, metadata or temp_data — the JSONLogic
evaluation context is exactly those three slots — and may not be
metadata.progress. Violations fail EngineBuilder::build.
Sourcepub fn with_error_context_limit(self, limit: usize) -> Self
pub fn with_error_context_limit(self, limit: usize) -> Self
Cap the number of records retained at the error-context path, keeping the most recent (default 32).
The bound is what keeps the option’s memory cost independent of a looping
workflow’s iteration count: Message.context is deep-cloned into every
trace snapshot, so an uncapped list in a loop with a failing body grows the
trace quadratically. Conditions overwhelmingly read the latest failure, so
the oldest records are the ones dropped.
Setting a limit without a path is inert, not an error. A limit of 0 fails
EngineBuilder::build.
Sourcepub fn with_datalogic_operator<T>(
self,
name: impl Into<String>,
operator: T,
) -> Selfwhere
T: CustomOperator + 'static,
pub fn with_datalogic_operator<T>(
self,
name: impl Into<String>,
operator: T,
) -> Selfwhere
T: CustomOperator + 'static,
Register a custom JSONLogic operator on the engine’s internal datalogic
instance, under name. Later calls with the same name replace the
earlier registration.
This is the host’s door for domain operators: the engine builds (and on
Engine::with_new_workflows rebuilds) its datalogic engine
internally, where registration is builder-only — so operators must
enter here to exist at all, and are retained on the engine so every
hot reload re-registers them.
Semantics follow datalogic_rs: arguments arrive pre-evaluated, and a
built-in operator name always wins over a custom registration — pick
names no built-in uses. Because the engine always runs in templating
mode, a name that is not registered is not an error: the object
echoes back as literal data, exactly like a disabled operator family.
Registering a name therefore converts previously-inert values into
live operator calls, the same caveat the cargo features carry.