use std::{collections::BTreeMap, fmt, net::SocketAddr, str::FromStr};
use serde::{Deserialize, Serialize};
use crate::discovery::{DiscoveryError, DiscoveryResult};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct InstanceEndpoint {
host: String,
port: u16,
}
impl InstanceEndpoint {
pub fn new(host: impl Into<String>, port: u16) -> DiscoveryResult<Self> {
let host = host.into();
if host.trim().is_empty() {
return Err(DiscoveryError::InvalidEndpoint { endpoint: host });
}
Ok(Self { host, port })
}
pub fn host(&self) -> &str {
&self.host
}
pub fn port(&self) -> u16 {
self.port
}
pub fn authority(&self) -> String {
format!("{}:{}", self.host, self.port)
}
pub fn from_socket_addr(addr: SocketAddr) -> Self {
Self {
host: addr.ip().to_string(),
port: addr.port(),
}
}
}
impl fmt::Display for InstanceEndpoint {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}:{}", self.host, self.port)
}
}
impl FromStr for InstanceEndpoint {
type Err = DiscoveryError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let (host, port) =
value
.rsplit_once(':')
.ok_or_else(|| DiscoveryError::InvalidEndpoint {
endpoint: value.to_string(),
})?;
let port = port.parse().map_err(|_| DiscoveryError::InvalidEndpoint {
endpoint: value.to_string(),
})?;
Self::new(host, port)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceInstance {
pub service: String,
pub id: String,
pub endpoint: InstanceEndpoint,
pub metadata: BTreeMap<String, String>,
pub healthy: bool,
pub weight: u32,
}
impl ServiceInstance {
pub fn new(
service: impl Into<String>,
id: impl Into<String>,
endpoint: InstanceEndpoint,
) -> Self {
Self {
service: service.into(),
id: id.into(),
endpoint,
metadata: BTreeMap::new(),
healthy: true,
weight: 1,
}
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_health(mut self, healthy: bool) -> Self {
self.healthy = healthy;
self
}
pub fn with_weight(mut self, weight: u32) -> Self {
self.weight = weight.max(1);
self
}
}
#[cfg(test)]
mod tests {
use super::{InstanceEndpoint, ServiceInstance};
#[test]
fn endpoint_parses_authority() {
let endpoint: InstanceEndpoint = "127.0.0.1:8080".parse().expect("endpoint");
assert_eq!(endpoint.host(), "127.0.0.1");
assert_eq!(endpoint.port(), 8080);
assert_eq!(endpoint.authority(), "127.0.0.1:8080");
}
#[test]
fn service_instance_defaults_are_healthy() {
let endpoint = InstanceEndpoint::new("localhost", 9000).expect("endpoint");
let instance = ServiceInstance::new("user", "user-1", endpoint).with_weight(0);
assert!(instance.healthy);
assert_eq!(instance.weight, 1);
}
}