use std::collections::HashMap;
use std::sync::Arc;
use super::functions;
use crate::connector::{ConnectorRegistry, ConnectorType};
pub fn is_known_function(function: &str) -> bool {
match dataflow_rs::builtin_function_kind(function) {
Some(dataflow_rs::BuiltinKind::SelfContained) => true,
_ => super::CUSTOM_HANDLER_FUNCTIONS.contains(&function),
}
}
pub fn known_functions() -> impl Iterator<Item = &'static str> {
dataflow_rs::BUILTIN_FUNCTION_NAMES
.iter()
.copied()
.filter(|name| is_known_function(name))
.chain(
super::CUSTOM_HANDLER_FUNCTIONS
.iter()
.copied()
.filter(|name| !dataflow_rs::is_builtin_function(name)),
)
}
pub const CONNECTOR_FUNCTIONS: &[&str] = &[
"http_call",
"publish_kafka",
"db_read",
"db_write",
"data_query",
"data_write",
"cache_read",
"cache_write",
"mongo_read",
];
pub fn required_connector_types(function: &str) -> Option<&'static [ConnectorType]> {
use ConnectorType::{Cache, Db, Es, Http, Kafka};
Some(match function {
"http_call" => &[Http],
"publish_kafka" => &[Kafka],
"cache_read" | "cache_write" => &[Cache],
"db_read" | "db_write" | "mongo_read" => &[Db],
"data_query" | "data_write" => &[Db, Es],
_ => return None,
})
}
pub fn requires_mongo_database(function: &str) -> bool {
matches!(function, "mongo_read" | "data_query" | "data_write")
}
pub struct HandlerDeps<'a> {
pub registry: Arc<ConnectorRegistry>,
pub client: reqwest::Client,
pub engine: Arc<crate::engine::EngineHandle>,
pub channel_registry: Arc<crate::channel::ChannelRegistry>,
pub engine_config: &'a crate::config::EngineConfig,
pub query_config: &'a crate::config::QueryConfig,
pub write_config: &'a crate::config::WriteConfig,
pub cache_pool: Arc<crate::connector::cache_backend::CachePool>,
pub sql_pool_cache: Arc<crate::connector::pool_cache::SqlPoolCache>,
pub mongo_pool_cache: Arc<crate::connector::mongo_pool::MongoPoolCache>,
}
impl<'a> HandlerDeps<'a> {
pub fn from_state(state: &'a crate::server::state::AppState) -> Self {
Self {
registry: state.connector_registry.clone(),
client: state.http_client.clone(),
engine: state.engine.clone(),
channel_registry: state.channel_registry.clone(),
engine_config: &state.config.engine,
query_config: &state.config.query,
write_config: &state.config.write,
cache_pool: state.caches.cache_pool.clone(),
sql_pool_cache: state.caches.sql_pool_cache.clone(),
mongo_pool_cache: state.caches.mongo_pool_cache.clone(),
}
}
}
pub fn build_custom_functions(
deps: HandlerDeps<'_>,
) -> HashMap<String, dataflow_rs::BoxedFunctionHandler> {
let HandlerDeps {
registry,
client,
engine,
channel_registry,
engine_config,
query_config,
write_config,
cache_pool,
sql_pool_cache,
mongo_pool_cache,
} = deps;
let mut fns: HashMap<String, dataflow_rs::BoxedFunctionHandler> = HashMap::new();
fns.insert(
"http_call".to_string(),
Box::new(functions::http_call::HttpCallHandler {
registry: registry.clone(),
client: client.clone(),
}),
);
fns.insert(
"channel_call".to_string(),
Box::new(functions::channel_call::ChannelCallHandler {
engine,
channel_registry,
max_call_depth: engine_config.max_channel_call_depth,
default_timeout_ms: engine_config.default_channel_call_timeout_ms,
}),
);
fns.insert(
"publish_kafka".to_string(),
Box::new(functions::publish_kafka::PublishKafkaHandler {
registry: registry.clone(),
producers: None,
}),
);
fns.insert(
"db_read".to_string(),
Box::new(functions::db_read::DbReadHandler {
pool_cache: sql_pool_cache.clone(),
registry: registry.clone(),
max_rows: query_config.max_limit as usize,
}),
);
fns.insert(
"db_write".to_string(),
Box::new(functions::db_write::DbWriteHandler {
pool_cache: sql_pool_cache.clone(),
registry: registry.clone(),
}),
);
fns.insert(
"data_query".to_string(),
Box::new(functions::data_query::DataQueryHandler {
pool_cache: sql_pool_cache.clone(),
mongo_pool_cache: mongo_pool_cache.clone(),
http_client: client.clone(),
registry: registry.clone(),
limits: query_config.clone(),
}),
);
fns.insert(
"data_write".to_string(),
Box::new(functions::data_write::DataWriteHandler {
pool_cache: sql_pool_cache,
mongo_pool_cache: mongo_pool_cache.clone(),
http_client: client.clone(),
registry: registry.clone(),
write_config: write_config.clone(),
}),
);
fns.insert(
"cache_read".to_string(),
Box::new(functions::cache_read::CacheReadHandler {
cache_pool: cache_pool.clone(),
registry: registry.clone(),
}),
);
fns.insert(
"cache_write".to_string(),
Box::new(functions::cache_write::CacheWriteHandler {
cache_pool,
registry: registry.clone(),
}),
);
fns.insert(
"mongo_read".to_string(),
Box::new(functions::mongo_read::MongoReadHandler {
pool_cache: mongo_pool_cache,
registry: registry.clone(),
max_rows: query_config.max_limit as usize,
}),
);
fns
}
pub fn register_kafka_publisher(
fns: &mut HashMap<String, dataflow_rs::BoxedFunctionHandler>,
registry: Arc<ConnectorRegistry>,
producers: Arc<crate::kafka::producer::KafkaProducerCache>,
) {
fns.insert(
"publish_kafka".to_string(),
Box::new(functions::publish_kafka::PublishKafkaHandler {
registry,
producers: Some(producers),
}),
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::loader::CUSTOM_HANDLER_FUNCTIONS;
use dataflow_rs::BuiltinKind;
#[test]
fn every_self_contained_builtin_is_accepted() {
let mut checked = 0;
for name in dataflow_rs::BUILTIN_FUNCTION_NAMES {
if dataflow_rs::builtin_function_kind(name) == Some(BuiltinKind::SelfContained) {
assert!(
is_known_function(name),
"'{name}' runs with no registration, so rejecting it at create \
refuses a workflow the engine would happily execute"
);
checked += 1;
}
}
assert!(checked >= 8, "implausibly few self-contained built-ins");
}
#[test]
fn a_builtin_needing_a_handler_is_accepted_only_when_one_is_registered() {
for name in dataflow_rs::BUILTIN_FUNCTION_NAMES {
if dataflow_rs::builtin_function_kind(name) != Some(BuiltinKind::RequiresHandler) {
continue;
}
assert_eq!(
is_known_function(name),
CUSTOM_HANDLER_FUNCTIONS.contains(name),
"'{name}' needs a registered handler; accepting it without one \
green-lights a workflow that 500s on every request"
);
}
assert!(is_known_function("http_call"));
assert!(is_known_function("publish_kafka"));
assert!(
!is_known_function("enrich"),
"Orion registers no `enrich` handler, so the name must be refused \
at create rather than at every request"
);
}
#[test]
fn every_registered_custom_handler_is_accepted() {
for name in CUSTOM_HANDLER_FUNCTIONS {
assert!(
is_known_function(name),
"handler '{name}' is registered but the gate rejects it, \
so workflows using it are refused at create"
);
}
assert!(!is_known_function("__not_a_function__"));
}
#[test]
fn every_connector_function_declares_its_connector_types() {
for f in CONNECTOR_FUNCTIONS {
let types = required_connector_types(f).unwrap_or_default();
assert!(
!types.is_empty(),
"'{f}' takes a connector but declares no connector type, so activation \
cannot check it (proposal F52)"
);
}
for f in known_functions() {
if !CONNECTOR_FUNCTIONS.contains(&f) {
assert!(
required_connector_types(f).is_none(),
"'{f}' declares connector types but takes no connector"
);
}
}
}
}