use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use crate::domain::error::{WireError, WireResult};
use crate::domain::port::ProjectionRenderer;
use crate::infrastructure::adapter::Adapter;
use crate::infrastructure::filter::FilterCap;
use crate::infrastructure::template::TemplateEngine;
use crate::infrastructure::wire_uri::WireUri;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdapterInfo {
pub scheme: &'static str,
pub filter_caps: Vec<FilterCap>,
}
#[derive(Clone, Default)]
pub struct PluginRegistry {
adapters: HashMap<&'static str, Arc<dyn Adapter>>,
engines: HashMap<&'static str, Arc<dyn TemplateEngine>>,
projections: HashMap<&'static str, Arc<dyn ProjectionRenderer>>,
}
impl fmt::Debug for PluginRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PluginRegistry")
.field("schemes", &self.schemes())
.field("engine_ids", &self.engine_ids())
.field("projection_kinds", &self.projection_kinds())
.finish()
}
}
impl PluginRegistry {
pub fn builder() -> PluginRegistryBuilder {
PluginRegistryBuilder::default()
}
pub fn default_builder_for_wire() -> PluginRegistryBuilder {
use crate::infrastructure::adapter::FileAdapter;
use crate::infrastructure::projection::StaticProjection;
use crate::infrastructure::template::HandlebarsEngine;
Self::builder()
.with_adapter(FileAdapter)
.with_engine(HandlebarsEngine::new())
.with_projection(StaticProjection::new())
}
pub fn default_for_wire() -> WireResult<Self> {
Self::default_builder_for_wire().build()
}
pub fn adapter_for_uri(&self, source_uri: &str) -> Option<&Arc<dyn Adapter>> {
let scheme = source_uri.split_once(':').map(|(s, _)| s)?;
self.adapters.get(scheme)
}
pub fn route(&self, source_uri: &str) -> WireResult<(Arc<dyn Adapter>, WireUri)> {
let uri = WireUri::parse(source_uri)?;
let adapter = self.adapters.get(uri.scheme()).cloned().ok_or_else(|| {
WireError::Storage(format!(
"plugin registry: no adapter registered for scheme `{}` (uri: {})",
uri.scheme(),
source_uri,
))
})?;
Ok((adapter, uri))
}
pub fn adapter(&self, scheme: &str) -> Option<&Arc<dyn Adapter>> {
self.adapters.get(scheme)
}
pub fn engine(&self, id: &str) -> Option<&Arc<dyn TemplateEngine>> {
self.engines.get(id)
}
pub fn projection(&self, kind: &str) -> Option<&Arc<dyn ProjectionRenderer>> {
self.projections.get(kind)
}
pub fn schemes(&self) -> Vec<&'static str> {
let mut v: Vec<_> = self.adapters.keys().copied().collect();
v.sort_unstable();
v
}
pub fn describe(&self) -> Vec<AdapterInfo> {
let mut v: Vec<AdapterInfo> = self
.adapters
.iter()
.map(|(&scheme, adapter)| AdapterInfo {
scheme,
filter_caps: adapter.filter_caps().to_vec(),
})
.collect();
v.sort_unstable_by_key(|info| info.scheme);
v
}
pub fn engine_ids(&self) -> Vec<&'static str> {
let mut v: Vec<_> = self.engines.keys().copied().collect();
v.sort_unstable();
v
}
pub fn projection_kinds(&self) -> Vec<&'static str> {
let mut v: Vec<_> = self.projections.keys().copied().collect();
v.sort_unstable();
v
}
}
#[derive(Default)]
pub struct PluginRegistryBuilder {
adapters: Vec<Arc<dyn Adapter>>,
engines: Vec<Arc<dyn TemplateEngine>>,
projections: Vec<Arc<dyn ProjectionRenderer>>,
}
impl PluginRegistryBuilder {
pub fn with_adapter<A: Adapter + 'static>(mut self, adapter: A) -> Self {
self.adapters.push(Arc::new(adapter));
self
}
pub fn with_engine<E: TemplateEngine + 'static>(mut self, engine: E) -> Self {
self.engines.push(Arc::new(engine));
self
}
pub fn with_projection<P: ProjectionRenderer + 'static>(mut self, projection: P) -> Self {
self.projections.push(Arc::new(projection));
self
}
pub fn build(self) -> WireResult<PluginRegistry> {
let mut adapters = HashMap::new();
for a in self.adapters {
let scheme = a.scheme();
if adapters.insert(scheme, a).is_some() {
return Err(WireError::Storage(format!(
"plugin registry: duplicate adapter scheme `{scheme}`"
)));
}
}
let mut engines = HashMap::new();
for e in self.engines {
let id = e.id();
if engines.insert(id, e).is_some() {
return Err(WireError::Storage(format!(
"plugin registry: duplicate template engine id `{id}`"
)));
}
}
let mut projections = HashMap::new();
for p in self.projections {
let kind = p.kind();
if projections.insert(kind, p).is_some() {
return Err(WireError::Storage(format!(
"plugin registry: duplicate projection kind `{kind}`"
)));
}
}
Ok(PluginRegistry {
adapters,
engines,
projections,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::infrastructure::adapter::FileAdapter;
use crate::infrastructure::projection::StaticProjection;
use crate::infrastructure::template::HandlebarsEngine;
#[test]
fn empty_registry_has_no_plugins() {
let reg = PluginRegistry::builder().build().unwrap();
assert!(reg.schemes().is_empty());
assert!(reg.engine_ids().is_empty());
assert!(reg.projection_kinds().is_empty());
}
#[test]
fn registers_all_three_axes() {
let reg = PluginRegistry::builder()
.with_adapter(FileAdapter)
.with_engine(HandlebarsEngine::new())
.with_projection(StaticProjection::new())
.build()
.unwrap();
assert_eq!(reg.schemes(), vec!["file"]);
assert_eq!(reg.engine_ids(), vec!["handlebars"]);
assert_eq!(reg.projection_kinds(), vec!["static"]);
}
#[test]
fn default_builder_for_wire_has_core_plugins_only() {
let reg = PluginRegistry::default_builder_for_wire().build().unwrap();
assert_eq!(reg.schemes(), vec!["file"]);
assert_eq!(reg.engine_ids(), vec!["handlebars"]);
assert_eq!(reg.projection_kinds(), vec!["static"]);
}
struct NoFilterAdapter;
#[async_trait::async_trait]
impl Adapter for NoFilterAdapter {
fn scheme(&self) -> &'static str {
"aaa-test"
}
async fn fetch(&self, _uri: &WireUri) -> WireResult<serde_json::Value> {
Ok(serde_json::json!({}))
}
}
#[test]
fn describe_returns_scheme_and_filter_caps_sorted_by_scheme() {
let reg = PluginRegistry::builder()
.with_adapter(FileAdapter)
.with_adapter(NoFilterAdapter)
.build()
.unwrap();
let info = reg.describe();
assert_eq!(info.len(), 2);
assert_eq!(info[0].scheme, "aaa-test");
assert!(
info[0].filter_caps.is_empty(),
"adapter without filter_caps override should describe as empty"
);
assert_eq!(info[1].scheme, "file");
assert_eq!(
info[1].filter_caps,
vec![FilterCap::LineRange, FilterCap::Tail { n_max: 1000 }],
);
}
#[test]
fn describe_empty_registry_returns_empty_vec() {
let reg = PluginRegistry::builder().build().unwrap();
assert!(reg.describe().is_empty());
}
#[test]
fn adapter_for_uri_dispatches_by_scheme() {
let reg = PluginRegistry::builder()
.with_adapter(FileAdapter)
.build()
.unwrap();
assert!(reg.adapter_for_uri("file:///tmp/x").is_some());
assert!(reg.adapter_for_uri("mini-app://x").is_none());
assert!(reg.adapter_for_uri("no-scheme").is_none());
}
#[test]
fn duplicate_scheme_fails_build() {
let err = PluginRegistry::builder()
.with_adapter(FileAdapter)
.with_adapter(FileAdapter)
.build()
.unwrap_err();
let msg = format!("{:?}", err);
assert!(msg.contains("duplicate adapter scheme"));
assert!(msg.contains("file"));
}
#[test]
fn duplicate_engine_fails_build() {
let err = PluginRegistry::builder()
.with_engine(HandlebarsEngine::new())
.with_engine(HandlebarsEngine::new())
.build()
.unwrap_err();
let msg = format!("{:?}", err);
assert!(msg.contains("duplicate template engine id"));
}
#[test]
fn duplicate_projection_fails_build() {
let err = PluginRegistry::builder()
.with_projection(StaticProjection::new())
.with_projection(StaticProjection::new())
.build()
.unwrap_err();
let msg = format!("{:?}", err);
assert!(msg.contains("duplicate projection kind"));
}
}