crabka_replicator/
config.rs1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::ReplicatorError;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ReplicatorConfig {
12 pub clusters: BTreeMap<String, ClusterConfig>,
14 pub flows: Vec<FlowConfig>,
16 #[serde(default)]
18 pub policies: Vec<PolicyConfig>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ClusterConfig {
24 pub bootstrap: String,
26 pub region: String,
28 #[serde(default)]
30 pub zones: Vec<String>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct FlowConfig {
36 pub from: String,
38 pub to: String,
40 #[serde(default)]
42 pub topics: Selectors,
43 #[serde(default)]
45 pub groups: Selectors,
46 #[serde(default)]
48 pub naming: NamingPolicy,
49 #[serde(default)]
51 pub delivery: Delivery,
52}
53
54#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct Selectors {
57 #[serde(default)]
59 pub include: Vec<String>,
60 #[serde(default)]
62 pub exclude: Vec<String>,
63}
64
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "kebab-case")]
68pub enum NamingPolicy {
69 #[default]
71 Default,
72 Identity,
74}
75
76#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "kebab-case")]
79pub enum Delivery {
80 #[default]
82 AtLeastOnce,
83 ExactlyOnce,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct PolicyConfig {
90 pub name: String,
92 #[serde(default)]
94 pub topics: Vec<String>,
95 #[serde(default)]
97 pub residency: Option<Residency>,
98}
99
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
102pub struct Residency {
103 #[serde(default)]
105 pub allow_zones: Vec<String>,
106 #[serde(default)]
108 pub deny_zones: Vec<String>,
109}
110
111impl ReplicatorConfig {
112 pub fn from_yaml(s: &str) -> Result<Self, ReplicatorError> {
119 serde_yaml::from_str(s).map_err(|e| ReplicatorError::Config(e.to_string()))
120 }
121
122 pub fn validate(&self) -> Result<(), ReplicatorError> {
130 for f in &self.flows {
131 if !self.clusters.contains_key(&f.from) {
132 return Err(ReplicatorError::Config(format!(
133 "flow.from unknown cluster `{}`",
134 f.from
135 )));
136 }
137 if !self.clusters.contains_key(&f.to) {
138 return Err(ReplicatorError::Config(format!(
139 "flow.to unknown cluster `{}`",
140 f.to
141 )));
142 }
143 if f.delivery == Delivery::ExactlyOnce {
144 return Err(ReplicatorError::Config(
145 "delivery `exactly-once` is not supported until Slice 3; use `at-least-once`"
146 .into(),
147 ));
148 }
149 }
150 Ok(())
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use assert2::assert;
157
158 use super::*;
159
160 const YAML: &str = r#"
161clusters:
162 us-east: { bootstrap: "127.0.0.1:9092", region: us, zones: [us] }
163 eu-west: { bootstrap: "127.0.0.1:9093", region: eu, zones: [eu, gdpr] }
164flows:
165 - from: us-east
166 to: eu-west
167 topics: { include: ["orders", "telemetry.*"], exclude: ["*.internal"] }
168 groups: { include: ["analytics-*"] }
169 naming: default
170 delivery: at-least-once
171policies:
172 - name: keep-pii-in-eu
173 topics: ["customers"]
174 residency: { allow_zones: [gdpr] }
175"#;
176
177 #[test]
178 fn parses_and_validates() {
179 let cfg = ReplicatorConfig::from_yaml(YAML).unwrap();
180 assert!(cfg.clusters.len() == 2);
181 assert!(cfg.clusters["eu-west"].zones == vec!["eu".to_string(), "gdpr".to_string()]);
182 assert!(cfg.flows[0].naming == NamingPolicy::Default);
183 assert!(cfg.flows[0].delivery == Delivery::AtLeastOnce);
184 cfg.validate().unwrap();
185 }
186
187 #[test]
188 fn rejects_eos_in_slice1() {
189 let y = YAML.replace("delivery: at-least-once", "delivery: exactly-once");
190 let cfg = ReplicatorConfig::from_yaml(&y).unwrap();
191 let err = cfg.validate().unwrap_err();
192 assert!(format!("{err}").contains("exactly-once"), "got: {err}");
193 }
194
195 #[test]
196 fn rejects_unknown_cluster_ref() {
197 let y = YAML.replace("to: eu-west", "to: nowhere");
198 let cfg = ReplicatorConfig::from_yaml(&y).unwrap();
199 assert!(cfg.validate().is_err());
200 }
201}