Skip to main content

rs_zero/discovery/
instance.rs

1use std::{collections::BTreeMap, fmt, net::SocketAddr, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4
5use crate::discovery::{DiscoveryError, DiscoveryResult};
6
7/// Network endpoint for a service instance.
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
9pub struct InstanceEndpoint {
10    host: String,
11    port: u16,
12}
13
14impl InstanceEndpoint {
15    /// Creates an endpoint from a host and port.
16    pub fn new(host: impl Into<String>, port: u16) -> DiscoveryResult<Self> {
17        let host = host.into();
18        if host.trim().is_empty() {
19            return Err(DiscoveryError::InvalidEndpoint { endpoint: host });
20        }
21        Ok(Self { host, port })
22    }
23
24    /// Returns the endpoint host.
25    pub fn host(&self) -> &str {
26        &self.host
27    }
28
29    /// Returns the endpoint port.
30    pub fn port(&self) -> u16 {
31        self.port
32    }
33
34    /// Returns a `host:port` string.
35    pub fn authority(&self) -> String {
36        format!("{}:{}", self.host, self.port)
37    }
38
39    /// Converts a socket address into an endpoint.
40    pub fn from_socket_addr(addr: SocketAddr) -> Self {
41        Self {
42            host: addr.ip().to_string(),
43            port: addr.port(),
44        }
45    }
46}
47
48impl fmt::Display for InstanceEndpoint {
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        write!(formatter, "{}:{}", self.host, self.port)
51    }
52}
53
54impl FromStr for InstanceEndpoint {
55    type Err = DiscoveryError;
56
57    fn from_str(value: &str) -> Result<Self, Self::Err> {
58        let (host, port) =
59            value
60                .rsplit_once(':')
61                .ok_or_else(|| DiscoveryError::InvalidEndpoint {
62                    endpoint: value.to_string(),
63                })?;
64        let port = port.parse().map_err(|_| DiscoveryError::InvalidEndpoint {
65            endpoint: value.to_string(),
66        })?;
67        Self::new(host, port)
68    }
69}
70
71/// A discoverable service instance.
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct ServiceInstance {
74    /// Service name used by discovery clients.
75    pub service: String,
76    /// Stable instance id inside a service.
77    pub id: String,
78    /// Network endpoint.
79    pub endpoint: InstanceEndpoint,
80    /// Free-form metadata for adapters and clients.
81    pub metadata: BTreeMap<String, String>,
82    /// Whether this instance is considered healthy.
83    pub healthy: bool,
84    /// Relative weight used by selectors.
85    pub weight: u32,
86}
87
88impl ServiceInstance {
89    /// Creates a healthy service instance with weight `1`.
90    pub fn new(
91        service: impl Into<String>,
92        id: impl Into<String>,
93        endpoint: InstanceEndpoint,
94    ) -> Self {
95        Self {
96            service: service.into(),
97            id: id.into(),
98            endpoint,
99            metadata: BTreeMap::new(),
100            healthy: true,
101            weight: 1,
102        }
103    }
104
105    /// Adds metadata and returns the updated instance.
106    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
107        self.metadata.insert(key.into(), value.into());
108        self
109    }
110
111    /// Sets the health flag and returns the updated instance.
112    pub fn with_health(mut self, healthy: bool) -> Self {
113        self.healthy = healthy;
114        self
115    }
116
117    /// Sets the selector weight. Zero is normalized to one.
118    pub fn with_weight(mut self, weight: u32) -> Self {
119        self.weight = weight.max(1);
120        self
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::{InstanceEndpoint, ServiceInstance};
127
128    #[test]
129    fn endpoint_parses_authority() {
130        let endpoint: InstanceEndpoint = "127.0.0.1:8080".parse().expect("endpoint");
131        assert_eq!(endpoint.host(), "127.0.0.1");
132        assert_eq!(endpoint.port(), 8080);
133        assert_eq!(endpoint.authority(), "127.0.0.1:8080");
134    }
135
136    #[test]
137    fn service_instance_defaults_are_healthy() {
138        let endpoint = InstanceEndpoint::new("localhost", 9000).expect("endpoint");
139        let instance = ServiceInstance::new("user", "user-1", endpoint).with_weight(0);
140        assert!(instance.healthy);
141        assert_eq!(instance.weight, 1);
142    }
143}