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    /// The engine calls [`Self::parse_input_with`], whose default delegates
109    /// here; override that one instead when the parse depends on the instance.
110    ///
111    /// Built-in async function variants (`HttpCall`, `Enrich`,
112    /// `PublishKafka`) bypass this method — their typed configs are already
113    /// parsed by `serde(untagged)` on `FunctionConfig` and dispatched
114    /// directly to the registered handler.
115    fn parse_input(input: &Value) -> Result<Self::Input> {
116        serde_json::from_value(input.clone()).map_err(DataflowError::from_serde)
117    }
118
119    /// Receiver-taking form of [`Self::parse_input`], and the one the engine
120    /// calls — once per task at `Engine::new` / `Engine::builder().build()` /
121    /// `Engine::with_new_workflows`, and from
122    /// [`EngineBuilder::check_workflow`](crate::EngineBuilder::check_workflow).
123    /// The default delegates to `parse_input`, so overriding both leaves
124    /// `parse_input` unreached unless this calls it.
125    ///
126    /// Override it when the parse depends on `self`: one handler type
127    /// registered under several names, each carrying its own schema — a
128    /// plugin host with one instance per manifest function, say.
129    ///
130    /// # Errors
131    ///
132    /// As [`Self::parse_input`].
133    fn parse_input_with(&self, input: &Value) -> Result<Self::Input> {
134        <Self as AsyncFunctionHandler>::parse_input(input)
135    }
136
137    /// Compile the [`Template`] fields of a just-parsed input.
138    ///
139    /// Called once per task at engine construction, immediately after
140    /// [`Self::parse_input_with`]. The default is a no-op, so a handler with no
141    /// `Template` fields needs no implementation.
142    ///
143    /// The engine calls [`Self::compile_input_with`], whose default delegates
144    /// here; override that one instead when *which* fields are templates
145    /// depends on the instance.
146    ///
147    /// A malformed expression fails here — at `Engine::new` / `Engine::builder().build()`
148    /// / `Engine::with_new_workflows` — rather than on the first message that
149    /// reaches the task, matching the crate's existing stance for the built-in
150    /// `*_logic` fields. A host that loads workflows from a database and must
151    /// not let one bad row take the whole process down needs a per-row
152    /// pre-check before activation; this method does not change that trade-off,
153    /// only makes it apply to custom handlers too.
154    ///
155    /// # Errors
156    ///
157    /// Propagate whatever [`Template::compile`] returns — typically
158    /// [`crate::DataflowError::LogicEvaluation`].
159    fn compile_input(_input: &mut Self::Input, _c: &TemplateCompiler) -> Result<()> {
160        Ok(())
161    }
162
163    /// Receiver-taking form of [`Self::compile_input`], same rule as
164    /// [`Self::parse_input_with`]: the engine calls this one, the default
165    /// delegates, and overriding both leaves `compile_input` unreached unless
166    /// this calls it. Override it when the set of template positions is
167    /// per-registration data rather than a property of `Self::Input`.
168    ///
169    /// # Errors
170    ///
171    /// As [`Self::compile_input`].
172    fn compile_input_with(&self, input: &mut Self::Input, c: &TemplateCompiler) -> Result<()> {
173        <Self as AsyncFunctionHandler>::compile_input(input, c)
174    }
175
176    /// Execute the handler. The `ctx` accumulates audit-trail changes
177    /// pushed via its `set` family; the workflow executor folds them into
178    /// the audit trail when this method returns.
179    async fn execute(&self, ctx: &mut TaskContext<'_>, input: &Self::Input) -> Result<TaskOutcome>;
180}
181
182/// Object-safe sibling of [`AsyncFunctionHandler`]. Engine-internal — users
183/// should not implement this directly; the blanket impl below derives it
184/// for any `AsyncFunctionHandler`. Exposed (rather than `pub(crate)`) only
185/// because [`BoxedFunctionHandler`] mentions it in its public type alias.
186#[doc(hidden)]
187#[async_trait]
188pub trait DynAsyncFunctionHandler: Send + Sync + 'static {
189    /// Pre-parse the raw JSON input into the handler's typed shape and box
190    /// it as `dyn Any`. Called once per task at `Engine::new()` time.
191    fn parse_input_box(&self, input: &Value) -> Result<Box<dyn Any + Send + Sync>>;
192
193    /// Compile the [`Template`] fields of an already-parsed boxed input, in
194    /// place. Defaulted to a no-op so a hand-written impl of this
195    /// `#[doc(hidden)]` trait — which is not expected to exist — keeps
196    /// compiling regardless.
197    fn compile_input_box(
198        &self,
199        _boxed: &mut (dyn Any + Send + Sync),
200        _c: &TemplateCompiler,
201    ) -> Result<()> {
202        Ok(())
203    }
204
205    /// Execute against an already-parsed typed input. The implementation
206    /// downcasts `input` to `<Self as AsyncFunctionHandler>::Input`; the
207    /// downcast is infallible in the engine's call paths because
208    /// `parse_input_box` produced the very same type.
209    async fn dyn_execute(
210        &self,
211        ctx: &mut TaskContext<'_>,
212        input: &(dyn Any + Send + Sync),
213    ) -> Result<TaskOutcome>;
214}
215
216#[async_trait]
217impl<F: AsyncFunctionHandler> DynAsyncFunctionHandler for F {
218    fn parse_input_box(&self, input: &Value) -> Result<Box<dyn Any + Send + Sync>> {
219        let typed = <F as AsyncFunctionHandler>::parse_input_with(self, input)?;
220        Ok(Box::new(typed))
221    }
222
223    fn compile_input_box(
224        &self,
225        boxed: &mut (dyn Any + Send + Sync),
226        c: &TemplateCompiler,
227    ) -> Result<()> {
228        let typed = boxed.downcast_mut::<F::Input>().ok_or_else(|| {
229            DataflowError::Validation(format!(
230                "Handler input type mismatch (expected {})",
231                std::any::type_name::<F::Input>()
232            ))
233        })?;
234        <F as AsyncFunctionHandler>::compile_input_with(self, typed, c)
235    }
236
237    async fn dyn_execute(
238        &self,
239        ctx: &mut TaskContext<'_>,
240        input: &(dyn Any + Send + Sync),
241    ) -> Result<TaskOutcome> {
242        let typed = input.downcast_ref::<F::Input>().ok_or_else(|| {
243            DataflowError::Validation(format!(
244                "Handler input type mismatch (expected {})",
245                std::any::type_name::<F::Input>()
246            ))
247        })?;
248        AsyncFunctionHandler::execute(self, ctx, typed).await
249    }
250}
251
252/// Boxed handler stored in the engine's function registry. Users construct
253/// these with `Box::new(MyHandler)` — the blanket impl above auto-coerces
254/// any `AsyncFunctionHandler` into `Box<dyn DynAsyncFunctionHandler + Send + Sync>`.
255pub type BoxedFunctionHandler = Box<dyn DynAsyncFunctionHandler + Send + Sync>;
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::engine::compiler::LogicCompiler;
261
262    /// Overrides both forms of both hooks, with the associated ones refusing.
263    /// The boxed dispatch is the only path the engine has to a handler, so
264    /// reaching the receiver forms through it — and never the associated
265    /// ones — is the whole precedence rule.
266    struct BothForms;
267
268    #[async_trait]
269    impl AsyncFunctionHandler for BothForms {
270        type Input = Value;
271
272        fn parse_input(_input: &Value) -> Result<Self::Input> {
273            Err(DataflowError::Validation(
274                "the associated parse_input must not be reached".to_string(),
275            ))
276        }
277
278        fn parse_input_with(&self, input: &Value) -> Result<Self::Input> {
279            Ok(input.clone())
280        }
281
282        fn compile_input(_input: &mut Self::Input, _c: &TemplateCompiler) -> Result<()> {
283            Err(DataflowError::Validation(
284                "the associated compile_input must not be reached".to_string(),
285            ))
286        }
287
288        fn compile_input_with(
289            &self,
290            _input: &mut Self::Input,
291            _c: &TemplateCompiler,
292        ) -> Result<()> {
293            Ok(())
294        }
295
296        async fn execute(
297            &self,
298            _ctx: &mut TaskContext<'_>,
299            _input: &Self::Input,
300        ) -> Result<TaskOutcome> {
301            Ok(TaskOutcome::Success)
302        }
303    }
304
305    #[test]
306    fn the_boxed_dispatch_calls_the_receiver_forms_and_never_the_associated_ones() {
307        let handler: BoxedFunctionHandler = Box::new(BothForms);
308        let compiler = TemplateCompiler::new(LogicCompiler::new().engine());
309
310        let mut parsed = handler
311            .parse_input_box(&Value::Null)
312            .expect("parse_input_box routes to parse_input_with");
313        handler
314            .compile_input_box(&mut *parsed, &compiler)
315            .expect("compile_input_box routes to compile_input_with");
316    }
317}