use crate::error::ValueError;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct ScopeEntry {
pub prefix: Vec<u8>,
pub writer: String,
pub fallback: Option<String>,
}
impl ScopeEntry {
#[must_use]
pub fn new(prefix: Vec<u8>, writer: String) -> Self {
Self { prefix, writer, fallback: None }
}
#[must_use]
pub fn with_fallback(mut self, fallback: String) -> Self {
self.fallback = Some(fallback);
self
}
pub fn to_token(&self) -> String {
let prefix = String::from_utf8_lossy(&self.prefix);
match &self.fallback {
Some(fb) => format!("{prefix}={}|{fb}", self.writer),
None => format!("{prefix}={}", self.writer),
}
}
pub fn parse_one(token: &str) -> Option<Self> {
if token.contains(',') {
return None;
}
let (prefix, owners) = token.split_once('=')?;
if prefix.is_empty() || owners.is_empty() {
return None;
}
let (writer, fallback) = match owners.split_once('|') {
Some((w, f)) if !w.is_empty() && !f.is_empty() => (w, Some(f.to_string())),
Some(_) => return None, None => (owners, None),
};
Some(ScopeEntry {
prefix: prefix.as_bytes().to_vec(),
writer: writer.to_string(),
fallback,
})
}
pub fn parse_list(s: &str) -> Result<Vec<ScopeEntry>, ValueError> {
let mut out = Vec::new();
for raw in s.split(',') {
let token = raw.trim();
if token.is_empty() {
continue;
}
match Self::parse_one(token) {
Some(p) => out.push(p),
None => return Err(ValueError::new(format!("bad scope token: {token:?}"))),
}
}
Ok(out)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct PeerEntry {
pub node_id: String,
pub host: String,
pub port: u16,
pub client_port: Option<u16>,
pub repl_port_base: Option<u16>,
}
impl PeerEntry {
#[must_use]
pub fn new(node_id: String, host: String, port: u16) -> Self {
Self { node_id, host, port, client_port: None, repl_port_base: None }
}
#[must_use]
pub fn with_client_port(mut self, client_port: u16) -> Self {
self.client_port = Some(client_port);
self
}
#[must_use]
pub fn with_repl_port_base(mut self, repl_port_base: u16) -> Self {
self.repl_port_base = Some(repl_port_base);
self
}
pub fn to_token(&self) -> String {
let mut t = format!("{}@{}:{}", self.node_id, self.host, self.port);
if let Some(cp) = self.client_port {
t.push_str(&format!(":{cp}"));
if let Some(rb) = self.repl_port_base {
t.push_str(&format!(":{rb}"));
}
}
t
}
pub fn parse_one(token: &str) -> Option<Self> {
let (node_id, rest) = token.split_once('@')?;
let mut fields = rest.split(':');
let host = fields.next()?;
if node_id.is_empty() || host.is_empty() {
return None;
}
let ports = fields.map(|f| f.parse::<u16>().ok()).collect::<Option<Vec<u16>>>()?;
let (port, client_port, repl_port_base) = match ports[..] {
[elect] => (elect, None, None),
[elect, client] => (elect, Some(client), None),
[elect, client, repl] => (elect, Some(client), Some(repl)),
_ => return None,
};
Some(PeerEntry {
node_id: node_id.to_string(),
host: host.to_string(),
port,
client_port,
repl_port_base,
})
}
pub fn parse_list(s: &str) -> Result<Vec<PeerEntry>, ValueError> {
let mut out = Vec::new();
for raw in s.split(',') {
let token = raw.trim();
if token.is_empty() {
continue;
}
match Self::parse_one(token) {
Some(p) => out.push(p),
None => return Err(ValueError::new(format!("bad peer token: {token:?}"))),
}
}
Ok(out)
}
}
#[cfg(test)]
#[path = "cluster_tests.rs"]
mod tests;