Skip to main content

agent_uri_dht/
endpoint.rs

1//! Network endpoint type for agent discovery.
2
3use std::fmt;
4
5/// Network endpoint for contacting an agent.
6///
7/// Represents a reachable address where an agent can be contacted.
8/// Multiple endpoints may be provided for redundancy or protocol flexibility.
9///
10/// # Examples
11///
12/// ```
13/// use agent_uri_dht::Endpoint;
14///
15/// let https = Endpoint::new("https", "agent.example.com:443", Some("/v1/agent"));
16/// let grpc = Endpoint::new("grpc", "agent.example.com:50051", None::<&str>);
17/// ```
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub struct Endpoint {
20    /// Transport protocol (https, grpc, ws, etc.)
21    protocol: String,
22    /// Host and port (e.g., "agent.example.com:443")
23    address: String,
24    /// Optional path prefix for routing
25    path: Option<String>,
26}
27
28impl Endpoint {
29    /// Creates a new endpoint.
30    ///
31    /// # Arguments
32    ///
33    /// * `protocol` - Transport protocol (e.g., "https", "grpc", "ws")
34    /// * `address` - Host and optional port (e.g., "example.com:443")
35    /// * `path` - Optional path prefix (e.g., "/v1/agent")
36    #[must_use]
37    pub fn new(
38        protocol: impl Into<String>,
39        address: impl Into<String>,
40        path: Option<impl Into<String>>,
41    ) -> Self {
42        Self {
43            protocol: protocol.into(),
44            address: address.into(),
45            path: path.map(Into::into),
46        }
47    }
48
49    /// Creates an HTTPS endpoint.
50    #[must_use]
51    pub fn https(address: impl Into<String>) -> Self {
52        Self::new("https", address, None::<String>)
53    }
54
55    /// Creates an HTTPS endpoint with a path.
56    #[must_use]
57    pub fn https_with_path(address: impl Into<String>, path: impl Into<String>) -> Self {
58        Self::new("https", address, Some(path))
59    }
60
61    /// Creates a gRPC endpoint.
62    #[must_use]
63    pub fn grpc(address: impl Into<String>) -> Self {
64        Self::new("grpc", address, None::<String>)
65    }
66
67    /// Creates a WebSocket endpoint.
68    #[must_use]
69    pub fn websocket(address: impl Into<String>) -> Self {
70        Self::new("ws", address, None::<String>)
71    }
72
73    /// Returns the protocol.
74    #[must_use]
75    pub fn protocol(&self) -> &str {
76        &self.protocol
77    }
78
79    /// Returns the address.
80    #[must_use]
81    pub fn address(&self) -> &str {
82        &self.address
83    }
84
85    /// Returns the path, if any.
86    #[must_use]
87    pub fn path(&self) -> Option<&str> {
88        self.path.as_deref()
89    }
90
91    /// Returns the full URI representation.
92    #[must_use]
93    pub fn to_uri(&self) -> String {
94        match &self.path {
95            Some(p) => format!("{}://{}{}", self.protocol, self.address, p),
96            None => format!("{}://{}", self.protocol, self.address),
97        }
98    }
99}
100
101impl fmt::Display for Endpoint {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(f, "{}", self.to_uri())
104    }
105}
106
107#[cfg(feature = "serde")]
108impl serde::Serialize for Endpoint {
109    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
110    where
111        S: serde::Serializer,
112    {
113        use serde::ser::SerializeStruct;
114        let mut state = serializer.serialize_struct("Endpoint", 3)?;
115        state.serialize_field("protocol", &self.protocol)?;
116        state.serialize_field("address", &self.address)?;
117        state.serialize_field("path", &self.path)?;
118        state.end()
119    }
120}
121
122#[cfg(feature = "serde")]
123impl<'de> serde::Deserialize<'de> for Endpoint {
124    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
125    where
126        D: serde::Deserializer<'de>,
127    {
128        #[derive(serde::Deserialize)]
129        struct EndpointData {
130            protocol: String,
131            address: String,
132            path: Option<String>,
133        }
134
135        let data = EndpointData::deserialize(deserializer)?;
136        Ok(Self {
137            protocol: data.protocol,
138            address: data.address,
139            path: data.path,
140        })
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn https_endpoint() {
150        let endpoint = Endpoint::https("agent.example.com:443");
151        assert_eq!(endpoint.protocol(), "https");
152        assert_eq!(endpoint.address(), "agent.example.com:443");
153        assert!(endpoint.path().is_none());
154        assert_eq!(endpoint.to_uri(), "https://agent.example.com:443");
155    }
156
157    #[test]
158    fn https_with_path_endpoint() {
159        let endpoint = Endpoint::https_with_path("agent.example.com:443", "/v1/agent");
160        assert_eq!(endpoint.protocol(), "https");
161        assert_eq!(endpoint.address(), "agent.example.com:443");
162        assert_eq!(endpoint.path(), Some("/v1/agent"));
163        assert_eq!(endpoint.to_uri(), "https://agent.example.com:443/v1/agent");
164    }
165
166    #[test]
167    fn grpc_endpoint() {
168        let endpoint = Endpoint::grpc("agent.example.com:50051");
169        assert_eq!(endpoint.protocol(), "grpc");
170        assert_eq!(endpoint.to_uri(), "grpc://agent.example.com:50051");
171    }
172
173    #[test]
174    fn websocket_endpoint() {
175        let endpoint = Endpoint::websocket("agent.example.com:8080");
176        assert_eq!(endpoint.protocol(), "ws");
177        assert_eq!(endpoint.to_uri(), "ws://agent.example.com:8080");
178    }
179
180    #[test]
181    fn custom_endpoint() {
182        let endpoint = Endpoint::new("mqtt", "broker.example.com:1883", Some("/agents"));
183        assert_eq!(endpoint.protocol(), "mqtt");
184        assert_eq!(endpoint.address(), "broker.example.com:1883");
185        assert_eq!(endpoint.path(), Some("/agents"));
186        assert_eq!(endpoint.to_uri(), "mqtt://broker.example.com:1883/agents");
187    }
188
189    #[test]
190    fn display_matches_to_uri() {
191        let endpoint = Endpoint::https("agent.example.com:443");
192        assert_eq!(format!("{endpoint}"), endpoint.to_uri());
193    }
194
195    #[test]
196    fn endpoints_are_equal() {
197        let endpoint1 = Endpoint::https("agent.example.com:443");
198        let endpoint2 = Endpoint::https("agent.example.com:443");
199        assert_eq!(endpoint1, endpoint2);
200    }
201
202    #[test]
203    fn different_endpoints_are_not_equal() {
204        let endpoint1 = Endpoint::https("agent1.example.com:443");
205        let endpoint2 = Endpoint::https("agent2.example.com:443");
206        assert_ne!(endpoint1, endpoint2);
207    }
208}