scoutquest_rust/
models.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5/// Represents a service instance in the ScoutQuest discovery system.
6///
7/// A service instance contains all the information needed to connect to
8/// and identify a specific instance of a service, including its network
9/// location, health status, and metadata.
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11pub struct ServiceInstance {
12    /// Unique identifier for this service instance
13    pub id: String,
14    /// Name of the service this instance belongs to
15    pub service_name: String,
16    /// Hostname or IP address where the service is running
17    pub host: String,
18    /// Port number where the service is listening
19    pub port: u16,
20    /// Whether the service uses HTTPS/TLS
21    pub secure: bool,
22    /// Current status of the service instance
23    pub status: InstanceStatus,
24    /// Custom metadata key-value pairs
25    pub metadata: HashMap<String, String>,
26    /// Tags associated with this service instance
27    pub tags: Vec<String>,
28    /// Timestamp when the service was first registered
29    pub registered_at: DateTime<Utc>,
30    /// Timestamp of the last heartbeat received
31    pub last_heartbeat: DateTime<Utc>,
32    /// Timestamp when the status last changed
33    pub last_status_change: DateTime<Utc>,
34}
35
36impl ServiceInstance {
37    /// Returns true if the service instance is healthy and ready to serve requests.
38    pub fn is_healthy(&self) -> bool {
39        matches!(self.status, InstanceStatus::Up)
40    }
41
42    /// Constructs the full URL for a given path on this service instance.
43    ///
44    /// # Arguments
45    ///
46    /// * `path` - The API path to append to the base URL
47    ///
48    /// # Returns
49    ///
50    /// A complete URL string ready to be used for HTTP requests.
51    ///
52    /// # Examples
53    ///
54    /// ```rust
55    /// use scoutquest_rust::*;
56    /// use std::collections::HashMap;
57    /// use chrono::Utc;
58    ///
59    /// let instance = ServiceInstance {
60    ///     id: "test-1".to_string(),
61    ///     service_name: "api".to_string(),
62    ///     host: "localhost".to_string(),
63    ///     port: 3000,
64    ///     secure: false,
65    ///     status: InstanceStatus::Up,
66    ///     metadata: HashMap::new(),
67    ///     tags: Vec::new(),
68    ///     registered_at: Utc::now(),
69    ///     last_heartbeat: Utc::now(),
70    ///     last_status_change: Utc::now(),
71    /// };
72    ///
73    /// assert_eq!(instance.get_url("/users"), "http://localhost:3000/users");
74    /// assert_eq!(instance.get_url("users"), "http://localhost:3000/users");
75    /// ```
76    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/// Represents the operational status of a service instance.
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub enum InstanceStatus {
90    /// Service is running and ready to accept requests
91    Up,
92    /// Service is not responding or has failed
93    Down,
94    /// Service is in the process of starting up
95    Starting,
96    /// Service is gracefully shutting down
97    Stopping,
98    /// Service is running but temporarily out of service
99    OutOfService,
100    /// Service status is unknown or could not be determined
101    Unknown,
102}
103
104/// Configuration for health check endpoints.
105///
106/// Defines how the ScoutQuest server should check the health of a service instance.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct HealthCheck {
109    /// URL path for the health check endpoint
110    pub url: String,
111    /// How often to perform health checks (in seconds)
112    pub interval_seconds: u64,
113    /// Maximum time to wait for a health check response (in seconds)
114    pub timeout_seconds: u64,
115    /// HTTP method to use for health checks
116    pub method: String,
117    /// Expected HTTP status code for a healthy response
118    pub expected_status: u16,
119    /// Optional HTTP headers to send with health check requests
120    pub headers: Option<HashMap<String, String>>,
121}
122
123/// Default implementation for HealthCheck
124impl 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/// Optional configuration for service registration.
138///
139/// This struct allows you to specify additional metadata, tags, health checks,
140/// and security settings when registering a service.
141#[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
149/// Service registration options.
150impl ServiceRegistrationOptions {
151    pub fn new() -> Self {
152        Self::default()
153    }
154
155    /// Set metadata for the service.
156    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
157        self.metadata = metadata;
158        self
159    }
160
161    /// Set tags for the service.
162    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
163        self.tags = tags;
164        self
165    }
166
167    /// Set health check configuration for the service.
168    pub fn with_health_check(mut self, health_check: HealthCheck) -> Self {
169        self.health_check = Some(health_check);
170        self
171    }
172
173    /// Set whether the service uses HTTPS/TLS.
174    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
187/// Service discovery options.
188impl ServiceDiscoveryOptions {
189    pub fn new() -> Self {
190        Self {
191            healthy_only: true,
192            ..Default::default()
193        }
194    }
195
196    /// Set whether to include only healthy instances in the discovery results.
197    pub fn with_healthy_only(mut self, healthy_only: bool) -> Self {
198        self.healthy_only = healthy_only;
199        self
200    }
201
202    /// Set tags for the service.
203    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
204        self.tags = Some(tags);
205        self
206    }
207
208    /// Set the maximum number of instances to return.
209    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        // Test healthy status
262        instance.status = InstanceStatus::Up;
263        assert!(instance.is_healthy());
264
265        // Test unhealthy statuses
266        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}