rapace_registry/introspection.rs
1//! Service introspection support.
2//!
3//! Provides the `ServiceIntrospection` trait and default implementation for
4//! querying available services at runtime.
5
6use facet::Facet;
7
8use crate::ServiceRegistry;
9
10/// Information about a registered service.
11#[derive(Clone, Debug, Facet)]
12pub struct ServiceInfo {
13 /// Service name (e.g., "Calculator").
14 pub name: String,
15 /// Service documentation.
16 pub doc: String,
17 /// Methods provided by this service.
18 pub methods: Vec<MethodInfo>,
19}
20
21/// Information about a method.
22#[derive(Clone, Debug, Facet)]
23pub struct MethodInfo {
24 /// Method ID (for debugging/logging).
25 pub id: u32,
26 /// Method name (e.g., "add").
27 pub name: String,
28 /// Full qualified name (e.g., "Calculator.add").
29 pub full_name: String,
30 /// Method documentation.
31 pub doc: String,
32 /// Argument names and types.
33 pub args: Vec<ArgInfo>,
34 /// Whether this is a streaming method.
35 pub is_streaming: bool,
36}
37
38/// Argument metadata.
39#[derive(Clone, Debug, Facet)]
40pub struct ArgInfo {
41 /// Argument name (e.g., "a", "name").
42 pub name: String,
43 /// Argument type name (e.g., "i32", "String").
44 pub type_name: String,
45}
46
47// Note: We can't use #[rapace::service] here because it would create a circular dependency
48// (rapace-macros depends on rapace-registry). Instead, we define the trait and let
49// the user generate the client/server in a crate that depends on rapace.
50//
51// Users will do:
52// ```
53// use rapace_registry::introspection::ServiceIntrospection;
54//
55// #[rapace::service]
56// pub trait ServiceIntrospection {
57// async fn list_services(&self) -> Vec<ServiceInfo>;
58// async fn describe_service(&self, name: String) -> Option<ServiceInfo>;
59// async fn has_method(&self, method_id: u32) -> bool;
60// }
61// ```
62
63/// Default implementation that reads from the global registry.
64///
65/// This implementation provides runtime introspection by reading
66/// from the global service registry.
67///
68/// # Example
69///
70/// ```ignore
71/// use rapace_registry::introspection::DefaultServiceIntrospection;
72///
73/// let introspection = DefaultServiceIntrospection;
74/// let services = introspection.list_services();
75/// for service in services {
76/// println!("Service: {}", service.name);
77/// }
78/// ```
79#[derive(Clone, Debug, Default)]
80pub struct DefaultServiceIntrospection;
81
82impl DefaultServiceIntrospection {
83 /// Create a new default introspection implementation.
84 pub fn new() -> Self {
85 Self
86 }
87
88 /// List all services registered in the global registry.
89 ///
90 /// Returns a snapshot of all currently registered services and their methods.
91 pub fn list_services(&self) -> Vec<ServiceInfo> {
92 ServiceRegistry::with_global(|registry| {
93 registry
94 .iter_services()
95 .map(|service| ServiceInfo {
96 name: service.name.to_string(),
97 doc: service.doc.clone(),
98 methods: service
99 .iter_methods()
100 .map(|method| MethodInfo {
101 id: method.id.0,
102 name: method.name.to_string(),
103 full_name: method.full_name.clone(),
104 doc: method.doc.clone(),
105 args: method
106 .args
107 .iter()
108 .map(|arg| ArgInfo {
109 name: arg.name.to_string(),
110 type_name: arg.type_name.to_string(),
111 })
112 .collect(),
113 is_streaming: method.is_streaming,
114 })
115 .collect(),
116 })
117 .collect()
118 })
119 }
120
121 /// Describe a specific service by name.
122 ///
123 /// Returns `Some(ServiceInfo)` if the service exists, `None` otherwise.
124 pub fn describe_service(&self, name: &str) -> Option<ServiceInfo> {
125 self.list_services().into_iter().find(|s| s.name == name)
126 }
127
128 /// Check if a method ID is supported.
129 ///
130 /// Returns `true` if any registered service has a method with this ID.
131 pub fn has_method(&self, method_id: u32) -> bool {
132 ServiceRegistry::with_global(|registry| {
133 registry.method_by_id(crate::MethodId(method_id)).is_some()
134 })
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_introspection_empty() {
144 // Note: Global registry might have services from other tests,
145 // so we just verify the API works
146 let introspection = DefaultServiceIntrospection::new();
147 let services = introspection.list_services();
148 // Should return a list (possibly empty or with services from other tests)
149 let _ = services;
150 }
151
152 #[test]
153 fn test_describe_service() {
154 let introspection = DefaultServiceIntrospection::new();
155 // Non-existent service should return None
156 let result = introspection.describe_service("NonExistentService12345");
157 assert!(result.is_none() || result.is_some()); // May or may not exist depending on test order
158 }
159
160 #[test]
161 fn test_has_method() {
162 let introspection = DefaultServiceIntrospection::new();
163 // Method 0 is reserved for control
164 let has_control = introspection.has_method(0);
165 // Should be false (control is not registered as a service method)
166 let _ = has_control;
167 }
168}