use chrono::{DateTime, Utc};
use drasi_plugin_sdk::{
BootstrapPluginDescriptor, ReactionPluginDescriptor, SourcePluginDescriptor,
};
use std::collections::HashMap;
use std::sync::Arc;
pub struct RegisteredDescriptor<T: ?Sized> {
pub descriptor: Arc<T>,
pub plugin_id: String,
pub registered_at: DateTime<Utc>,
}
impl<T: ?Sized> std::fmt::Debug for RegisteredDescriptor<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegisteredDescriptor")
.field("plugin_id", &self.plugin_id)
.field("registered_at", &self.registered_at)
.finish()
}
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginKindInfo {
pub kind: String,
pub config_version: String,
pub config_schema_json: String,
pub config_schema_name: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub plugin_id: String,
}
pub struct PluginRegistry {
sources: HashMap<String, RegisteredDescriptor<dyn SourcePluginDescriptor>>,
reactions: HashMap<String, RegisteredDescriptor<dyn ReactionPluginDescriptor>>,
bootstrappers: HashMap<String, RegisteredDescriptor<dyn BootstrapPluginDescriptor>>,
version: u64,
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
sources: HashMap::new(),
reactions: HashMap::new(),
bootstrappers: HashMap::new(),
version: 0,
}
}
pub fn version(&self) -> u64 {
self.version
}
pub fn register_source(&mut self, descriptor: Arc<dyn SourcePluginDescriptor>) {
let kind = descriptor.kind().to_string();
self.sources.insert(
kind,
RegisteredDescriptor {
descriptor,
plugin_id: String::new(),
registered_at: Utc::now(),
},
);
self.version += 1;
}
pub fn register_source_with_metadata(
&mut self,
descriptor: Arc<dyn SourcePluginDescriptor>,
plugin_id: &str,
) {
let kind = descriptor.kind().to_string();
self.sources.insert(
kind,
RegisteredDescriptor {
descriptor,
plugin_id: plugin_id.to_string(),
registered_at: Utc::now(),
},
);
self.version += 1;
}
pub fn register_reaction(&mut self, descriptor: Arc<dyn ReactionPluginDescriptor>) {
let kind = descriptor.kind().to_string();
self.reactions.insert(
kind,
RegisteredDescriptor {
descriptor,
plugin_id: String::new(),
registered_at: Utc::now(),
},
);
self.version += 1;
}
pub fn register_reaction_with_metadata(
&mut self,
descriptor: Arc<dyn ReactionPluginDescriptor>,
plugin_id: &str,
) {
let kind = descriptor.kind().to_string();
self.reactions.insert(
kind,
RegisteredDescriptor {
descriptor,
plugin_id: plugin_id.to_string(),
registered_at: Utc::now(),
},
);
self.version += 1;
}
pub fn register_bootstrapper(&mut self, descriptor: Arc<dyn BootstrapPluginDescriptor>) {
let kind = descriptor.kind().to_string();
self.bootstrappers.insert(
kind,
RegisteredDescriptor {
descriptor,
plugin_id: String::new(),
registered_at: Utc::now(),
},
);
self.version += 1;
}
pub fn register_bootstrapper_with_metadata(
&mut self,
descriptor: Arc<dyn BootstrapPluginDescriptor>,
plugin_id: &str,
) {
let kind = descriptor.kind().to_string();
self.bootstrappers.insert(
kind,
RegisteredDescriptor {
descriptor,
plugin_id: plugin_id.to_string(),
registered_at: Utc::now(),
},
);
self.version += 1;
}
pub fn get_source(&self, kind: &str) -> Option<&Arc<dyn SourcePluginDescriptor>> {
self.sources.get(kind).map(|r| &r.descriptor)
}
pub fn get_reaction(&self, kind: &str) -> Option<&Arc<dyn ReactionPluginDescriptor>> {
self.reactions.get(kind).map(|r| &r.descriptor)
}
pub fn get_bootstrapper(&self, kind: &str) -> Option<&Arc<dyn BootstrapPluginDescriptor>> {
self.bootstrappers.get(kind).map(|r| &r.descriptor)
}
pub fn get_source_registration(
&self,
kind: &str,
) -> Option<&RegisteredDescriptor<dyn SourcePluginDescriptor>> {
self.sources.get(kind)
}
pub fn get_reaction_registration(
&self,
kind: &str,
) -> Option<&RegisteredDescriptor<dyn ReactionPluginDescriptor>> {
self.reactions.get(kind)
}
pub fn get_bootstrapper_registration(
&self,
kind: &str,
) -> Option<&RegisteredDescriptor<dyn BootstrapPluginDescriptor>> {
self.bootstrappers.get(kind)
}
pub fn source_kinds(&self) -> Vec<&str> {
let mut kinds: Vec<&str> = self.sources.keys().map(String::as_str).collect();
kinds.sort();
kinds
}
pub fn reaction_kinds(&self) -> Vec<&str> {
let mut kinds: Vec<&str> = self.reactions.keys().map(String::as_str).collect();
kinds.sort();
kinds
}
pub fn bootstrapper_kinds(&self) -> Vec<&str> {
let mut kinds: Vec<&str> = self.bootstrappers.keys().map(String::as_str).collect();
kinds.sort();
kinds
}
pub fn source_plugin_infos(&self) -> Vec<PluginKindInfo> {
let mut infos: Vec<PluginKindInfo> = self
.sources
.values()
.map(|r| PluginKindInfo {
kind: r.descriptor.kind().to_string(),
config_version: r.descriptor.config_version().to_string(),
config_schema_json: r.descriptor.config_schema_json(),
config_schema_name: r.descriptor.config_schema_name().to_string(),
plugin_id: r.plugin_id.clone(),
})
.collect();
infos.sort_by(|a, b| a.kind.cmp(&b.kind));
infos
}
pub fn reaction_plugin_infos(&self) -> Vec<PluginKindInfo> {
let mut infos: Vec<PluginKindInfo> = self
.reactions
.values()
.map(|r| PluginKindInfo {
kind: r.descriptor.kind().to_string(),
config_version: r.descriptor.config_version().to_string(),
config_schema_json: r.descriptor.config_schema_json(),
config_schema_name: r.descriptor.config_schema_name().to_string(),
plugin_id: r.plugin_id.clone(),
})
.collect();
infos.sort_by(|a, b| a.kind.cmp(&b.kind));
infos
}
pub fn bootstrapper_plugin_infos(&self) -> Vec<PluginKindInfo> {
let mut infos: Vec<PluginKindInfo> = self
.bootstrappers
.values()
.map(|r| PluginKindInfo {
kind: r.descriptor.kind().to_string(),
config_version: r.descriptor.config_version().to_string(),
config_schema_json: r.descriptor.config_schema_json(),
config_schema_name: r.descriptor.config_schema_name().to_string(),
plugin_id: r.plugin_id.clone(),
})
.collect();
infos.sort_by(|a, b| a.kind.cmp(&b.kind));
infos
}
pub fn is_empty(&self) -> bool {
self.sources.is_empty() && self.reactions.is_empty() && self.bootstrappers.is_empty()
}
pub fn descriptor_count(&self) -> usize {
self.sources.len() + self.reactions.len() + self.bootstrappers.len()
}
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for PluginRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PluginRegistry")
.field("sources", &self.source_kinds())
.field("reactions", &self.reaction_kinds())
.field("bootstrappers", &self.bootstrapper_kinds())
.field("version", &self.version)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use drasi_lib::sources::Source;
struct MockSourceDescriptor {
kind: &'static str,
}
#[async_trait]
impl SourcePluginDescriptor for MockSourceDescriptor {
fn kind(&self) -> &str {
self.kind
}
fn config_version(&self) -> &str {
"1.0.0"
}
fn config_schema_json(&self) -> String {
r#"{"MockSourceConfig":{"type":"object","properties":{"host":{"type":"string"}}}}"#
.to_string()
}
fn config_schema_name(&self) -> &str {
"MockSourceConfig"
}
async fn create_source(
&self,
_id: &str,
_config_json: &serde_json::Value,
_auto_start: bool,
) -> anyhow::Result<Box<dyn Source>> {
anyhow::bail!("mock: not implemented")
}
}
struct MockReactionDescriptor {
kind: &'static str,
}
#[async_trait]
impl ReactionPluginDescriptor for MockReactionDescriptor {
fn kind(&self) -> &str {
self.kind
}
fn config_version(&self) -> &str {
"1.0.0"
}
fn config_schema_json(&self) -> String {
r#"{"MockReactionConfig":{"type":"object"}}"#.to_string()
}
fn config_schema_name(&self) -> &str {
"MockReactionConfig"
}
async fn create_reaction(
&self,
_id: &str,
_query_ids: Vec<String>,
_config_json: &serde_json::Value,
_auto_start: bool,
) -> anyhow::Result<Box<dyn drasi_lib::reactions::Reaction>> {
anyhow::bail!("mock: not implemented")
}
}
#[test]
fn test_new_registry_is_empty() {
let registry = PluginRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.descriptor_count(), 0);
assert_eq!(registry.version(), 0);
}
#[test]
fn test_register_source() {
let mut registry = PluginRegistry::new();
registry.register_source(Arc::new(MockSourceDescriptor { kind: "mock" }));
assert_eq!(registry.source_kinds(), vec!["mock"]);
assert!(registry.get_source("mock").is_some());
assert!(registry.get_source("nonexistent").is_none());
assert_eq!(registry.descriptor_count(), 1);
assert_eq!(registry.version(), 1);
}
#[test]
fn test_source_plugin_infos_includes_plugin_id() {
let mut registry = PluginRegistry::new();
registry.register_source_with_metadata(
Arc::new(MockSourceDescriptor { kind: "mock" }),
"drasi-source-mock",
);
let infos = registry.source_plugin_infos();
assert_eq!(infos.len(), 1);
assert_eq!(infos[0].plugin_id, "drasi-source-mock");
}
#[test]
fn test_version_increments_on_mutations() {
let mut registry = PluginRegistry::new();
assert_eq!(registry.version(), 0);
registry.register_source(Arc::new(MockSourceDescriptor { kind: "a" }));
assert_eq!(registry.version(), 1);
registry.register_reaction(Arc::new(MockReactionDescriptor { kind: "b" }));
assert_eq!(registry.version(), 2);
}
#[test]
fn test_duplicate_kind_replaces() {
let mut registry = PluginRegistry::new();
registry.register_source(Arc::new(MockSourceDescriptor { kind: "mock" }));
registry.register_source(Arc::new(MockSourceDescriptor { kind: "mock" }));
assert_eq!(registry.source_kinds(), vec!["mock"]);
assert_eq!(registry.descriptor_count(), 1);
}
#[test]
fn test_kinds_are_sorted() {
let mut registry = PluginRegistry::new();
registry.register_source(Arc::new(MockSourceDescriptor { kind: "zeta" }));
registry.register_source(Arc::new(MockSourceDescriptor { kind: "alpha" }));
registry.register_source(Arc::new(MockSourceDescriptor { kind: "beta" }));
assert_eq!(registry.source_kinds(), vec!["alpha", "beta", "zeta"]);
}
#[test]
fn test_register_with_metadata_tracks_plugin_id() {
let mut registry = PluginRegistry::new();
registry.register_source_with_metadata(
Arc::new(MockSourceDescriptor { kind: "pg" }),
"drasi-source-pg",
);
registry.register_reaction_with_metadata(
Arc::new(MockReactionDescriptor { kind: "webhook" }),
"drasi-reaction-webhook",
);
let src_reg = registry
.get_source_registration("pg")
.expect("source exists");
assert_eq!(src_reg.plugin_id, "drasi-source-pg");
let rx_reg = registry
.get_reaction_registration("webhook")
.expect("reaction exists");
assert_eq!(rx_reg.plugin_id, "drasi-reaction-webhook");
let src_infos = registry.source_plugin_infos();
assert_eq!(src_infos.len(), 1);
assert_eq!(src_infos[0].plugin_id, "drasi-source-pg");
assert_eq!(src_infos[0].kind, "pg");
let rx_infos = registry.reaction_plugin_infos();
assert_eq!(rx_infos.len(), 1);
assert_eq!(rx_infos[0].plugin_id, "drasi-reaction-webhook");
assert_eq!(rx_infos[0].kind, "webhook");
}
#[test]
fn test_descriptor_count_across_categories() {
let mut registry = PluginRegistry::new();
assert_eq!(registry.descriptor_count(), 0);
registry.register_source(Arc::new(MockSourceDescriptor { kind: "s1" }));
registry.register_source(Arc::new(MockSourceDescriptor { kind: "s2" }));
registry.register_reaction(Arc::new(MockReactionDescriptor { kind: "r1" }));
assert_eq!(registry.descriptor_count(), 3);
assert!(!registry.is_empty());
}
#[test]
fn test_get_registration_none_for_missing() {
let registry = PluginRegistry::new();
assert!(registry.get_source_registration("x").is_none());
assert!(registry.get_reaction_registration("x").is_none());
assert!(registry.get_bootstrapper_registration("x").is_none());
}
#[test]
fn test_replace_updates_metadata() {
let mut registry = PluginRegistry::new();
registry.register_source_with_metadata(
Arc::new(MockSourceDescriptor { kind: "pg" }),
"plugin-v1",
);
registry.register_source_with_metadata(
Arc::new(MockSourceDescriptor { kind: "pg" }),
"plugin-v2",
);
let reg = registry.get_source_registration("pg").expect("exists");
assert_eq!(reg.plugin_id, "plugin-v2");
assert_eq!(registry.descriptor_count(), 1); }
#[test]
fn test_default_trait() {
let registry = PluginRegistry::default();
assert!(registry.is_empty());
assert_eq!(registry.version(), 0);
}
#[test]
fn test_debug_format() {
let mut registry = PluginRegistry::new();
registry.register_source(Arc::new(MockSourceDescriptor { kind: "mock" }));
let debug_str = format!("{registry:?}");
assert!(debug_str.contains("PluginRegistry"));
assert!(debug_str.contains("mock"));
}
}