1use indexmap::IndexMap;
10use serde::{Deserialize, Serialize};
11use smol_str::SmolStr;
12
13use super::{Attribute, Documentation, Ident, Span};
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct ServerGroup {
46 pub name: Ident,
48 pub servers: IndexMap<SmolStr, Server>,
50 pub attributes: Vec<Attribute>,
52 pub documentation: Option<Documentation>,
54 pub span: Span,
56}
57
58impl ServerGroup {
59 pub fn new(name: Ident, span: Span) -> Self {
61 Self {
62 name,
63 servers: IndexMap::new(),
64 attributes: vec![],
65 documentation: None,
66 span,
67 }
68 }
69
70 pub fn add_server(&mut self, server: Server) {
72 self.servers.insert(server.name.name.clone(), server);
73 }
74
75 pub fn add_attribute(&mut self, attr: Attribute) {
77 self.attributes.push(attr);
78 }
79
80 pub fn set_documentation(&mut self, doc: Documentation) {
82 self.documentation = Some(doc);
83 }
84
85 pub fn strategy(&self) -> Option<ServerGroupStrategy> {
87 for attr in &self.attributes {
88 if attr.name.name == "strategy" {
89 if let Some(arg) = attr.args.first() {
90 let value_str = arg.value.as_string()
91 .map(|s| s.to_string())
92 .or_else(|| arg.value.as_ident().map(|s| s.to_string()))?;
93 return ServerGroupStrategy::from_str(&value_str);
94 }
95 }
96 }
97 None
98 }
99
100 pub fn load_balance(&self) -> Option<LoadBalanceStrategy> {
102 for attr in &self.attributes {
103 if attr.name.name == "loadBalance" {
104 if let Some(arg) = attr.args.first() {
105 let value_str = arg.value.as_string()
106 .map(|s| s.to_string())
107 .or_else(|| arg.value.as_ident().map(|s| s.to_string()))?;
108 return LoadBalanceStrategy::from_str(&value_str);
109 }
110 }
111 }
112 None
113 }
114
115 pub fn primary(&self) -> Option<&Server> {
117 self.servers
118 .values()
119 .find(|s| s.role() == Some(ServerRole::Primary))
120 }
121
122 pub fn replicas(&self) -> Vec<&Server> {
124 self.servers
125 .values()
126 .filter(|s| s.role() == Some(ServerRole::Replica))
127 .collect()
128 }
129
130 pub fn servers_in_region(&self, region: &str) -> Vec<&Server> {
132 self.servers
133 .values()
134 .filter(|s| s.region() == Some(region))
135 .collect()
136 }
137
138 pub fn failover_order(&self) -> Vec<&Server> {
140 let mut servers: Vec<_> = self.servers.values().collect();
141 servers.sort_by_key(|s| s.priority().unwrap_or(u32::MAX));
142 servers
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
148pub struct Server {
149 pub name: Ident,
151 pub properties: IndexMap<SmolStr, ServerProperty>,
153 pub span: Span,
155}
156
157impl Server {
158 pub fn new(name: Ident, span: Span) -> Self {
160 Self {
161 name,
162 properties: IndexMap::new(),
163 span,
164 }
165 }
166
167 pub fn add_property(&mut self, prop: ServerProperty) {
169 self.properties.insert(prop.name.clone(), prop);
170 }
171
172 pub fn get_property(&self, name: &str) -> Option<&ServerPropertyValue> {
174 self.properties.get(name).map(|p| &p.value)
175 }
176
177 pub fn url(&self) -> Option<&str> {
179 match self.get_property("url")? {
180 ServerPropertyValue::String(s) => Some(s),
181 ServerPropertyValue::EnvVar(var) => Some(var),
182 _ => None,
183 }
184 }
185
186 pub fn role(&self) -> Option<ServerRole> {
188 match self.get_property("role")? {
189 ServerPropertyValue::String(s) | ServerPropertyValue::Identifier(s) => {
190 ServerRole::from_str(s)
191 }
192 _ => None,
193 }
194 }
195
196 pub fn weight(&self) -> Option<u32> {
198 match self.get_property("weight")? {
199 ServerPropertyValue::Number(n) => Some(*n as u32),
200 _ => None,
201 }
202 }
203
204 pub fn region(&self) -> Option<&str> {
206 match self.get_property("region")? {
207 ServerPropertyValue::String(s) => Some(s),
208 _ => None,
209 }
210 }
211
212 pub fn priority(&self) -> Option<u32> {
214 match self.get_property("priority")? {
215 ServerPropertyValue::Number(n) => Some(*n as u32),
216 _ => None,
217 }
218 }
219
220 pub fn is_read_only(&self) -> bool {
222 match self.get_property("readOnly") {
223 Some(ServerPropertyValue::Boolean(b)) => *b,
224 _ => self.role() == Some(ServerRole::Replica),
225 }
226 }
227
228 pub fn max_connections(&self) -> Option<u32> {
230 match self.get_property("maxConnections")? {
231 ServerPropertyValue::Number(n) => Some(*n as u32),
232 _ => None,
233 }
234 }
235
236 pub fn health_check(&self) -> Option<&str> {
238 match self.get_property("healthCheck")? {
239 ServerPropertyValue::String(s) => Some(s),
240 _ => None,
241 }
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
247pub struct ServerProperty {
248 pub name: SmolStr,
250 pub value: ServerPropertyValue,
252 pub span: Span,
254}
255
256impl ServerProperty {
257 pub fn new(name: impl Into<SmolStr>, value: ServerPropertyValue, span: Span) -> Self {
259 Self {
260 name: name.into(),
261 value,
262 span,
263 }
264 }
265}
266
267#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
269pub enum ServerPropertyValue {
270 String(String),
272 Number(f64),
274 Boolean(bool),
276 Identifier(String),
278 EnvVar(String),
280 Array(Vec<ServerPropertyValue>),
282}
283
284impl std::fmt::Display for ServerPropertyValue {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 match self {
287 Self::String(s) => write!(f, "\"{}\"", s),
288 Self::Number(n) => write!(f, "{}", n),
289 Self::Boolean(b) => write!(f, "{}", b),
290 Self::Identifier(s) => write!(f, "{}", s),
291 Self::EnvVar(var) => write!(f, "env(\"{}\")", var),
292 Self::Array(arr) => {
293 write!(f, "[")?;
294 for (i, v) in arr.iter().enumerate() {
295 if i > 0 {
296 write!(f, ", ")?;
297 }
298 write!(f, "{}", v)?;
299 }
300 write!(f, "]")
301 }
302 }
303 }
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
308pub enum ServerRole {
309 Primary,
311 Replica,
313 Analytics,
315 Archive,
317 Shard,
319}
320
321impl ServerRole {
322 pub fn from_str(s: &str) -> Option<Self> {
324 match s.to_lowercase().as_str() {
325 "primary" | "master" | "writer" => Some(Self::Primary),
326 "replica" | "slave" | "reader" | "read" => Some(Self::Replica),
327 "analytics" | "reporting" | "olap" => Some(Self::Analytics),
328 "archive" | "historical" => Some(Self::Archive),
329 "shard" => Some(Self::Shard),
330 _ => None,
331 }
332 }
333
334 pub fn as_str(&self) -> &'static str {
336 match self {
337 Self::Primary => "primary",
338 Self::Replica => "replica",
339 Self::Analytics => "analytics",
340 Self::Archive => "archive",
341 Self::Shard => "shard",
342 }
343 }
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
348pub enum ServerGroupStrategy {
349 ReadReplica,
351 Sharding,
353 MultiRegion,
355 HighAvailability,
357 Custom,
359}
360
361impl ServerGroupStrategy {
362 pub fn from_str(s: &str) -> Option<Self> {
364 match s.to_lowercase().replace(['-', '_'], "").as_str() {
365 "readreplica" | "replication" => Some(Self::ReadReplica),
366 "sharding" | "shard" | "partition" => Some(Self::Sharding),
367 "multiregion" | "georeplica" | "geographic" => Some(Self::MultiRegion),
368 "highavailability" | "ha" | "failover" => Some(Self::HighAvailability),
369 "custom" => Some(Self::Custom),
370 _ => None,
371 }
372 }
373
374 pub fn as_str(&self) -> &'static str {
376 match self {
377 Self::ReadReplica => "ReadReplica",
378 Self::Sharding => "Sharding",
379 Self::MultiRegion => "MultiRegion",
380 Self::HighAvailability => "HighAvailability",
381 Self::Custom => "Custom",
382 }
383 }
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
388pub enum LoadBalanceStrategy {
389 RoundRobin,
391 Random,
393 LeastConnections,
395 Weighted,
397 Nearest,
399 Sticky,
401}
402
403impl LoadBalanceStrategy {
404 pub fn from_str(s: &str) -> Option<Self> {
406 match s.to_lowercase().replace(['-', '_'], "").as_str() {
407 "roundrobin" | "rr" => Some(Self::RoundRobin),
408 "random" | "rand" => Some(Self::Random),
409 "leastconnections" | "leastconn" | "least" => Some(Self::LeastConnections),
410 "weighted" | "weight" => Some(Self::Weighted),
411 "nearest" | "latency" | "geo" => Some(Self::Nearest),
412 "sticky" | "affinity" | "session" => Some(Self::Sticky),
413 _ => None,
414 }
415 }
416
417 pub fn as_str(&self) -> &'static str {
419 match self {
420 Self::RoundRobin => "RoundRobin",
421 Self::Random => "Random",
422 Self::LeastConnections => "LeastConnections",
423 Self::Weighted => "Weighted",
424 Self::Nearest => "Nearest",
425 Self::Sticky => "Sticky",
426 }
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn test_server_role_from_str() {
436 assert_eq!(ServerRole::from_str("primary"), Some(ServerRole::Primary));
437 assert_eq!(ServerRole::from_str("master"), Some(ServerRole::Primary));
438 assert_eq!(ServerRole::from_str("replica"), Some(ServerRole::Replica));
439 assert_eq!(ServerRole::from_str("slave"), Some(ServerRole::Replica));
440 assert_eq!(
441 ServerRole::from_str("analytics"),
442 Some(ServerRole::Analytics)
443 );
444 assert_eq!(ServerRole::from_str("shard"), Some(ServerRole::Shard));
445 assert_eq!(ServerRole::from_str("invalid"), None);
446 }
447
448 #[test]
449 fn test_server_group_strategy_from_str() {
450 assert_eq!(
451 ServerGroupStrategy::from_str("ReadReplica"),
452 Some(ServerGroupStrategy::ReadReplica)
453 );
454 assert_eq!(
455 ServerGroupStrategy::from_str("sharding"),
456 Some(ServerGroupStrategy::Sharding)
457 );
458 assert_eq!(
459 ServerGroupStrategy::from_str("multi-region"),
460 Some(ServerGroupStrategy::MultiRegion)
461 );
462 assert_eq!(
463 ServerGroupStrategy::from_str("HA"),
464 Some(ServerGroupStrategy::HighAvailability)
465 );
466 }
467
468 #[test]
469 fn test_load_balance_strategy_from_str() {
470 assert_eq!(
471 LoadBalanceStrategy::from_str("RoundRobin"),
472 Some(LoadBalanceStrategy::RoundRobin)
473 );
474 assert_eq!(
475 LoadBalanceStrategy::from_str("rr"),
476 Some(LoadBalanceStrategy::RoundRobin)
477 );
478 assert_eq!(
479 LoadBalanceStrategy::from_str("weighted"),
480 Some(LoadBalanceStrategy::Weighted)
481 );
482 assert_eq!(
483 LoadBalanceStrategy::from_str("nearest"),
484 Some(LoadBalanceStrategy::Nearest)
485 );
486 }
487
488 #[test]
489 fn test_server_property_value_display() {
490 assert_eq!(
491 ServerPropertyValue::String("test".to_string()).to_string(),
492 "\"test\""
493 );
494 assert_eq!(ServerPropertyValue::Number(42.0).to_string(), "42");
495 assert_eq!(ServerPropertyValue::Boolean(true).to_string(), "true");
496 assert_eq!(
497 ServerPropertyValue::Identifier("primary".to_string()).to_string(),
498 "primary"
499 );
500 assert_eq!(
501 ServerPropertyValue::EnvVar("DATABASE_URL".to_string()).to_string(),
502 "env(\"DATABASE_URL\")"
503 );
504 }
505
506 fn test_span() -> Span {
507 Span::new(0, 0)
508 }
509
510 #[test]
511 fn test_server_group_primary_and_replicas() {
512 let mut group = ServerGroup::new(
513 Ident::new("TestCluster", test_span()),
514 test_span(),
515 );
516
517 let mut primary = Server::new(
518 Ident::new("primary", test_span()),
519 test_span(),
520 );
521 primary.add_property(ServerProperty::new(
522 "role",
523 ServerPropertyValue::Identifier("primary".to_string()),
524 test_span(),
525 ));
526 group.add_server(primary);
527
528 let mut replica1 = Server::new(
529 Ident::new("replica1", test_span()),
530 test_span(),
531 );
532 replica1.add_property(ServerProperty::new(
533 "role",
534 ServerPropertyValue::Identifier("replica".to_string()),
535 test_span(),
536 ));
537 group.add_server(replica1);
538
539 let mut replica2 = Server::new(
540 Ident::new("replica2", test_span()),
541 test_span(),
542 );
543 replica2.add_property(ServerProperty::new(
544 "role",
545 ServerPropertyValue::Identifier("replica".to_string()),
546 test_span(),
547 ));
548 group.add_server(replica2);
549
550 assert!(group.primary().is_some());
551 assert_eq!(group.primary().unwrap().name.name.as_str(), "primary");
552 assert_eq!(group.replicas().len(), 2);
553 }
554}
555