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}