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