use super::config::{
CacheConnectorConfig, ConnectorConfig, ConnectorType, DbConnectorConfig, EsConnectorConfig,
HttpConnectorConfig, KafkaConnectorConfig, SmtpConnectorConfig, StorageConnectorConfig,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PoolSlot {
Sql,
Mongo,
Cache,
Smtp,
}
impl PoolSlot {
pub const ALL: &'static [PoolSlot] = &[
PoolSlot::Sql,
PoolSlot::Mongo,
PoolSlot::Cache,
PoolSlot::Smtp,
];
}
pub trait ConnectorTarget {
type Config;
fn extract(config: &ConnectorConfig) -> Option<&Self::Config>;
fn noun() -> &'static str;
}
pub trait ConnectorKind: ConnectorTarget {
const TYPE: ConnectorType;
}
macro_rules! kinds {
($( $kind:ident => $variant:ident, $config:ty, $noun:literal ; )*) => {
$(
pub struct $kind;
impl ConnectorTarget for $kind {
type Config = $config;
fn extract(config: &ConnectorConfig) -> Option<&Self::Config> {
match config {
ConnectorConfig::$variant(c) => Some(c),
_ => None,
}
}
fn noun() -> &'static str {
$noun
}
}
impl ConnectorKind for $kind {
const TYPE: ConnectorType = ConnectorType::$variant;
}
)*
};
}
kinds! {
Http => Http, HttpConnectorConfig, "an HTTP connector";
Kafka => Kafka, KafkaConnectorConfig, "a Kafka connector";
Db => Db, DbConnectorConfig, "a database connector";
Cache => Cache, CacheConnectorConfig, "a cache connector";
Es => Es, EsConnectorConfig, "an Elasticsearch connector";
Smtp => Smtp, SmtpConnectorConfig, "an SMTP connector";
Storage => Storage, StorageConnectorConfig, "a storage connector";
}
pub struct DataBackend;
impl ConnectorTarget for DataBackend {
type Config = ConnectorConfig;
fn extract(config: &ConnectorConfig) -> Option<&Self::Config> {
matches!(config, ConnectorConfig::Db(_) | ConnectorConfig::Es(_)).then_some(config)
}
fn noun() -> &'static str {
"a db or es connector"
}
}
impl ConnectorType {
pub fn pool_slots(self) -> &'static [PoolSlot] {
match self {
ConnectorType::Db => &[PoolSlot::Sql, PoolSlot::Mongo],
ConnectorType::Cache => &[PoolSlot::Cache],
ConnectorType::Smtp => &[PoolSlot::Smtp],
ConnectorType::Http
| ConnectorType::Kafka
| ConnectorType::Es
| ConnectorType::Storage => &[],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_pool_slot_is_reachable_from_some_kind() {
let claimed: std::collections::BTreeSet<PoolSlot> = [
ConnectorType::Http,
ConnectorType::Kafka,
ConnectorType::Db,
ConnectorType::Cache,
ConnectorType::Es,
ConnectorType::Smtp,
ConnectorType::Storage,
]
.iter()
.flat_map(|t| t.pool_slots().iter().copied())
.collect();
let all: std::collections::BTreeSet<PoolSlot> = PoolSlot::ALL.iter().copied().collect();
assert_eq!(
claimed, all,
"every PoolSlot must be claimed by at least one connector type — an \
unclaimed slot is a pool cache that nothing evicts"
);
}
#[test]
fn a_kind_extracts_only_its_own_variant() {
let cfg = ConnectorConfig::Kafka(KafkaConnectorConfig {
brokers: vec!["localhost:9092".to_string()],
topic: "orders".to_string(),
allow_private_urls: false,
operations: Default::default(),
});
assert!(Kafka::extract(&cfg).is_some());
assert!(
Db::extract(&cfg).is_none(),
"a kafka connector is not a db one"
);
assert_eq!(Db::noun(), "a database connector");
}
#[test]
fn the_data_backend_target_accepts_db_and_es_and_nothing_else() {
let db = ConnectorConfig::Db(DbConnectorConfig {
connection_string: "sqlite::memory:".to_string(),
max_connections: None,
connect_timeout_ms: None,
query_timeout_ms: None,
allow_private_urls: false,
operations: Default::default(),
dialect: Default::default(),
aggregate_write_stages: false,
});
let kafka = ConnectorConfig::Kafka(KafkaConnectorConfig {
brokers: vec!["localhost:9092".to_string()],
topic: "orders".to_string(),
allow_private_urls: false,
operations: Default::default(),
});
assert!(DataBackend::extract(&db).is_some());
assert!(
DataBackend::extract(&kafka).is_none(),
"the portable dialect does not speak Kafka"
);
assert_eq!(DataBackend::noun(), "a db or es connector");
}
#[test]
fn a_db_connector_evicts_both_sql_and_mongo_pools() {
assert_eq!(
ConnectorType::Db.pool_slots(),
&[PoolSlot::Sql, PoolSlot::Mongo]
);
}
}