Skip to main content

matrixcode_core/matrixrpc/
service.rs

1//! MatrixRPC Extension Service Data Models
2//!
3//! Defines the core data structures for extension services including
4//! service metadata, capabilities, and registration info.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::time::Instant;
9
10/// Unique identifier for an extension service
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct ServiceId(pub String);
14
15impl ServiceId {
16    /// Create a new service ID
17    pub fn new(id: impl Into<String>) -> Self {
18        Self(id.into())
19    }
20
21    /// Generate a new unique service ID
22    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/// Extension service status
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum ServiceStatus {
49    /// Service is starting up
50    Starting,
51    /// Service is running and healthy
52    Running,
53    /// Service is stopping
54    Stopping,
55    /// Service has stopped
56    Stopped,
57    /// Service encountered an error
58    Error,
59    /// Service is reconnecting
60    Reconnecting,
61}
62
63impl Default for ServiceStatus {
64    fn default() -> Self {
65        Self::Starting
66    }
67}
68
69/// Extension service metadata
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ExtensionService {
72    /// Unique service identifier
73    pub id: ServiceId,
74    /// Human-readable service name
75    pub name: String,
76    /// Service version (semver)
77    pub version: String,
78    /// Service description
79    #[serde(default)]
80    pub description: String,
81    /// Supported capabilities
82    pub capabilities: Vec<Capability>,
83    /// Transport configuration
84    pub transport: TransportConfig,
85    /// Custom metadata
86    #[serde(default)]
87    pub metadata: HashMap<String, serde_json::Value>,
88    /// Current status
89    #[serde(default)]
90    pub status: ServiceStatus,
91    /// Time of last heartbeat
92    #[serde(skip)]
93    pub last_heartbeat: Option<Instant>,
94    /// Connection retry count
95    #[serde(default)]
96    pub retry_count: u32,
97}
98
99impl ExtensionService {
100    /// Create a new extension service
101    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    /// Set service description
117    pub fn description(mut self, desc: impl Into<String>) -> Self {
118        self.description = desc.into();
119        self
120    }
121
122    /// Add a capability
123    pub fn capability(mut self, cap: Capability) -> Self {
124        self.capabilities.push(cap);
125        self
126    }
127
128    /// Set transport configuration
129    pub fn transport(mut self, transport: TransportConfig) -> Self {
130        self.transport = transport;
131        self
132    }
133
134    /// Add custom metadata
135    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    /// Check if service has a specific capability
141    pub fn has_capability(&self, name: &str) -> bool {
142        self.capabilities.iter().any(|c| c.name == name)
143    }
144
145    /// Get capability by name
146    pub fn get_capability(&self, name: &str) -> Option<&Capability> {
147        self.capabilities.iter().find(|c| c.name == name)
148    }
149
150    /// Update service status
151    pub fn set_status(&mut self, status: ServiceStatus) {
152        self.status = status;
153    }
154
155    /// Record a heartbeat
156    pub fn heartbeat(&mut self) {
157        self.last_heartbeat = Some(Instant::now());
158        self.retry_count = 0;
159    }
160
161    /// Check if service is healthy (received heartbeat recently)
162    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/// Capability provided by an extension service
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct Capability {
176    /// Capability name (e.g., "tools", "resources", "prompts")
177    pub name: String,
178    /// Capability version
179    #[serde(default)]
180    pub version: String,
181    /// Capability-specific configuration
182    #[serde(default)]
183    pub config: HashMap<String, serde_json::Value>,
184}
185
186impl Capability {
187    /// Create a new capability
188    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    /// Set capability version
197    pub fn version(mut self, version: impl Into<String>) -> Self {
198        self.version = version.into();
199        self
200    }
201
202    /// Add configuration
203    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/// Transport configuration for extension services
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct TransportConfig {
212    /// Transport type
213    #[serde(rename = "type")]
214    pub transport_type: TransportType,
215    /// Connection address (for TCP/UDP)
216    #[serde(default)]
217    pub address: Option<String>,
218    /// Port number (for TCP/UDP)
219    #[serde(default)]
220    pub port: Option<u16>,
221    /// Command to execute (for stdio)
222    #[serde(default)]
223    pub command: Option<String>,
224    /// Command arguments
225    #[serde(default)]
226    pub args: Vec<String>,
227    /// Environment variables
228    #[serde(default)]
229    pub env: HashMap<String, String>,
230    /// Working directory
231    #[serde(default)]
232    pub cwd: Option<String>,
233    /// Connection timeout in seconds
234    #[serde(default = "default_timeout")]
235    pub timeout_secs: u64,
236    /// Enable auto-reconnect
237    #[serde(default = "default_true")]
238    pub auto_reconnect: bool,
239    /// Maximum retry attempts
240    #[serde(default = "default_max_retries")]
241    pub max_retries: u32,
242    /// Heartbeat interval in seconds
243    #[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/// Transport type
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(rename_all = "lowercase")]
284pub enum TransportType {
285    /// Standard input/output transport
286    Stdio,
287    /// TCP socket transport
288    Tcp,
289    /// Unix domain socket transport
290    #[cfg(unix)]
291    Unix,
292    /// WebSocket transport
293    WebSocket,
294}
295
296/// Service registration info
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct RegistrationInfo {
299    /// Service being registered
300    pub service: ExtensionService,
301    /// Registration timestamp
302    pub registered_at: chrono::DateTime<chrono::Utc>,
303    /// Last update timestamp
304    pub updated_at: chrono::DateTime<chrono::Utc>,
305}
306
307impl RegistrationInfo {
308    /// Create a new registration
309    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    /// Update the timestamp
319    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}