1use std::path::PathBuf;
12
13use serde::{Deserialize, Serialize};
14use validator::Validate;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(tag = "type", rename_all = "lowercase")]
50pub enum DiscoveryConfig {
51 Etcd(EtcdDiscoveryConfig),
53 P2p(P2pDiscoveryConfig),
55 Filesystem(FilesystemDiscoveryConfig),
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
61pub struct EtcdDiscoveryConfig {
62 pub cluster_id: String,
64
65 #[serde(default = "default_etcd_endpoints")]
67 pub endpoints: Vec<String>,
68
69 #[serde(default = "default_etcd_ttl")]
71 #[validate(range(min = 10, max = 600))]
72 pub ttl_secs: u64,
73
74 #[serde(default = "default_operation_timeout")]
76 pub operation_timeout_secs: u64,
77
78 #[serde(default = "default_max_retries")]
80 #[validate(range(min = 0, max = 10))]
81 pub max_retries: u32,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
86pub struct P2pDiscoveryConfig {
87 pub cluster_id: String,
89
90 #[serde(default)]
92 pub listen_port: u16,
93
94 #[serde(default)]
96 pub bootstrap_peers: Vec<String>,
97
98 #[serde(default = "default_replication_factor")]
100 pub replication_factor: usize,
101
102 #[serde(default)]
104 pub enable_mdns: bool,
105
106 #[serde(default = "default_record_ttl")]
108 pub record_ttl_secs: u64,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct FilesystemDiscoveryConfig {
114 pub path: PathBuf,
116}
117
118fn default_etcd_endpoints() -> Vec<String> {
119 vec!["http://localhost:2379".to_string()]
120}
121
122fn default_etcd_ttl() -> u64 {
123 60
124}
125
126fn default_operation_timeout() -> u64 {
127 30
128}
129
130fn default_max_retries() -> u32 {
131 3
132}
133
134fn default_replication_factor() -> usize {
135 3
136}
137
138fn default_record_ttl() -> u64 {
139 600
140}
141
142impl Default for EtcdDiscoveryConfig {
143 fn default() -> Self {
144 Self {
145 cluster_id: String::new(),
146 endpoints: default_etcd_endpoints(),
147 ttl_secs: default_etcd_ttl(),
148 operation_timeout_secs: default_operation_timeout(),
149 max_retries: default_max_retries(),
150 }
151 }
152}
153
154impl Default for P2pDiscoveryConfig {
155 fn default() -> Self {
156 Self {
157 cluster_id: String::new(),
158 listen_port: 0,
159 bootstrap_peers: Vec::new(),
160 replication_factor: default_replication_factor(),
161 enable_mdns: false,
162 record_ttl_secs: default_record_ttl(),
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_deserialize_etcd_config() {
173 let json = r#"{
174 "type": "etcd",
175 "cluster_id": "test-cluster",
176 "endpoints": ["http://etcd1:2379"],
177 "ttl_secs": 120
178 }"#;
179
180 let config: DiscoveryConfig = serde_json::from_str(json).unwrap();
181 match config {
182 DiscoveryConfig::Etcd(etcd) => {
183 assert_eq!(etcd.cluster_id, "test-cluster");
184 assert_eq!(etcd.endpoints, vec!["http://etcd1:2379"]);
185 assert_eq!(etcd.ttl_secs, 120);
186 assert_eq!(etcd.operation_timeout_secs, 30); assert_eq!(etcd.max_retries, 3); }
189 _ => panic!("Expected Etcd config"),
190 }
191 }
192
193 #[test]
194 fn test_deserialize_p2p_config() {
195 let json = r#"{
196 "type": "p2p",
197 "cluster_id": "test-cluster",
198 "listen_port": 4001,
199 "bootstrap_peers": ["192.168.1.10:4001"],
200 "enable_mdns": true
201 }"#;
202
203 let config: DiscoveryConfig = serde_json::from_str(json).unwrap();
204 match config {
205 DiscoveryConfig::P2p(p2p) => {
206 assert_eq!(p2p.cluster_id, "test-cluster");
207 assert_eq!(p2p.listen_port, 4001);
208 assert_eq!(p2p.bootstrap_peers, vec!["192.168.1.10:4001"]);
209 assert!(p2p.enable_mdns);
210 assert_eq!(p2p.replication_factor, 3); assert_eq!(p2p.record_ttl_secs, 600); }
213 _ => panic!("Expected P2p config"),
214 }
215 }
216
217 #[test]
218 fn test_deserialize_filesystem_config() {
219 let json = r#"{
220 "type": "filesystem",
221 "path": "/tmp/discovery.json"
222 }"#;
223
224 let config: DiscoveryConfig = serde_json::from_str(json).unwrap();
225 match config {
226 DiscoveryConfig::Filesystem(fs) => {
227 assert_eq!(fs.path, PathBuf::from("/tmp/discovery.json"));
228 }
229 _ => panic!("Expected Filesystem config"),
230 }
231 }
232
233 #[test]
234 fn test_serialize_etcd_config() {
235 let config = DiscoveryConfig::Etcd(EtcdDiscoveryConfig {
236 cluster_id: "my-cluster".to_string(),
237 endpoints: vec!["http://localhost:2379".to_string()],
238 ttl_secs: 60,
239 operation_timeout_secs: 30,
240 max_retries: 3,
241 });
242
243 let json = serde_json::to_string(&config).unwrap();
244 assert!(json.contains(r#""type":"etcd""#));
245 assert!(json.contains(r#""cluster_id":"my-cluster""#));
246 }
247
248 #[test]
249 fn test_etcd_default() {
250 let config = EtcdDiscoveryConfig::default();
251 assert!(config.cluster_id.is_empty());
252 assert_eq!(config.endpoints, vec!["http://localhost:2379"]);
253 assert_eq!(config.ttl_secs, 60);
254 assert_eq!(config.operation_timeout_secs, 30);
255 assert_eq!(config.max_retries, 3);
256 }
257
258 #[test]
259 fn test_p2p_default() {
260 let config = P2pDiscoveryConfig::default();
261 assert!(config.cluster_id.is_empty());
262 assert_eq!(config.listen_port, 0);
263 assert!(config.bootstrap_peers.is_empty());
264 assert_eq!(config.replication_factor, 3);
265 assert!(!config.enable_mdns);
266 assert_eq!(config.record_ttl_secs, 600);
267 }
268}