rapace_registry/
lib.rs

1//! rapace-registry: Service registry with schema support for rapace RPC.
2//!
3//! This crate provides a registry for RPC services that stores:
4//! - Service names and IDs
5//! - Method names and IDs
6//! - Request/response schemas (via facet shapes)
7//! - Supported encodings per method
8//!
9//! # Example
10//!
11//! ```ignore
12//! use rapace_registry::{ServiceRegistry, ServiceEntry, MethodEntry, Encoding};
13//!
14//! let mut registry = ServiceRegistry::new();
15//!
16//! // Register services (usually done via macro-generated code)
17//! Adder::register(&mut registry);
18//!
19//! // Lookup methods by name
20//! if let Some(method) = registry.lookup_method("Adder", "add") {
21//!     println!("method_id: {}", method.id.0);
22//! }
23//! ```
24
25#![forbid(unsafe_op_in_unsafe_fn)]
26
27use facet_core::Shape;
28use std::collections::HashMap;
29
30/// A unique identifier for a service within a registry.
31///
32/// Service IDs are assigned sequentially when services are registered.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct ServiceId(pub u32);
35
36/// A unique identifier for a method within a registry.
37///
38/// Method IDs are assigned sequentially across all services to ensure
39/// global uniqueness. Method ID 0 is reserved for control frames.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub struct MethodId(pub u32);
42
43impl MethodId {
44    /// Reserved method ID for control channel operations.
45    pub const CONTROL: MethodId = MethodId(0);
46}
47
48/// Supported wire encodings for RPC payloads.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50#[repr(u8)]
51pub enum Encoding {
52    /// Postcard binary encoding (default, most efficient).
53    Postcard = 0,
54    /// JSON text encoding (for debugging/interop).
55    Json = 1,
56}
57
58impl Encoding {
59    /// All available encodings.
60    pub const ALL: &'static [Encoding] = &[Encoding::Postcard, Encoding::Json];
61}
62
63/// Information about an argument to an RPC method.
64#[derive(Debug, Clone)]
65pub struct ArgInfo {
66    /// The argument's name (e.g., "a", "name").
67    pub name: &'static str,
68    /// The argument's type as a string (e.g., "i32", "String").
69    pub type_name: &'static str,
70}
71
72/// Information about a single RPC method.
73#[derive(Debug)]
74pub struct MethodEntry {
75    /// The method's unique ID within the registry.
76    pub id: MethodId,
77    /// The method's name (e.g., "add").
78    pub name: &'static str,
79    /// The canonical full name (e.g., "Adder.add").
80    pub full_name: String,
81    /// Documentation string from the method's `///` comments.
82    pub doc: String,
83    /// The arguments to this method, in order.
84    pub args: Vec<ArgInfo>,
85    /// The request type's shape (schema).
86    pub request_shape: &'static Shape,
87    /// The response type's shape (schema).
88    pub response_shape: &'static Shape,
89    /// Whether this is a streaming method.
90    pub is_streaming: bool,
91    /// Supported wire encodings for this method.
92    pub supported_encodings: Vec<Encoding>,
93}
94
95impl MethodEntry {
96    /// Check if a given encoding is supported by this method.
97    pub fn supports_encoding(&self, encoding: Encoding) -> bool {
98        self.supported_encodings.contains(&encoding)
99    }
100}
101
102/// Information about an RPC service.
103#[derive(Debug)]
104pub struct ServiceEntry {
105    /// The service's unique ID within the registry.
106    pub id: ServiceId,
107    /// The service's name (e.g., "Adder").
108    pub name: &'static str,
109    /// Documentation string from the service trait's `///` comments.
110    pub doc: String,
111    /// Methods provided by this service, keyed by method name.
112    pub methods: HashMap<&'static str, MethodEntry>,
113}
114
115impl ServiceEntry {
116    /// Look up a method by name.
117    pub fn method(&self, name: &str) -> Option<&MethodEntry> {
118        self.methods.get(name)
119    }
120
121    /// Iterate over all methods in this service.
122    pub fn iter_methods(&self) -> impl Iterator<Item = &MethodEntry> {
123        self.methods.values()
124    }
125}
126
127/// A registry of RPC services and their methods.
128///
129/// The registry assigns globally unique method IDs and provides
130/// lookup by name or ID.
131#[derive(Debug, Default)]
132pub struct ServiceRegistry {
133    /// Services keyed by name.
134    services_by_name: HashMap<&'static str, ServiceEntry>,
135    /// Method lookup by ID for fast dispatch.
136    methods_by_id: HashMap<MethodId, MethodLookup>,
137    /// Next service ID to assign.
138    next_service_id: u32,
139    /// Next method ID to assign (starts at 1, 0 is reserved for control).
140    next_method_id: u32,
141}
142
143/// Lookup result for method by ID.
144#[derive(Debug, Clone)]
145struct MethodLookup {
146    service_name: &'static str,
147    method_name: &'static str,
148}
149
150impl ServiceRegistry {
151    /// Create a new empty registry.
152    pub fn new() -> Self {
153        Self {
154            services_by_name: HashMap::new(),
155            methods_by_id: HashMap::new(),
156            next_service_id: 0,
157            next_method_id: 1, // 0 is reserved for control
158        }
159    }
160
161    /// Register a new service with the given name and documentation.
162    ///
163    /// Returns a builder for adding methods to the service.
164    pub fn register_service(
165        &mut self,
166        name: &'static str,
167        doc: impl Into<String>,
168    ) -> ServiceBuilder<'_> {
169        let id = ServiceId(self.next_service_id);
170        self.next_service_id += 1;
171
172        ServiceBuilder {
173            registry: self,
174            service_name: name,
175            service_doc: doc.into(),
176            service_id: id,
177            methods: HashMap::new(),
178        }
179    }
180
181    /// Look up a service by name.
182    pub fn service(&self, name: &str) -> Option<&ServiceEntry> {
183        self.services_by_name.get(name)
184    }
185
186    /// Look up a method by service name and method name.
187    pub fn lookup_method(&self, service_name: &str, method_name: &str) -> Option<&MethodEntry> {
188        self.services_by_name
189            .get(service_name)
190            .and_then(|s| s.method(method_name))
191    }
192
193    /// Look up a method by its ID.
194    pub fn method_by_id(&self, id: MethodId) -> Option<&MethodEntry> {
195        let lookup = self.methods_by_id.get(&id)?;
196        self.lookup_method(lookup.service_name, lookup.method_name)
197    }
198
199    /// Resolve a (service_name, method_name) pair to a MethodId.
200    pub fn resolve_method_id(&self, service_name: &str, method_name: &str) -> Option<MethodId> {
201        self.lookup_method(service_name, method_name).map(|m| m.id)
202    }
203
204    /// Iterate over all registered services.
205    pub fn iter_services(&self) -> impl Iterator<Item = &ServiceEntry> {
206        self.services_by_name.values()
207    }
208
209    /// Iterate over all registered services (alias for iter_services).
210    pub fn services(&self) -> impl Iterator<Item = &ServiceEntry> {
211        self.iter_services()
212    }
213
214    /// Look up a service by its ID.
215    pub fn service_by_id(&self, id: ServiceId) -> Option<&ServiceEntry> {
216        self.services_by_name.values().find(|s| s.id == id)
217    }
218
219    /// Get the total number of registered services.
220    pub fn service_count(&self) -> usize {
221        self.services_by_name.len()
222    }
223
224    /// Get the total number of registered methods (excluding control).
225    pub fn method_count(&self) -> usize {
226        self.methods_by_id.len()
227    }
228}
229
230/// Builder for registering methods on a service.
231pub struct ServiceBuilder<'a> {
232    registry: &'a mut ServiceRegistry,
233    service_name: &'static str,
234    service_doc: String,
235    service_id: ServiceId,
236    methods: HashMap<&'static str, MethodEntry>,
237}
238
239impl ServiceBuilder<'_> {
240    /// Add a unary method to the service.
241    pub fn add_method(
242        &mut self,
243        name: &'static str,
244        doc: impl Into<String>,
245        args: Vec<ArgInfo>,
246        request_shape: &'static Shape,
247        response_shape: &'static Shape,
248    ) -> MethodId {
249        self.add_method_inner(name, doc.into(), args, request_shape, response_shape, false)
250    }
251
252    /// Add a streaming method to the service.
253    pub fn add_streaming_method(
254        &mut self,
255        name: &'static str,
256        doc: impl Into<String>,
257        args: Vec<ArgInfo>,
258        request_shape: &'static Shape,
259        response_shape: &'static Shape,
260    ) -> MethodId {
261        self.add_method_inner(name, doc.into(), args, request_shape, response_shape, true)
262    }
263
264    fn add_method_inner(
265        &mut self,
266        name: &'static str,
267        doc: String,
268        args: Vec<ArgInfo>,
269        request_shape: &'static Shape,
270        response_shape: &'static Shape,
271        is_streaming: bool,
272    ) -> MethodId {
273        let id = MethodId(self.registry.next_method_id);
274        self.registry.next_method_id += 1;
275
276        let full_name = format!("{}.{}", self.service_name, name);
277
278        let entry = MethodEntry {
279            id,
280            name,
281            full_name,
282            doc,
283            args,
284            request_shape,
285            response_shape,
286            is_streaming,
287            supported_encodings: vec![Encoding::Postcard], // Default to postcard only
288        };
289
290        self.methods.insert(name, entry);
291
292        // Register in the global method lookup
293        self.registry.methods_by_id.insert(
294            id,
295            MethodLookup {
296                service_name: self.service_name,
297                method_name: name,
298            },
299        );
300
301        id
302    }
303
304    /// Finish building the service and add it to the registry.
305    pub fn finish(self) {
306        let entry = ServiceEntry {
307            id: self.service_id,
308            name: self.service_name,
309            doc: self.service_doc,
310            methods: self.methods,
311        };
312        self.registry
313            .services_by_name
314            .insert(self.service_name, entry);
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use facet::Facet;
322
323    #[derive(Facet)]
324    struct AddRequest {
325        a: i32,
326        b: i32,
327    }
328
329    #[derive(Facet)]
330    struct AddResponse {
331        result: i32,
332    }
333
334    #[derive(Facet)]
335    struct RangeRequest {
336        n: u32,
337    }
338
339    #[derive(Facet)]
340    struct RangeItem {
341        value: u32,
342    }
343
344    #[test]
345    fn test_register_service() {
346        let mut registry = ServiceRegistry::new();
347
348        let mut builder = registry.register_service("Adder", "A simple adder service.");
349        let add_id = builder.add_method(
350            "add",
351            "Add two numbers together.",
352            vec![
353                ArgInfo {
354                    name: "a",
355                    type_name: "i32",
356                },
357                ArgInfo {
358                    name: "b",
359                    type_name: "i32",
360                },
361            ],
362            <AddRequest as Facet>::SHAPE,
363            <AddResponse as Facet>::SHAPE,
364        );
365        builder.finish();
366
367        assert_eq!(registry.service_count(), 1);
368        assert_eq!(registry.method_count(), 1);
369
370        let service = registry.service("Adder").unwrap();
371        assert_eq!(service.name, "Adder");
372        assert_eq!(service.doc, "A simple adder service.");
373        assert_eq!(service.id.0, 0);
374
375        let method = service.method("add").unwrap();
376        assert_eq!(method.id, add_id);
377        assert_eq!(method.name, "add");
378        assert_eq!(method.full_name, "Adder.add");
379        assert_eq!(method.doc, "Add two numbers together.");
380        assert!(!method.is_streaming);
381        assert_eq!(method.args.len(), 2);
382        assert_eq!(method.args[0].name, "a");
383        assert_eq!(method.args[1].name, "b");
384    }
385
386    #[test]
387    fn test_register_multiple_services() {
388        let mut registry = ServiceRegistry::new();
389
390        // Register Adder
391        let mut builder = registry.register_service("Adder", "");
392        let add_id = builder.add_method(
393            "add",
394            "",
395            vec![
396                ArgInfo {
397                    name: "a",
398                    type_name: "i32",
399                },
400                ArgInfo {
401                    name: "b",
402                    type_name: "i32",
403                },
404            ],
405            <AddRequest as Facet>::SHAPE,
406            <AddResponse as Facet>::SHAPE,
407        );
408        builder.finish();
409
410        // Register RangeService
411        let mut builder = registry.register_service("RangeService", "");
412        let range_id = builder.add_streaming_method(
413            "range",
414            "",
415            vec![ArgInfo {
416                name: "n",
417                type_name: "u32",
418            }],
419            <RangeRequest as Facet>::SHAPE,
420            <RangeItem as Facet>::SHAPE,
421        );
422        builder.finish();
423
424        assert_eq!(registry.service_count(), 2);
425        assert_eq!(registry.method_count(), 2);
426
427        // Method IDs should be unique across services
428        assert_ne!(add_id, range_id);
429        assert_eq!(add_id.0, 1); // First method after control (0)
430        assert_eq!(range_id.0, 2);
431
432        // Lookup by name
433        let method = registry.lookup_method("RangeService", "range").unwrap();
434        assert!(method.is_streaming);
435
436        // Lookup by ID
437        let method = registry.method_by_id(range_id).unwrap();
438        assert_eq!(method.full_name, "RangeService.range");
439    }
440
441    #[test]
442    fn test_resolve_method_id() {
443        let mut registry = ServiceRegistry::new();
444
445        let mut builder = registry.register_service("Adder", "");
446        builder.add_method(
447            "add",
448            "",
449            vec![
450                ArgInfo {
451                    name: "a",
452                    type_name: "i32",
453                },
454                ArgInfo {
455                    name: "b",
456                    type_name: "i32",
457                },
458            ],
459            <AddRequest as Facet>::SHAPE,
460            <AddResponse as Facet>::SHAPE,
461        );
462        builder.finish();
463
464        let id = registry.resolve_method_id("Adder", "add").unwrap();
465        assert_eq!(id.0, 1);
466
467        // Non-existent lookups return None
468        assert!(registry.resolve_method_id("Adder", "subtract").is_none());
469        assert!(registry.resolve_method_id("Calculator", "add").is_none());
470    }
471
472    #[test]
473    fn test_method_id_zero_reserved() {
474        assert_eq!(MethodId::CONTROL.0, 0);
475
476        let mut registry = ServiceRegistry::new();
477        let mut builder = registry.register_service("Test", "");
478        let first_method_id = builder.add_method(
479            "test",
480            "",
481            vec![],
482            <AddRequest as Facet>::SHAPE,
483            <AddResponse as Facet>::SHAPE,
484        );
485        builder.finish();
486
487        // First assigned method ID should be 1, not 0
488        assert_eq!(first_method_id.0, 1);
489    }
490
491    #[test]
492    fn test_encoding_support() {
493        let mut registry = ServiceRegistry::new();
494
495        let mut builder = registry.register_service("Adder", "");
496        builder.add_method(
497            "add",
498            "",
499            vec![
500                ArgInfo {
501                    name: "a",
502                    type_name: "i32",
503                },
504                ArgInfo {
505                    name: "b",
506                    type_name: "i32",
507                },
508            ],
509            <AddRequest as Facet>::SHAPE,
510            <AddResponse as Facet>::SHAPE,
511        );
512        builder.finish();
513
514        let method = registry.lookup_method("Adder", "add").unwrap();
515
516        // By default, only Postcard is supported
517        assert!(method.supports_encoding(Encoding::Postcard));
518        assert!(!method.supports_encoding(Encoding::Json));
519    }
520
521    #[test]
522    fn test_shapes_are_present() {
523        let mut registry = ServiceRegistry::new();
524
525        let mut builder = registry.register_service("Adder", "");
526        builder.add_method(
527            "add",
528            "",
529            vec![
530                ArgInfo {
531                    name: "a",
532                    type_name: "i32",
533                },
534                ArgInfo {
535                    name: "b",
536                    type_name: "i32",
537                },
538            ],
539            <AddRequest as Facet>::SHAPE,
540            <AddResponse as Facet>::SHAPE,
541        );
542        builder.finish();
543
544        let method = registry.lookup_method("Adder", "add").unwrap();
545
546        // Shapes should be non-null static references
547        assert!(!method.request_shape.type_identifier.is_empty());
548        assert!(!method.response_shape.type_identifier.is_empty());
549    }
550
551    #[test]
552    fn test_docs_captured() {
553        let mut registry = ServiceRegistry::new();
554
555        let service_doc = "This is the service documentation.\nIt can span multiple lines.";
556        let method_doc = "This method adds two numbers.\n\n# Arguments\n* `a` - First number\n* `b` - Second number";
557
558        let mut builder = registry.register_service("Calculator", service_doc);
559        builder.add_method(
560            "add",
561            method_doc,
562            vec![
563                ArgInfo {
564                    name: "a",
565                    type_name: "i32",
566                },
567                ArgInfo {
568                    name: "b",
569                    type_name: "i32",
570                },
571            ],
572            <AddRequest as Facet>::SHAPE,
573            <AddResponse as Facet>::SHAPE,
574        );
575        builder.finish();
576
577        let service = registry.service("Calculator").unwrap();
578        assert_eq!(service.doc, service_doc);
579
580        let method = service.method("add").unwrap();
581        assert_eq!(method.doc, method_doc);
582    }
583}