use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ServiceInstance {
pub id: String,
pub service_name: String,
pub host: String,
pub port: u16,
pub secure: bool,
pub status: InstanceStatus,
pub metadata: HashMap<String, String>,
pub tags: Vec<String>,
pub registered_at: DateTime<Utc>,
pub last_heartbeat: DateTime<Utc>,
pub last_status_change: DateTime<Utc>,
}
impl ServiceInstance {
pub fn is_healthy(&self) -> bool {
matches!(self.status, InstanceStatus::Up)
}
pub fn get_url(&self, path: &str) -> String {
let protocol = if self.secure { "https" } else { "http" };
let clean_path = if path.starts_with('/') {
path
} else {
&format!("/{}", path)
};
format!("{}://{}:{}{}", protocol, self.host, self.port, clean_path)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum InstanceStatus {
Up,
Down,
Starting,
Stopping,
OutOfService,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheck {
pub url: String,
pub interval_seconds: u64,
pub timeout_seconds: u64,
pub method: String,
pub expected_status: u16,
pub headers: Option<HashMap<String, String>>,
}
impl Default for HealthCheck {
fn default() -> Self {
Self {
url: String::new(),
interval_seconds: 30,
timeout_seconds: 10,
method: "GET".to_string(),
expected_status: 200,
headers: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ServiceRegistrationOptions {
pub metadata: HashMap<String, String>,
pub tags: Vec<String>,
pub health_check: Option<HealthCheck>,
pub secure: bool,
}
impl ServiceRegistrationOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
self.metadata = metadata;
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = tags;
self
}
pub fn with_health_check(mut self, health_check: HealthCheck) -> Self {
self.health_check = Some(health_check);
self
}
pub fn with_secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct ServiceDiscoveryOptions {
pub healthy_only: bool,
pub tags: Option<Vec<String>>,
pub limit: Option<usize>,
}
impl ServiceDiscoveryOptions {
pub fn new() -> Self {
Self {
healthy_only: true,
..Default::default()
}
}
pub fn with_healthy_only(mut self, healthy_only: bool) -> Self {
self.healthy_only = healthy_only;
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = Some(tags);
self
}
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Serialize)]
pub struct RegisterServiceRequest {
pub service_name: String,
pub host: String,
pub port: u16,
pub secure: bool,
pub metadata: HashMap<String, String>,
pub tags: Vec<String>,
pub health_check: Option<HealthCheck>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Service {
pub name: String,
pub instances: Vec<ServiceInstance>,
pub tags: Vec<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use std::collections::HashMap;
fn create_test_instance() -> ServiceInstance {
ServiceInstance {
id: "test-123".to_string(),
service_name: "test-service".to_string(),
host: "localhost".to_string(),
port: 3000,
secure: false,
status: InstanceStatus::Up,
metadata: HashMap::new(),
tags: vec!["test".to_string()],
registered_at: Utc::now(),
last_heartbeat: Utc::now(),
last_status_change: Utc::now(),
}
}
#[test]
fn test_service_instance_is_healthy() {
let mut instance = create_test_instance();
instance.status = InstanceStatus::Up;
assert!(instance.is_healthy());
instance.status = InstanceStatus::Down;
assert!(!instance.is_healthy());
instance.status = InstanceStatus::Starting;
assert!(!instance.is_healthy());
instance.status = InstanceStatus::Stopping;
assert!(!instance.is_healthy());
instance.status = InstanceStatus::OutOfService;
assert!(!instance.is_healthy());
instance.status = InstanceStatus::Unknown;
assert!(!instance.is_healthy());
}
#[test]
fn test_service_instance_get_url() {
let instance = create_test_instance();
assert_eq!(
instance.get_url("/api/users"),
"http://localhost:3000/api/users"
);
assert_eq!(
instance.get_url("api/users"),
"http://localhost:3000/api/users"
);
assert_eq!(instance.get_url("/"), "http://localhost:3000/");
assert_eq!(instance.get_url(""), "http://localhost:3000/");
}
#[test]
fn test_service_instance_get_url_secure() {
let mut instance = create_test_instance();
instance.secure = true;
assert_eq!(
instance.get_url("/api/users"),
"https://localhost:3000/api/users"
);
}
#[test]
fn test_health_check_default() {
let health_check = HealthCheck::default();
assert_eq!(health_check.url, "");
assert_eq!(health_check.interval_seconds, 30);
assert_eq!(health_check.timeout_seconds, 10);
assert_eq!(health_check.method, "GET");
assert_eq!(health_check.expected_status, 200);
assert!(health_check.headers.is_none());
}
#[test]
fn test_service_registration_options_builder() {
let mut metadata = HashMap::new();
metadata.insert("version".to_string(), "1.0".to_string());
let options = ServiceRegistrationOptions::new()
.with_metadata(metadata.clone())
.with_tags(vec!["api".to_string(), "v1".to_string()])
.with_secure(true);
assert_eq!(options.metadata, metadata);
assert_eq!(options.tags, vec!["api", "v1"]);
assert!(options.secure);
}
#[test]
fn test_service_discovery_options_builder() {
let options = ServiceDiscoveryOptions::new()
.with_healthy_only(false)
.with_tags(vec!["production".to_string()])
.with_limit(10);
assert!(!options.healthy_only);
assert_eq!(options.tags, Some(vec!["production".to_string()]));
assert_eq!(options.limit, Some(10));
}
#[test]
fn test_service_discovery_options_default() {
let options = ServiceDiscoveryOptions::new();
assert!(options.healthy_only);
assert!(options.tags.is_none());
assert!(options.limit.is_none());
}
}