rapace_registry/
lib.rs

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