use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::error::ReplicatorError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplicatorConfig {
pub clusters: BTreeMap<String, ClusterConfig>,
pub flows: Vec<FlowConfig>,
#[serde(default)]
pub policies: Vec<PolicyConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClusterConfig {
pub bootstrap: String,
pub region: String,
#[serde(default)]
pub zones: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowConfig {
pub from: String,
pub to: String,
#[serde(default)]
pub topics: Selectors,
#[serde(default)]
pub groups: Selectors,
#[serde(default)]
pub naming: NamingPolicy,
#[serde(default)]
pub delivery: Delivery,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Selectors {
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum NamingPolicy {
#[default]
Default,
Identity,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Delivery {
#[default]
AtLeastOnce,
ExactlyOnce,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyConfig {
pub name: String,
#[serde(default)]
pub topics: Vec<String>,
#[serde(default)]
pub residency: Option<Residency>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Residency {
#[serde(default)]
pub allow_zones: Vec<String>,
#[serde(default)]
pub deny_zones: Vec<String>,
}
impl ReplicatorConfig {
pub fn from_yaml(s: &str) -> Result<Self, ReplicatorError> {
serde_yaml::from_str(s).map_err(|e| ReplicatorError::Config(e.to_string()))
}
pub fn validate(&self) -> Result<(), ReplicatorError> {
for f in &self.flows {
if !self.clusters.contains_key(&f.from) {
return Err(ReplicatorError::Config(format!(
"flow.from unknown cluster `{}`",
f.from
)));
}
if !self.clusters.contains_key(&f.to) {
return Err(ReplicatorError::Config(format!(
"flow.to unknown cluster `{}`",
f.to
)));
}
if f.delivery == Delivery::ExactlyOnce {
return Err(ReplicatorError::Config(
"delivery `exactly-once` is not supported until Slice 3; use `at-least-once`"
.into(),
));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use assert2::assert;
use super::*;
const YAML: &str = r#"
clusters:
us-east: { bootstrap: "127.0.0.1:9092", region: us, zones: [us] }
eu-west: { bootstrap: "127.0.0.1:9093", region: eu, zones: [eu, gdpr] }
flows:
- from: us-east
to: eu-west
topics: { include: ["orders", "telemetry.*"], exclude: ["*.internal"] }
groups: { include: ["analytics-*"] }
naming: default
delivery: at-least-once
policies:
- name: keep-pii-in-eu
topics: ["customers"]
residency: { allow_zones: [gdpr] }
"#;
#[test]
fn parses_and_validates() {
let cfg = ReplicatorConfig::from_yaml(YAML).unwrap();
assert!(cfg.clusters.len() == 2);
assert!(cfg.clusters["eu-west"].zones == vec!["eu".to_string(), "gdpr".to_string()]);
assert!(cfg.flows[0].naming == NamingPolicy::Default);
assert!(cfg.flows[0].delivery == Delivery::AtLeastOnce);
cfg.validate().unwrap();
}
#[test]
fn rejects_eos_in_slice1() {
let y = YAML.replace("delivery: at-least-once", "delivery: exactly-once");
let cfg = ReplicatorConfig::from_yaml(&y).unwrap();
let err = cfg.validate().unwrap_err();
assert!(format!("{err}").contains("exactly-once"), "got: {err}");
}
#[test]
fn rejects_unknown_cluster_ref() {
let y = YAML.replace("to: eu-west", "to: nowhere");
let cfg = ReplicatorConfig::from_yaml(&y).unwrap();
assert!(cfg.validate().is_err());
}
}