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