Skip to main content

dataflow_rs/engine/functions/
mod.rs

1use crate::engine::error::{DataflowError, Result};
2use crate::engine::task_context::TaskContext;
3use crate::engine::task_outcome::TaskOutcome;
4use async_trait::async_trait;
5use serde::de::DeserializeOwned;
6use serde_json::Value;
7use std::any::Any;
8
9pub mod config;
10pub use config::{
11    BUILTIN_FUNCTION_NAMES, BuiltinKind, CompiledCustomInput, ConnectorName, DispatchableFunction,
12    FunctionConfig, builtin_function_kind, is_builtin_function,
13};
14
15pub mod validation;
16pub use validation::{ValidationConfig, ValidationRule};
17
18pub mod map;
19pub use map::{MapConfig, MapMapping};
20
21pub mod parse;
22pub use parse::ParseConfig;
23
24pub mod publish;
25pub use publish::PublishConfig;
26
27pub mod filter;
28pub use filter::{FilterConfig, RejectAction};
29
30pub mod log;
31pub use log::{LogConfig, LogLevel};
32
33pub mod integration;
34pub use integration::{EnrichConfig, HttpCallConfig, HttpMethod, PublishKafkaConfig};
35
36pub mod template;
37pub use template::{Template, TemplateCompiler};
38
39pub mod path_template;
40pub use path_template::{ContextRoot, DataRoot, PathRoot, PathTemplate, ResolvedPath};
41
42/// Async interface for task functions that operate on messages.
43///
44/// Implement this trait for custom processing logic. The trait associates a
45/// typed `Input` deserialized from the task's `FunctionConfig` so that
46/// handlers receive their config already parsed — no `match
47/// FunctionConfig::Custom { input, .. }` boilerplate, no per-call
48/// `serde_json::from_value` cost in the hot path. The engine deserializes the
49/// `Custom.input` JSON exactly once at `Engine::new()` time and caches the
50/// typed value alongside the task; mismatched config shapes therefore fail
51/// at startup rather than on first message.
52///
53/// Handlers mutate the message via [`TaskContext`] — its `set` family records
54/// changes on the audit trail automatically when `message.capture_changes`
55/// is enabled, so handlers don't have to hand-build [`crate::engine::message::Change`]
56/// entries.
57///
58/// ## Example
59///
60/// ```rust,no_run
61/// use async_trait::async_trait;
62/// use dataflow_rs::{
63///     AsyncFunctionHandler, Result, TaskContext, TaskOutcome,
64/// };
65/// use datavalue::OwnedDataValue;
66/// use serde::Deserialize;
67///
68/// #[derive(Deserialize)]
69/// struct StatsInput {
70///     data_path: String,
71///     output_path: String,
72/// }
73///
74/// struct StatisticsFunction;
75///
76/// #[async_trait]
77/// impl AsyncFunctionHandler for StatisticsFunction {
78///     type Input = StatsInput;
79///
80///     async fn execute(
81///         &self,
82///         ctx: &mut TaskContext<'_>,
83///         input: &StatsInput,
84///     ) -> Result<TaskOutcome> {
85///         let count = ctx.data()
86///             .get(input.data_path.as_str())
87///             .and_then(|v| v.as_array())
88///             .map(|a| a.len())
89///             .unwrap_or(0);
90///         ctx.set(
91///             &format!("data.{}.count", input.output_path),
92///             OwnedDataValue::from(&serde_json::json!(count)),
93///         );
94///         Ok(TaskOutcome::Success)
95///     }
96/// }
97/// ```
98#[async_trait]
99pub trait AsyncFunctionHandler: Send + Sync + 'static {
100    /// Typed configuration shape for this handler. Use
101    /// `serde_json::Value` for handlers that take freeform JSON.
102    type Input: DeserializeOwned + Send + Sync + 'static;
103
104    /// Parse the raw `FunctionConfig::Custom { input }` JSON into
105    /// `Self::Input`. Default impl uses `serde_json::from_value`. Override
106    /// only if you need custom validation beyond what serde provides.
107    ///
108    /// Built-in async function variants (`HttpCall`, `Enrich`,
109    /// `PublishKafka`) bypass this method — their typed configs are already
110    /// parsed by `serde(untagged)` on `FunctionConfig` and dispatched
111    /// directly to the registered handler.
112    fn parse_input(input: &Value) -> Result<Self::Input> {
113        serde_json::from_value(input.clone()).map_err(DataflowError::from_serde)
114    }
115
116    /// Compile the [`Template`] fields of a just-parsed input.
117    ///
118    /// Called once per task at engine construction, immediately after
119    /// [`Self::parse_input`]. The default is a no-op, so a handler with no
120    /// `Template` fields needs no implementation.
121    ///
122    /// A malformed expression fails here — at `Engine::new` / `Engine::builder().build()`
123    /// / `Engine::with_new_workflows` — rather than on the first message that
124    /// reaches the task, matching the crate's existing stance for the built-in
125    /// `*_logic` fields. A host that loads workflows from a database and must
126    /// not let one bad row take the whole process down needs a per-row
127    /// pre-check before activation; this method does not change that trade-off,
128    /// only makes it apply to custom handlers too.
129    ///
130    /// # Errors
131    ///
132    /// Propagate whatever [`Template::compile`] returns — typically
133    /// [`crate::DataflowError::LogicEvaluation`].
134    fn compile_input(_input: &mut Self::Input, _c: &TemplateCompiler) -> Result<()> {
135        Ok(())
136    }
137
138    /// Execute the handler. The `ctx` accumulates audit-trail changes
139    /// pushed via its `set` family; the workflow executor folds them into
140    /// the audit trail when this method returns.
141    async fn execute(&self, ctx: &mut TaskContext<'_>, input: &Self::Input) -> Result<TaskOutcome>;
142}
143
144/// Object-safe sibling of [`AsyncFunctionHandler`]. Engine-internal — users
145/// should not implement this directly; the blanket impl below derives it
146/// for any `AsyncFunctionHandler`. Exposed (rather than `pub(crate)`) only
147/// because [`BoxedFunctionHandler`] mentions it in its public type alias.
148#[doc(hidden)]
149#[async_trait]
150pub trait DynAsyncFunctionHandler: Send + Sync + 'static {
151    /// Pre-parse the raw JSON input into the handler's typed shape and box
152    /// it as `dyn Any`. Called once per task at `Engine::new()` time.
153    fn parse_input_box(&self, input: &Value) -> Result<Box<dyn Any + Send + Sync>>;
154
155    /// Compile the [`Template`] fields of an already-parsed boxed input, in
156    /// place. Defaulted to a no-op so a hand-written impl of this
157    /// `#[doc(hidden)]` trait — which is not expected to exist — keeps
158    /// compiling regardless.
159    fn compile_input_box(
160        &self,
161        _boxed: &mut (dyn Any + Send + Sync),
162        _c: &TemplateCompiler,
163    ) -> Result<()> {
164        Ok(())
165    }
166
167    /// Execute against an already-parsed typed input. The implementation
168    /// downcasts `input` to `<Self as AsyncFunctionHandler>::Input`; the
169    /// downcast is infallible in the engine's call paths because
170    /// `parse_input_box` produced the very same type.
171    async fn dyn_execute(
172        &self,
173        ctx: &mut TaskContext<'_>,
174        input: &(dyn Any + Send + Sync),
175    ) -> Result<TaskOutcome>;
176}
177
178#[async_trait]
179impl<F: AsyncFunctionHandler> DynAsyncFunctionHandler for F {
180    fn parse_input_box(&self, input: &Value) -> Result<Box<dyn Any + Send + Sync>> {
181        let typed = <F as AsyncFunctionHandler>::parse_input(input)?;
182        Ok(Box::new(typed))
183    }
184
185    fn compile_input_box(
186        &self,
187        boxed: &mut (dyn Any + Send + Sync),
188        c: &TemplateCompiler,
189    ) -> Result<()> {
190        let typed = boxed.downcast_mut::<F::Input>().ok_or_else(|| {
191            DataflowError::Validation(format!(
192                "Handler input type mismatch (expected {})",
193                std::any::type_name::<F::Input>()
194            ))
195        })?;
196        <F as AsyncFunctionHandler>::compile_input(typed, c)
197    }
198
199    async fn dyn_execute(
200        &self,
201        ctx: &mut TaskContext<'_>,
202        input: &(dyn Any + Send + Sync),
203    ) -> Result<TaskOutcome> {
204        let typed = input.downcast_ref::<F::Input>().ok_or_else(|| {
205            DataflowError::Validation(format!(
206                "Handler input type mismatch (expected {})",
207                std::any::type_name::<F::Input>()
208            ))
209        })?;
210        AsyncFunctionHandler::execute(self, ctx, typed).await
211    }
212}
213
214/// Boxed handler stored in the engine's function registry. Users construct
215/// these with `Box::new(MyHandler)` — the blanket impl above auto-coerces
216/// any `AsyncFunctionHandler` into `Box<dyn DynAsyncFunctionHandler + Send + Sync>`.
217pub type BoxedFunctionHandler = Box<dyn DynAsyncFunctionHandler + Send + Sync>;