wasm_capability_core/
capability_registry.rs1use svc_registry::{Named, Registry};
6use svc_registry_adapter::DefaultRegistry;
7use wasm_capability_contract::{CapabilityDescriptor, CapabilityProtocol, CapabilityRegistry};
8
9struct NamedCapabilityDescriptor {
17 key: String,
18 descriptor: CapabilityDescriptor,
19}
20
21impl Named for NamedCapabilityDescriptor {
22 fn name(&self) -> &str {
23 &self.key
24 }
25}
26
27pub struct InMemoryCapabilityRegistry {
32 entries: DefaultRegistry<NamedCapabilityDescriptor>,
33}
34
35impl InMemoryCapabilityRegistry {
36 #[must_use]
40 pub fn with_defaults() -> Self {
41 let mut entries = DefaultRegistry::default();
42 for (name, protocol) in [
43 ("http-egress", CapabilityProtocol::Http),
44 ("grpc-egress", CapabilityProtocol::Grpc),
45 ("llm-complete", CapabilityProtocol::Complete),
46 ("mcp-egress", CapabilityProtocol::Mcp),
47 ("database", CapabilityProtocol::Database),
48 ("secrets", CapabilityProtocol::Secrets),
49 ] {
50 entries.register(NamedCapabilityDescriptor {
51 key: name.to_string(),
52 descriptor: CapabilityDescriptor {
53 import_name: name,
54 protocol,
55 },
56 });
57 }
58 Self { entries }
59 }
60
61 #[must_use]
65 pub fn register(mut self, name: impl Into<String>, descriptor: CapabilityDescriptor) -> Self {
66 self.entries.register(NamedCapabilityDescriptor {
67 key: name.into(),
68 descriptor,
69 });
70 self
71 }
72}
73
74impl CapabilityRegistry for InMemoryCapabilityRegistry {
75 fn descriptor(&self, capability: &str) -> Option<&CapabilityDescriptor> {
76 self.entries.get(capability).map(|entry| &entry.descriptor)
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
90 fn test_with_defaults_resolves_all_six_built_in_capabilities_to_correct_protocol() {
91 let registry = InMemoryCapabilityRegistry::with_defaults();
92 let expected = [
93 ("http-egress", CapabilityProtocol::Http),
94 ("grpc-egress", CapabilityProtocol::Grpc),
95 ("llm-complete", CapabilityProtocol::Complete),
96 ("mcp-egress", CapabilityProtocol::Mcp),
97 ("database", CapabilityProtocol::Database),
98 ("secrets", CapabilityProtocol::Secrets),
99 ];
100 for (name, protocol) in expected {
101 let descriptor = registry
102 .descriptor(name)
103 .unwrap_or_else(|| panic!("expected a descriptor for '{name}'"));
104 assert_eq!(descriptor.import_name, name);
105 assert_eq!(descriptor.protocol, protocol);
106 }
107 }
108
109 #[test]
113 fn test_descriptor_returns_none_for_unregistered_capability() {
114 let registry = InMemoryCapabilityRegistry::with_defaults();
115 assert!(registry.descriptor("not-a-real-capability").is_none());
116 }
117
118 #[test]
123 fn test_register_adds_new_entry_without_disturbing_defaults() {
124 let registry = InMemoryCapabilityRegistry::with_defaults().register(
125 "grpc-egress:inventory-svc",
126 CapabilityDescriptor {
127 import_name: "grpc-egress",
128 protocol: CapabilityProtocol::Grpc,
129 },
130 );
131
132 let scoped = registry
133 .descriptor("grpc-egress:inventory-svc")
134 .unwrap_or_else(|| panic!("expected the newly registered instance"));
135 assert_eq!(scoped.import_name, "grpc-egress");
136
137 let default = registry
138 .descriptor("grpc-egress")
139 .unwrap_or_else(|| panic!("expected the default 'grpc-egress' to still resolve"));
140 assert_eq!(default.protocol, CapabilityProtocol::Grpc);
141
142 for name in [
143 "http-egress",
144 "llm-complete",
145 "mcp-egress",
146 "database",
147 "secrets",
148 ] {
149 assert!(
150 registry.descriptor(name).is_some(),
151 "expected default '{name}' to still resolve after register()"
152 );
153 }
154 }
155
156 #[test]
162 fn test_register_replaces_existing_entry_with_same_key() {
163 let registry = InMemoryCapabilityRegistry::with_defaults().register(
164 "http-egress",
165 CapabilityDescriptor {
166 import_name: "http-egress-v2",
167 protocol: CapabilityProtocol::Http,
168 },
169 );
170
171 let descriptor = registry
172 .descriptor("http-egress")
173 .unwrap_or_else(|| panic!("expected 'http-egress' to still resolve"));
174 assert_eq!(descriptor.import_name, "http-egress-v2");
175 }
176}