host_extensions/
registry.rs1use crate::trait_def::{HostExtension, HostPushEvent};
2
3pub struct ExtensionRegistry {
9 extensions: Vec<Box<dyn HostExtension>>,
10}
11
12impl Default for ExtensionRegistry {
13 fn default() -> Self {
14 Self::new()
15 }
16}
17
18impl ExtensionRegistry {
19 pub fn new() -> Self {
20 Self {
21 extensions: Vec::new(),
22 }
23 }
24
25 pub fn register(&mut self, ext: Box<dyn HostExtension>) {
27 log::info!(
28 "[host-extensions] registering extension: namespace={} channel={}",
29 ext.namespace(),
30 ext.channel()
31 );
32 self.extensions.push(ext);
33 }
34
35 pub fn extensions(&self) -> &[Box<dyn HostExtension>] {
37 &self.extensions
38 }
39
40 pub fn combined_inject_script(&self) -> String {
45 let mut out = String::from(
47 "(function(){\
48 if(!window.host){window.host={};}\
49 if(!window.host.ext){window.host.ext={};}\
50 })();\n",
51 );
52 for ext in &self.extensions {
53 out.push_str(ext.inject_script());
54 out.push('\n');
55 }
56 out
57 }
58
59 pub fn channels(&self) -> Vec<&str> {
61 self.extensions.iter().map(|e| e.channel()).collect()
62 }
63
64 pub fn dispatch(&self, channel: &str, method: &str, params: &str) -> Option<String> {
70 log::info!("[host-extensions] dispatch channel={channel} method={method}");
71 self.extensions
72 .iter()
73 .filter(|ext| ext.channel() == channel)
74 .find_map(|ext| ext.handle_message(method, params))
75 }
76
77 pub fn drain_events(&self) -> Vec<HostPushEvent> {
79 self.extensions
80 .iter()
81 .flat_map(|ext| ext.drain_events())
82 .collect()
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 struct EchoExtension;
91
92 impl HostExtension for EchoExtension {
93 fn namespace(&self) -> &str {
94 "echo"
95 }
96 fn channel(&self) -> &str {
97 "hostEchoBridge"
98 }
99 fn inject_script(&self) -> &str {
100 "// echo inject"
101 }
102 fn handle_message(&self, method: &str, params: &str) -> Option<String> {
103 Some(format!(r#"{{"method":"{method}","params":{params}}}"#))
104 }
105 }
106
107 #[test]
108 fn register_and_dispatch() {
109 let mut registry = ExtensionRegistry::new();
110 registry.register(Box::new(EchoExtension));
111
112 assert_eq!(registry.channels(), vec!["hostEchoBridge"]);
113
114 let result = registry.dispatch("hostEchoBridge", "ping", r#"{"x":1}"#);
115 assert_eq!(
116 result,
117 Some(r#"{"method":"ping","params":{"x":1}}"#.to_string())
118 );
119 }
120
121 #[test]
122 fn dispatch_unknown_channel_returns_none() {
123 let registry = ExtensionRegistry::new();
124 assert!(registry.dispatch("nope", "ping", "{}").is_none());
125 }
126
127 #[test]
128 fn combined_inject_script_contains_extension_script() {
129 let mut registry = ExtensionRegistry::new();
130 registry.register(Box::new(EchoExtension));
131 let script = registry.combined_inject_script();
132 assert!(script.contains("// echo inject"));
133 assert!(script.contains("window.host.ext"));
134 }
135
136 struct EmptySharedChannel;
137
138 impl HostExtension for EmptySharedChannel {
139 fn namespace(&self) -> &str {
140 "empty"
141 }
142 fn channel(&self) -> &str {
143 "hostBridge"
144 }
145 fn inject_script(&self) -> &str {
146 "// empty"
147 }
148 fn handle_message(&self, _method: &str, _params: &str) -> Option<String> {
149 None
150 }
151 }
152
153 struct MeshLikeSharedChannel;
154
155 impl HostExtension for MeshLikeSharedChannel {
156 fn namespace(&self) -> &str {
157 "mesh"
158 }
159 fn channel(&self) -> &str {
160 "hostBridge"
161 }
162 fn inject_script(&self) -> &str {
163 "// mesh"
164 }
165 fn handle_message(&self, method: &str, _params: &str) -> Option<String> {
166 (method == "meshStatus").then(|| r#"{"health":"healthy"}"#.to_string())
167 }
168 }
169
170 struct EventfulExtension;
171
172 impl HostExtension for EventfulExtension {
173 fn namespace(&self) -> &str {
174 "events"
175 }
176 fn channel(&self) -> &str {
177 "hostBridge"
178 }
179 fn inject_script(&self) -> &str {
180 "// events"
181 }
182 fn handle_message(&self, _method: &str, _params: &str) -> Option<String> {
183 None
184 }
185 fn drain_events(&self) -> Vec<HostPushEvent> {
186 vec![HostPushEvent {
187 event: "meshTopic".into(),
188 payload_json: r#"{"topic":"room/1"}"#.into(),
189 }]
190 }
191 }
192
193 #[test]
194 fn dispatch_tries_all_extensions_on_shared_channel() {
195 let mut registry = ExtensionRegistry::new();
196 registry.register(Box::new(EmptySharedChannel));
197 registry.register(Box::new(MeshLikeSharedChannel));
198
199 let result = registry.dispatch("hostBridge", "meshStatus", "{}");
200 assert_eq!(result, Some(r#"{"health":"healthy"}"#.to_string()));
201 }
202
203 #[test]
204 fn drain_events_collects_from_registered_extensions() {
205 let mut registry = ExtensionRegistry::new();
206 registry.register(Box::new(EventfulExtension));
207
208 let events = registry.drain_events();
209 assert_eq!(
210 events,
211 vec![HostPushEvent {
212 event: "meshTopic".into(),
213 payload_json: r#"{"topic":"room/1"}"#.into(),
214 }]
215 );
216 }
217}