persona_wire_core/application/
plugin_registry.rs1use std::collections::HashMap;
29use std::fmt;
30use std::sync::Arc;
31
32use crate::domain::error::{WireError, WireResult};
33use crate::domain::port::ProjectionRenderer;
34use crate::infrastructure::adapter::Adapter;
35use crate::infrastructure::filter::FilterCap;
36use crate::infrastructure::template::TemplateEngine;
37use crate::infrastructure::wire_uri::WireUri;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct AdapterInfo {
43 pub scheme: &'static str,
45 pub filter_caps: Vec<FilterCap>,
49}
50
51#[derive(Clone, Default)]
55pub struct PluginRegistry {
56 adapters: HashMap<&'static str, Arc<dyn Adapter>>,
57 engines: HashMap<&'static str, Arc<dyn TemplateEngine>>,
58 projections: HashMap<&'static str, Arc<dyn ProjectionRenderer>>,
59}
60
61impl fmt::Debug for PluginRegistry {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 f.debug_struct("PluginRegistry")
64 .field("schemes", &self.schemes())
65 .field("engine_ids", &self.engine_ids())
66 .field("projection_kinds", &self.projection_kinds())
67 .finish()
68 }
69}
70
71impl PluginRegistry {
72 pub fn builder() -> PluginRegistryBuilder {
74 PluginRegistryBuilder::default()
75 }
76
77 pub fn default_builder_for_wire() -> PluginRegistryBuilder {
88 use crate::infrastructure::adapter::FileAdapter;
89 use crate::infrastructure::projection::StaticProjection;
90 use crate::infrastructure::template::HandlebarsEngine;
91 Self::builder()
92 .with_adapter(FileAdapter)
93 .with_engine(HandlebarsEngine::new())
94 .with_projection(StaticProjection::new())
95 }
96
97 pub fn default_for_wire() -> WireResult<Self> {
101 Self::default_builder_for_wire().build()
102 }
103
104 pub fn adapter_for_uri(&self, source_uri: &str) -> Option<&Arc<dyn Adapter>> {
110 let scheme = source_uri.split_once(':').map(|(s, _)| s)?;
111 self.adapters.get(scheme)
112 }
113
114 pub fn route(&self, source_uri: &str) -> WireResult<(Arc<dyn Adapter>, WireUri)> {
119 let uri = WireUri::parse(source_uri)?;
120 let adapter = self.adapters.get(uri.scheme()).cloned().ok_or_else(|| {
121 WireError::Storage(format!(
122 "plugin registry: no adapter registered for scheme `{}` (uri: {})",
123 uri.scheme(),
124 source_uri,
125 ))
126 })?;
127 Ok((adapter, uri))
128 }
129
130 pub fn adapter(&self, scheme: &str) -> Option<&Arc<dyn Adapter>> {
132 self.adapters.get(scheme)
133 }
134
135 pub fn engine(&self, id: &str) -> Option<&Arc<dyn TemplateEngine>> {
137 self.engines.get(id)
138 }
139
140 pub fn projection(&self, kind: &str) -> Option<&Arc<dyn ProjectionRenderer>> {
142 self.projections.get(kind)
143 }
144
145 pub fn schemes(&self) -> Vec<&'static str> {
147 let mut v: Vec<_> = self.adapters.keys().copied().collect();
148 v.sort_unstable();
149 v
150 }
151
152 pub fn describe(&self) -> Vec<AdapterInfo> {
155 let mut v: Vec<AdapterInfo> = self
156 .adapters
157 .iter()
158 .map(|(&scheme, adapter)| AdapterInfo {
159 scheme,
160 filter_caps: adapter.filter_caps().to_vec(),
161 })
162 .collect();
163 v.sort_unstable_by_key(|info| info.scheme);
164 v
165 }
166
167 pub fn engine_ids(&self) -> Vec<&'static str> {
169 let mut v: Vec<_> = self.engines.keys().copied().collect();
170 v.sort_unstable();
171 v
172 }
173
174 pub fn projection_kinds(&self) -> Vec<&'static str> {
176 let mut v: Vec<_> = self.projections.keys().copied().collect();
177 v.sort_unstable();
178 v
179 }
180}
181
182#[derive(Default)]
184pub struct PluginRegistryBuilder {
185 adapters: Vec<Arc<dyn Adapter>>,
186 engines: Vec<Arc<dyn TemplateEngine>>,
187 projections: Vec<Arc<dyn ProjectionRenderer>>,
188}
189
190impl PluginRegistryBuilder {
191 pub fn with_adapter<A: Adapter + 'static>(mut self, adapter: A) -> Self {
192 self.adapters.push(Arc::new(adapter));
193 self
194 }
195
196 pub fn with_engine<E: TemplateEngine + 'static>(mut self, engine: E) -> Self {
197 self.engines.push(Arc::new(engine));
198 self
199 }
200
201 pub fn with_projection<P: ProjectionRenderer + 'static>(mut self, projection: P) -> Self {
202 self.projections.push(Arc::new(projection));
203 self
204 }
205
206 pub fn build(self) -> WireResult<PluginRegistry> {
208 let mut adapters = HashMap::new();
209 for a in self.adapters {
210 let scheme = a.scheme();
211 if adapters.insert(scheme, a).is_some() {
212 return Err(WireError::Storage(format!(
213 "plugin registry: duplicate adapter scheme `{scheme}`"
214 )));
215 }
216 }
217 let mut engines = HashMap::new();
218 for e in self.engines {
219 let id = e.id();
220 if engines.insert(id, e).is_some() {
221 return Err(WireError::Storage(format!(
222 "plugin registry: duplicate template engine id `{id}`"
223 )));
224 }
225 }
226 let mut projections = HashMap::new();
227 for p in self.projections {
228 let kind = p.kind();
229 if projections.insert(kind, p).is_some() {
230 return Err(WireError::Storage(format!(
231 "plugin registry: duplicate projection kind `{kind}`"
232 )));
233 }
234 }
235 Ok(PluginRegistry {
236 adapters,
237 engines,
238 projections,
239 })
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::infrastructure::adapter::FileAdapter;
247 use crate::infrastructure::projection::StaticProjection;
248 use crate::infrastructure::template::HandlebarsEngine;
249
250 #[test]
251 fn empty_registry_has_no_plugins() {
252 let reg = PluginRegistry::builder().build().unwrap();
253 assert!(reg.schemes().is_empty());
254 assert!(reg.engine_ids().is_empty());
255 assert!(reg.projection_kinds().is_empty());
256 }
257
258 #[test]
259 fn registers_all_three_axes() {
260 let reg = PluginRegistry::builder()
261 .with_adapter(FileAdapter)
262 .with_engine(HandlebarsEngine::new())
263 .with_projection(StaticProjection::new())
264 .build()
265 .unwrap();
266 assert_eq!(reg.schemes(), vec!["file"]);
267 assert_eq!(reg.engine_ids(), vec!["handlebars"]);
268 assert_eq!(reg.projection_kinds(), vec!["static"]);
269 }
270
271 #[test]
272 fn default_builder_for_wire_has_core_plugins_only() {
273 let reg = PluginRegistry::default_builder_for_wire().build().unwrap();
274 assert_eq!(reg.schemes(), vec!["file"]);
275 assert_eq!(reg.engine_ids(), vec!["handlebars"]);
276 assert_eq!(reg.projection_kinds(), vec!["static"]);
277 }
278
279 struct NoFilterAdapter;
281
282 #[async_trait::async_trait]
283 impl Adapter for NoFilterAdapter {
284 fn scheme(&self) -> &'static str {
285 "aaa-test"
286 }
287
288 async fn fetch(&self, _uri: &WireUri) -> WireResult<serde_json::Value> {
289 Ok(serde_json::json!({}))
290 }
291 }
292
293 #[test]
294 fn describe_returns_scheme_and_filter_caps_sorted_by_scheme() {
295 let reg = PluginRegistry::builder()
296 .with_adapter(FileAdapter)
297 .with_adapter(NoFilterAdapter)
298 .build()
299 .unwrap();
300 let info = reg.describe();
301 assert_eq!(info.len(), 2);
302 assert_eq!(info[0].scheme, "aaa-test");
304 assert!(
305 info[0].filter_caps.is_empty(),
306 "adapter without filter_caps override should describe as empty"
307 );
308 assert_eq!(info[1].scheme, "file");
309 assert_eq!(
310 info[1].filter_caps,
311 vec![FilterCap::LineRange, FilterCap::Tail { n_max: 1000 }],
312 );
313 }
314
315 #[test]
316 fn describe_empty_registry_returns_empty_vec() {
317 let reg = PluginRegistry::builder().build().unwrap();
318 assert!(reg.describe().is_empty());
319 }
320
321 #[test]
322 fn adapter_for_uri_dispatches_by_scheme() {
323 let reg = PluginRegistry::builder()
324 .with_adapter(FileAdapter)
325 .build()
326 .unwrap();
327 assert!(reg.adapter_for_uri("file:///tmp/x").is_some());
328 assert!(reg.adapter_for_uri("mini-app://x").is_none());
329 assert!(reg.adapter_for_uri("no-scheme").is_none());
330 }
331
332 #[test]
333 fn duplicate_scheme_fails_build() {
334 let err = PluginRegistry::builder()
335 .with_adapter(FileAdapter)
336 .with_adapter(FileAdapter)
337 .build()
338 .unwrap_err();
339 let msg = format!("{:?}", err);
340 assert!(msg.contains("duplicate adapter scheme"));
341 assert!(msg.contains("file"));
342 }
343
344 #[test]
345 fn duplicate_engine_fails_build() {
346 let err = PluginRegistry::builder()
347 .with_engine(HandlebarsEngine::new())
348 .with_engine(HandlebarsEngine::new())
349 .build()
350 .unwrap_err();
351 let msg = format!("{:?}", err);
352 assert!(msg.contains("duplicate template engine id"));
353 }
354
355 #[test]
356 fn duplicate_projection_fails_build() {
357 let err = PluginRegistry::builder()
358 .with_projection(StaticProjection::new())
359 .with_projection(StaticProjection::new())
360 .build()
361 .unwrap_err();
362 let msg = format!("{:?}", err);
363 assert!(msg.contains("duplicate projection kind"));
364 }
365}