matrixcode_core/matrixrpc/
service.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::time::Instant;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct ServiceId(pub String);
14
15impl ServiceId {
16 pub fn new(id: impl Into<String>) -> Self {
18 Self(id.into())
19 }
20
21 pub fn generate() -> Self {
23 Self(uuid::Uuid::new_v4().to_string())
24 }
25}
26
27impl std::fmt::Display for ServiceId {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 write!(f, "{}", self.0)
30 }
31}
32
33impl From<&str> for ServiceId {
34 fn from(s: &str) -> Self {
35 Self(s.to_string())
36 }
37}
38
39impl From<String> for ServiceId {
40 fn from(s: String) -> Self {
41 Self(s)
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum ServiceStatus {
49 Starting,
51 Running,
53 Stopping,
55 Stopped,
57 Error,
59 Reconnecting,
61}
62
63impl Default for ServiceStatus {
64 fn default() -> Self {
65 Self::Starting
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ExtensionService {
72 pub id: ServiceId,
74 pub name: String,
76 pub version: String,
78 #[serde(default)]
80 pub description: String,
81 pub capabilities: Vec<Capability>,
83 pub transport: TransportConfig,
85 #[serde(default)]
87 pub metadata: HashMap<String, serde_json::Value>,
88 #[serde(default)]
90 pub status: ServiceStatus,
91 #[serde(skip)]
93 pub last_heartbeat: Option<Instant>,
94 #[serde(default)]
96 pub retry_count: u32,
97}
98
99impl ExtensionService {
100 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
102 Self {
103 id: ServiceId::generate(),
104 name: name.into(),
105 version: version.into(),
106 description: String::new(),
107 capabilities: Vec::new(),
108 transport: TransportConfig::default(),
109 metadata: HashMap::new(),
110 status: ServiceStatus::Starting,
111 last_heartbeat: None,
112 retry_count: 0,
113 }
114 }
115
116 pub fn description(mut self, desc: impl Into<String>) -> Self {
118 self.description = desc.into();
119 self
120 }
121
122 pub fn capability(mut self, cap: Capability) -> Self {
124 self.capabilities.push(cap);
125 self
126 }
127
128 pub fn transport(mut self, transport: TransportConfig) -> Self {
130 self.transport = transport;
131 self
132 }
133
134 pub fn metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
136 self.metadata.insert(key.into(), value);
137 self
138 }
139
140 pub fn has_capability(&self, name: &str) -> bool {
142 self.capabilities.iter().any(|c| c.name == name)
143 }
144
145 pub fn get_capability(&self, name: &str) -> Option<&Capability> {
147 self.capabilities.iter().find(|c| c.name == name)
148 }
149
150 pub fn set_status(&mut self, status: ServiceStatus) {
152 self.status = status;
153 }
154
155 pub fn heartbeat(&mut self) {
157 self.last_heartbeat = Some(Instant::now());
158 self.retry_count = 0;
159 }
160
161 pub fn is_healthy(&self, timeout_secs: u64) -> bool {
163 match self.last_heartbeat {
164 Some(last) => {
165 last.elapsed().as_secs() < timeout_secs
166 && self.status == ServiceStatus::Running
167 }
168 None => false,
169 }
170 }
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct Capability {
176 pub name: String,
178 #[serde(default)]
180 pub version: String,
181 #[serde(default)]
183 pub config: HashMap<String, serde_json::Value>,
184}
185
186impl Capability {
187 pub fn new(name: impl Into<String>) -> Self {
189 Self {
190 name: name.into(),
191 version: String::new(),
192 config: HashMap::new(),
193 }
194 }
195
196 pub fn version(mut self, version: impl Into<String>) -> Self {
198 self.version = version.into();
199 self
200 }
201
202 pub fn config(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
204 self.config.insert(key.into(), value);
205 self
206 }
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct TransportConfig {
212 #[serde(rename = "type")]
214 pub transport_type: TransportType,
215 #[serde(default)]
217 pub address: Option<String>,
218 #[serde(default)]
220 pub port: Option<u16>,
221 #[serde(default)]
223 pub command: Option<String>,
224 #[serde(default)]
226 pub args: Vec<String>,
227 #[serde(default)]
229 pub env: HashMap<String, String>,
230 #[serde(default)]
232 pub cwd: Option<String>,
233 #[serde(default = "default_timeout")]
235 pub timeout_secs: u64,
236 #[serde(default = "default_true")]
238 pub auto_reconnect: bool,
239 #[serde(default = "default_max_retries")]
241 pub max_retries: u32,
242 #[serde(default = "default_heartbeat_interval")]
244 pub heartbeat_interval_secs: u64,
245}
246
247fn default_timeout() -> u64 {
248 30
249}
250
251fn default_true() -> bool {
252 true
253}
254
255fn default_max_retries() -> u32 {
256 3
257}
258
259fn default_heartbeat_interval() -> u64 {
260 30
261}
262
263impl Default for TransportConfig {
264 fn default() -> Self {
265 Self {
266 transport_type: TransportType::Stdio,
267 address: None,
268 port: None,
269 command: None,
270 args: Vec::new(),
271 env: HashMap::new(),
272 cwd: None,
273 timeout_secs: default_timeout(),
274 auto_reconnect: true,
275 max_retries: default_max_retries(),
276 heartbeat_interval_secs: default_heartbeat_interval(),
277 }
278 }
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(rename_all = "lowercase")]
284pub enum TransportType {
285 Stdio,
287 Tcp,
289 #[cfg(unix)]
291 Unix,
292 WebSocket,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct RegistrationInfo {
299 pub service: ExtensionService,
301 pub registered_at: chrono::DateTime<chrono::Utc>,
303 pub updated_at: chrono::DateTime<chrono::Utc>,
305}
306
307impl RegistrationInfo {
308 pub fn new(service: ExtensionService) -> Self {
310 let now = chrono::Utc::now();
311 Self {
312 service,
313 registered_at: now,
314 updated_at: now,
315 }
316 }
317
318 pub fn touch(&mut self) {
320 self.updated_at = chrono::Utc::now();
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn test_service_id() {
330 let id = ServiceId::new("test-service");
331 assert_eq!(id.to_string(), "test-service");
332
333 let generated = ServiceId::generate();
334 assert!(!generated.0.is_empty());
335 }
336
337 #[test]
338 fn test_extension_service_creation() {
339 let service = ExtensionService::new("test-service", "1.0.0")
340 .description("A test service")
341 .capability(Capability::new("tools"));
342
343 assert_eq!(service.name, "test-service");
344 assert_eq!(service.version, "1.0.0");
345 assert_eq!(service.description, "A test service");
346 assert!(service.has_capability("tools"));
347 assert!(!service.has_capability("resources"));
348 }
349
350 #[test]
351 fn test_service_status() {
352 let mut service = ExtensionService::new("test", "1.0.0");
353 assert_eq!(service.status, ServiceStatus::Starting);
354
355 service.set_status(ServiceStatus::Running);
356 assert_eq!(service.status, ServiceStatus::Running);
357 }
358
359 #[test]
360 fn test_service_heartbeat() {
361 let mut service = ExtensionService::new("test", "1.0.0");
362 assert!(!service.is_healthy(30));
363
364 service.set_status(ServiceStatus::Running);
365 service.heartbeat();
366 assert!(service.is_healthy(30));
367 }
368
369 #[test]
370 fn test_capability() {
371 let cap = Capability::new("tools")
372 .version("1.0")
373 .config("max_items".to_string(), serde_json::json!(100));
374
375 assert_eq!(cap.name, "tools");
376 assert_eq!(cap.version, "1.0");
377 assert_eq!(cap.config.get("max_items"), Some(&serde_json::json!(100)));
378 }
379
380 #[test]
381 fn test_transport_config_defaults() {
382 let config = TransportConfig::default();
383 assert_eq!(config.transport_type, TransportType::Stdio);
384 assert!(config.auto_reconnect);
385 assert_eq!(config.max_retries, 3);
386 assert_eq!(config.heartbeat_interval_secs, 30);
387 }
388
389 #[test]
390 fn test_registration_info() {
391 let service = ExtensionService::new("test", "1.0.0");
392 let reg = RegistrationInfo::new(service);
393
394 assert!(reg.registered_at <= chrono::Utc::now());
395 assert!(reg.updated_at <= chrono::Utc::now());
396 }
397}