1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11pub struct ServiceInstance {
12 pub id: String,
14 pub service_name: String,
16 pub host: String,
18 pub port: u16,
20 pub secure: bool,
22 pub status: InstanceStatus,
24 pub metadata: HashMap<String, String>,
26 pub tags: Vec<String>,
28 pub registered_at: DateTime<Utc>,
30 pub last_heartbeat: DateTime<Utc>,
32 pub last_status_change: DateTime<Utc>,
34}
35
36impl ServiceInstance {
37 pub fn is_healthy(&self) -> bool {
39 matches!(self.status, InstanceStatus::Up)
40 }
41
42 pub fn get_url(&self, path: &str) -> String {
77 let protocol = if self.secure { "https" } else { "http" };
78 let clean_path = if path.starts_with('/') {
79 path
80 } else {
81 &format!("/{}", path)
82 };
83 format!("{}://{}:{}{}", protocol, self.host, self.port, clean_path)
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub enum InstanceStatus {
90 Up,
92 Down,
94 Starting,
96 Stopping,
98 OutOfService,
100 Unknown,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct HealthCheck {
109 pub url: String,
111 pub interval_seconds: u64,
113 pub timeout_seconds: u64,
115 pub method: String,
117 pub expected_status: u16,
119 pub headers: Option<HashMap<String, String>>,
121}
122
123impl Default for HealthCheck {
125 fn default() -> Self {
126 Self {
127 url: String::new(),
128 interval_seconds: 30,
129 timeout_seconds: 10,
130 method: "GET".to_string(),
131 expected_status: 200,
132 headers: None,
133 }
134 }
135}
136
137#[derive(Debug, Clone, Default)]
142pub struct ServiceRegistrationOptions {
143 pub metadata: HashMap<String, String>,
144 pub tags: Vec<String>,
145 pub health_check: Option<HealthCheck>,
146 pub secure: bool,
147}
148
149impl ServiceRegistrationOptions {
151 pub fn new() -> Self {
152 Self::default()
153 }
154
155 pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
157 self.metadata = metadata;
158 self
159 }
160
161 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
163 self.tags = tags;
164 self
165 }
166
167 pub fn with_health_check(mut self, health_check: HealthCheck) -> Self {
169 self.health_check = Some(health_check);
170 self
171 }
172
173 pub fn with_secure(mut self, secure: bool) -> Self {
175 self.secure = secure;
176 self
177 }
178}
179
180#[derive(Debug, Clone, Default)]
181pub struct ServiceDiscoveryOptions {
182 pub healthy_only: bool,
183 pub tags: Option<Vec<String>>,
184 pub limit: Option<usize>,
185}
186
187impl ServiceDiscoveryOptions {
189 pub fn new() -> Self {
190 Self {
191 healthy_only: true,
192 ..Default::default()
193 }
194 }
195
196 pub fn with_healthy_only(mut self, healthy_only: bool) -> Self {
198 self.healthy_only = healthy_only;
199 self
200 }
201
202 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
204 self.tags = Some(tags);
205 self
206 }
207
208 pub fn with_limit(mut self, limit: usize) -> Self {
210 self.limit = Some(limit);
211 self
212 }
213}
214
215#[derive(Debug, Serialize)]
216pub struct RegisterServiceRequest {
217 pub service_name: String,
218 pub host: String,
219 pub port: u16,
220 pub secure: bool,
221 pub metadata: HashMap<String, String>,
222 pub tags: Vec<String>,
223 pub health_check: Option<HealthCheck>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct Service {
228 pub name: String,
229 pub instances: Vec<ServiceInstance>,
230 pub tags: Vec<String>,
231 pub created_at: DateTime<Utc>,
232 pub updated_at: DateTime<Utc>,
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use chrono::Utc;
239 use std::collections::HashMap;
240
241 fn create_test_instance() -> ServiceInstance {
242 ServiceInstance {
243 id: "test-123".to_string(),
244 service_name: "test-service".to_string(),
245 host: "localhost".to_string(),
246 port: 3000,
247 secure: false,
248 status: InstanceStatus::Up,
249 metadata: HashMap::new(),
250 tags: vec!["test".to_string()],
251 registered_at: Utc::now(),
252 last_heartbeat: Utc::now(),
253 last_status_change: Utc::now(),
254 }
255 }
256
257 #[test]
258 fn test_service_instance_is_healthy() {
259 let mut instance = create_test_instance();
260
261 instance.status = InstanceStatus::Up;
263 assert!(instance.is_healthy());
264
265 instance.status = InstanceStatus::Down;
267 assert!(!instance.is_healthy());
268
269 instance.status = InstanceStatus::Starting;
270 assert!(!instance.is_healthy());
271
272 instance.status = InstanceStatus::Stopping;
273 assert!(!instance.is_healthy());
274
275 instance.status = InstanceStatus::OutOfService;
276 assert!(!instance.is_healthy());
277
278 instance.status = InstanceStatus::Unknown;
279 assert!(!instance.is_healthy());
280 }
281
282 #[test]
283 fn test_service_instance_get_url() {
284 let instance = create_test_instance();
285
286 assert_eq!(
287 instance.get_url("/api/users"),
288 "http://localhost:3000/api/users"
289 );
290 assert_eq!(
291 instance.get_url("api/users"),
292 "http://localhost:3000/api/users"
293 );
294 assert_eq!(instance.get_url("/"), "http://localhost:3000/");
295 assert_eq!(instance.get_url(""), "http://localhost:3000/");
296 }
297
298 #[test]
299 fn test_service_instance_get_url_secure() {
300 let mut instance = create_test_instance();
301 instance.secure = true;
302
303 assert_eq!(
304 instance.get_url("/api/users"),
305 "https://localhost:3000/api/users"
306 );
307 }
308
309 #[test]
310 fn test_health_check_default() {
311 let health_check = HealthCheck::default();
312
313 assert_eq!(health_check.url, "");
314 assert_eq!(health_check.interval_seconds, 30);
315 assert_eq!(health_check.timeout_seconds, 10);
316 assert_eq!(health_check.method, "GET");
317 assert_eq!(health_check.expected_status, 200);
318 assert!(health_check.headers.is_none());
319 }
320
321 #[test]
322 fn test_service_registration_options_builder() {
323 let mut metadata = HashMap::new();
324 metadata.insert("version".to_string(), "1.0".to_string());
325
326 let options = ServiceRegistrationOptions::new()
327 .with_metadata(metadata.clone())
328 .with_tags(vec!["api".to_string(), "v1".to_string()])
329 .with_secure(true);
330
331 assert_eq!(options.metadata, metadata);
332 assert_eq!(options.tags, vec!["api", "v1"]);
333 assert!(options.secure);
334 }
335
336 #[test]
337 fn test_service_discovery_options_builder() {
338 let options = ServiceDiscoveryOptions::new()
339 .with_healthy_only(false)
340 .with_tags(vec!["production".to_string()])
341 .with_limit(10);
342
343 assert!(!options.healthy_only);
344 assert_eq!(options.tags, Some(vec!["production".to_string()]));
345 assert_eq!(options.limit, Some(10));
346 }
347
348 #[test]
349 fn test_service_discovery_options_default() {
350 let options = ServiceDiscoveryOptions::new();
351 assert!(options.healthy_only);
352 assert!(options.tags.is_none());
353 assert!(options.limit.is_none());
354 }
355}