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>;