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