Skip to main content

oxihuman_core/
service_registry.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Service discovery registry stub — register and resolve service endpoints.
6
7/// Metadata describing a service instance.
8#[derive(Clone, Debug)]
9pub struct ServiceInstance {
10    pub name: String,
11    pub host: String,
12    pub port: u16,
13    pub version: String,
14    pub healthy: bool,
15    pub tags: Vec<String>,
16}
17
18/// Configuration for the service registry.
19#[derive(Clone, Debug)]
20pub struct ServiceRegistryConfig {
21    pub max_instances_per_service: usize,
22}
23
24impl Default for ServiceRegistryConfig {
25    fn default() -> Self {
26        Self {
27            max_instances_per_service: 16,
28        }
29    }
30}
31
32/// An in-memory service registry.
33pub struct ServiceRegistry {
34    pub config: ServiceRegistryConfig,
35    instances: Vec<ServiceInstance>,
36}
37
38/// Creates a new service registry.
39pub fn new_registry(config: ServiceRegistryConfig) -> ServiceRegistry {
40    ServiceRegistry {
41        config,
42        instances: Vec::new(),
43    }
44}
45
46/// Registers a service instance, returning false if the service is at capacity.
47pub fn register_instance(reg: &mut ServiceRegistry, instance: ServiceInstance) -> bool {
48    let count = reg
49        .instances
50        .iter()
51        .filter(|i| i.name == instance.name)
52        .count();
53    if count >= reg.config.max_instances_per_service {
54        return false;
55    }
56    reg.instances.push(instance);
57    true
58}
59
60/// Deregisters an instance by name and host+port combination.
61pub fn deregister_instance(reg: &mut ServiceRegistry, name: &str, host: &str, port: u16) -> bool {
62    let before = reg.instances.len();
63    reg.instances
64        .retain(|i| !(i.name == name && i.host == host && i.port == port));
65    reg.instances.len() < before
66}
67
68/// Resolves healthy instances of a service by name.
69pub fn resolve_service<'a>(reg: &'a ServiceRegistry, name: &str) -> Vec<&'a ServiceInstance> {
70    reg.instances
71        .iter()
72        .filter(|i| i.name == name && i.healthy)
73        .collect()
74}
75
76/// Marks all instances of a service as healthy or unhealthy.
77pub fn set_service_health(reg: &mut ServiceRegistry, name: &str, healthy: bool) {
78    for inst in reg.instances.iter_mut().filter(|i| i.name == name) {
79        inst.healthy = healthy;
80    }
81}
82
83/// Returns the total number of registered instances.
84pub fn total_instance_count(reg: &ServiceRegistry) -> usize {
85    reg.instances.len()
86}
87
88impl ServiceRegistry {
89    /// Creates a new registry with default config.
90    pub fn new(config: ServiceRegistryConfig) -> Self {
91        new_registry(config)
92    }
93}
94
95fn make_instance(name: &str, host: &str, port: u16) -> ServiceInstance {
96    ServiceInstance {
97        name: name.into(),
98        host: host.into(),
99        port,
100        version: "1.0".into(),
101        healthy: true,
102        tags: vec![],
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    fn make_reg() -> ServiceRegistry {
111        new_registry(ServiceRegistryConfig::default())
112    }
113
114    #[test]
115    fn test_register_and_count() {
116        let mut reg = make_reg();
117        let ok = register_instance(&mut reg, make_instance("svc-a", "host1", 8080));
118        assert!(ok);
119        assert_eq!(total_instance_count(&reg), 1);
120    }
121
122    #[test]
123    fn test_resolve_healthy_instance() {
124        let mut reg = make_reg();
125        register_instance(&mut reg, make_instance("svc-b", "host2", 9000));
126        let found = resolve_service(&reg, "svc-b");
127        assert_eq!(found.len(), 1);
128    }
129
130    #[test]
131    fn test_unhealthy_instance_not_resolved() {
132        let mut reg = make_reg();
133        let mut inst = make_instance("svc-c", "h", 80);
134        inst.healthy = false;
135        register_instance(&mut reg, inst);
136        assert!(resolve_service(&reg, "svc-c").is_empty());
137    }
138
139    #[test]
140    fn test_deregister_removes_instance() {
141        let mut reg = make_reg();
142        register_instance(&mut reg, make_instance("svc-d", "h", 80));
143        let removed = deregister_instance(&mut reg, "svc-d", "h", 80);
144        assert!(removed);
145        assert_eq!(total_instance_count(&reg), 0);
146    }
147
148    #[test]
149    fn test_deregister_nonexistent_returns_false() {
150        let mut reg = make_reg();
151        assert!(!deregister_instance(&mut reg, "none", "h", 80));
152    }
153
154    #[test]
155    fn test_set_service_health_to_unhealthy() {
156        let mut reg = make_reg();
157        register_instance(&mut reg, make_instance("svc-e", "h", 80));
158        set_service_health(&mut reg, "svc-e", false);
159        assert!(resolve_service(&reg, "svc-e").is_empty());
160    }
161
162    #[test]
163    fn test_capacity_limit_enforced() {
164        let mut reg = new_registry(ServiceRegistryConfig {
165            max_instances_per_service: 2,
166        });
167        register_instance(&mut reg, make_instance("svc-f", "h1", 1));
168        register_instance(&mut reg, make_instance("svc-f", "h2", 2));
169        let ok = register_instance(&mut reg, make_instance("svc-f", "h3", 3));
170        assert!(!ok);
171    }
172
173    #[test]
174    fn test_multiple_services_independent() {
175        let mut reg = make_reg();
176        register_instance(&mut reg, make_instance("alpha", "h1", 80));
177        register_instance(&mut reg, make_instance("beta", "h2", 80));
178        assert_eq!(resolve_service(&reg, "alpha").len(), 1);
179        assert_eq!(resolve_service(&reg, "beta").len(), 1);
180    }
181
182    #[test]
183    fn test_resolve_returns_empty_for_unknown_service() {
184        let reg = make_reg();
185        assert!(resolve_service(&reg, "unknown").is_empty());
186    }
187}