1use crate::dialect::{PlatformDialect, PlatformRegistry};
10use crate::event::{EventRegistry, EventSink, RazeriEvent};
11use crate::language::{LanguageRegistry, LanguageRendererFactory};
12use crate::llm::{LlmClientFactory, LlmRegistry};
13use crate::model::{LlmModelAdapter, ModelRegistry};
14use crate::registry::PluginRegistry;
15use crate::storage::{StorageProviderFactory, StorageRegistry};
16use chrono::Utc;
17use razeri_plugin_contract::StoragePlatform;
18use std::sync::Arc;
19
20pub struct PluginRegistryBuilder {
32 dialects: PlatformRegistry,
33 storage: StorageRegistry,
34 llm: LlmRegistry,
35 models: ModelRegistry,
36 language: LanguageRegistry,
37 events: EventRegistry,
38}
39
40impl PluginRegistryBuilder {
41 #[must_use]
43 pub fn new() -> Self {
44 Self {
45 dialects: PlatformRegistry::new(),
46 storage: StorageRegistry::new(),
47 llm: LlmRegistry::new(),
48 models: ModelRegistry::new(),
49 language: LanguageRegistry::new(),
50 events: EventRegistry::new(),
51 }
52 }
53
54 #[must_use]
58 pub fn with_dialect(mut self, name: &str, dialect: Arc<dyn PlatformDialect>) -> Self {
59 self.dialects.register(name.to_string(), dialect);
60 self
61 }
62
63 pub fn register_dialect(&mut self, dialect: Arc<dyn PlatformDialect>) {
69 let name = dialect.info().name.to_string();
70 self.dialects.register(name, dialect);
71 }
72
73 #[must_use]
77 pub fn with_storage(
78 mut self,
79 platform: StoragePlatform,
80 factory: Arc<dyn StorageProviderFactory>,
81 ) -> Self {
82 self.storage.register_known(platform, factory);
83 self
84 }
85
86 #[must_use]
88 pub fn with_storage_extension(
89 mut self,
90 name: String,
91 factory: Arc<dyn StorageProviderFactory>,
92 ) -> Self {
93 self.storage.register_extension(name, factory);
94 self
95 }
96
97 #[must_use]
101 pub fn with_llm(mut self, name: String, factory: Arc<dyn LlmClientFactory>) -> Self {
102 self.llm.register(name, factory);
103 self
104 }
105
106 #[must_use]
110 pub fn with_model(mut self, adapter: Arc<dyn LlmModelAdapter>) -> Self {
111 self.models.register(adapter);
112 self
113 }
114
115 #[must_use]
119 pub fn with_language(
120 mut self,
121 language: String,
122 factory: Arc<dyn LanguageRendererFactory>,
123 ) -> Self {
124 self.language.register(language, factory);
125 self
126 }
127
128 #[must_use]
132 pub fn with_event_sink(mut self, sink: Arc<dyn EventSink>) -> Self {
133 self.events.register(sink);
134 self
135 }
136
137 #[must_use]
141 pub fn with_auto_discovered(mut self) -> Self {
142 for reg in inventory::iter::<DialectRegistration> {
143 let dialect = (reg.factory)();
144 let name = dialect.info().name.to_string();
145 self.dialects.register(name, dialect);
146 self.record_plugin_loaded(reg.name, "dialect", "inventory");
147 }
148 for reg in inventory::iter::<StorageRegistration> {
149 let factory = (reg.factory)();
150 if let Some(platform) = factory.known_platform() {
151 self.storage.register_known(platform, factory);
152 } else {
153 self.storage.register_extension(reg.name.to_string(), factory);
154 }
155 self.record_plugin_loaded(reg.name, "storage", "inventory");
156 }
157 for reg in inventory::iter::<LlmRegistration> {
158 let factory = (reg.factory)();
159 self.llm.register(reg.name.to_string(), factory);
160 self.record_plugin_loaded(reg.name, "llm", "inventory");
161 }
162 for reg in inventory::iter::<ModelRegistration> {
163 let adapter = (reg.factory)();
164 self.models.register(adapter);
165 self.record_plugin_loaded(reg.model_id, "model", "inventory");
166 }
167 for reg in inventory::iter::<LanguageRegistration> {
168 let factory = (reg.factory)();
169 self.language.register(reg.language.to_string(), factory);
170 self.record_plugin_loaded(reg.language, "language", "inventory");
171 }
172 for reg in inventory::iter::<EventSinkRegistration> {
173 let sink = (reg.factory)();
174 self.events.register(sink);
175 self.record_plugin_loaded(reg.name, "event_sink", "inventory");
176 }
177 self
178 }
179
180 fn record_plugin_loaded(&self, name: &str, plugin_type: &str, source: &str) {
182 if let Ok(event) = RazeriEvent::plugin_loaded(
183 format!("plugin_loaded:{plugin_type}:{name}"),
184 Utc::now(),
185 name,
186 plugin_type,
187 source,
188 ) {
189 self.events.record(event);
190 }
191 }
192
193 #[cfg(all(feature = "dynamic", not(target_arch = "wasm32")))]
204 #[must_use]
205 pub fn with_dynamic_plugins(mut self, plugin_dir: &std::path::Path) -> Self {
206 let results = crate::dynamic::load_plugins_from_dir(plugin_dir, &mut self);
207 for result in &results {
208 match result {
209 crate::dynamic::PluginLoadResult::Loaded { name, path } => {
210 tracing::info!("Loaded dynamic plugin '{name}' from {}", path.display());
211 self.record_plugin_loaded(name, "dynamic", &path.display().to_string());
212 }
213 crate::dynamic::PluginLoadResult::VersionMismatch { path, expected, found } => {
214 tracing::warn!(
215 "Plugin {} API version mismatch (expected {expected}, found {found})",
216 path.display()
217 );
218 }
219 crate::dynamic::PluginLoadResult::Failed { path, error } => {
220 tracing::warn!("Failed to load plugin from {}: {error}", path.display());
221 }
222 }
223 }
224 self
225 }
226
227 pub fn build(self) -> PluginRegistry {
231 PluginRegistry {
232 dialects: self.dialects,
233 storage: self.storage,
234 llm: self.llm,
235 models: self.models,
236 language: self.language,
237 events: self.events,
238 }
239 }
240}
241
242impl Default for PluginRegistryBuilder {
243 fn default() -> Self {
247 Self::new()
248 }
249}
250
251pub struct DialectRegistration {
257 pub name: &'static str,
259 pub factory: fn() -> Arc<dyn PlatformDialect>,
261}
262
263impl DialectRegistration {
264 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn PlatformDialect>) -> Self {
266 Self { name, factory }
267 }
268}
269
270inventory::collect!(DialectRegistration);
271
272pub struct StorageRegistration {
274 pub name: &'static str,
276 pub factory: fn() -> Arc<dyn StorageProviderFactory>,
278}
279
280impl StorageRegistration {
281 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn StorageProviderFactory>) -> Self {
283 Self { name, factory }
284 }
285}
286
287inventory::collect!(StorageRegistration);
288
289pub struct LlmRegistration {
291 pub name: &'static str,
293 pub factory: fn() -> Arc<dyn LlmClientFactory>,
295}
296
297impl LlmRegistration {
298 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn LlmClientFactory>) -> Self {
300 Self { name, factory }
301 }
302}
303
304inventory::collect!(LlmRegistration);
305
306pub struct ModelRegistration {
308 pub model_id: &'static str,
310 pub factory: fn() -> Arc<dyn LlmModelAdapter>,
312}
313
314impl ModelRegistration {
315 pub const fn new(model_id: &'static str, factory: fn() -> Arc<dyn LlmModelAdapter>) -> Self {
317 Self { model_id, factory }
318 }
319}
320
321inventory::collect!(ModelRegistration);
322
323pub struct LanguageRegistration {
325 pub language: &'static str,
327 pub factory: fn() -> Arc<dyn LanguageRendererFactory>,
329}
330
331impl LanguageRegistration {
332 pub const fn new(
334 language: &'static str,
335 factory: fn() -> Arc<dyn LanguageRendererFactory>,
336 ) -> Self {
337 Self { language, factory }
338 }
339}
340
341inventory::collect!(LanguageRegistration);
342
343pub struct EventSinkRegistration {
345 pub name: &'static str,
347 pub factory: fn() -> Arc<dyn EventSink>,
349}
350
351impl EventSinkRegistration {
352 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn EventSink>) -> Self {
354 Self { name, factory }
355 }
356}
357
358inventory::collect!(EventSinkRegistration);
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::dialect::{DialectInfo, PlatformDialect};
364 use crate::error::{EventSinkError, LanguageError, LlmError};
365 use crate::event::RazeriEvent;
366 use crate::llm::{ChatResponse, LlmClient, Message, ModelInfo, ToolDefinition};
367 use crate::model::ModelCapabilities;
368 use async_trait::async_trait;
369 use razeri_plugin_contract::DataType;
370 use razeri_plugin_contract::DialectFamily;
371 use razeri_plugin_contract::{
372 DdlRenderer, DialectCapabilities, DmlRenderer, ExpressionRenderer, FormulaRenderer,
373 ProcedureRenderer, ReplicationRenderer, SqlRenderer, StorageRenderer, TaskRenderer,
374 };
375 use std::collections::HashMap;
376
377 struct TestDialect;
380
381 impl SqlRenderer for TestDialect {
382 fn render_data_type(&self, _dt: &DataType) -> String {
387 "TEXT".to_string()
388 }
389 fn quote_identifier(&self, name: &str) -> String {
394 format!("\"{name}\"")
395 }
396 fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
402 name.to_string()
403 }
404 fn render_current_timestamp(&self) -> String {
408 "NOW()".to_string()
409 }
410 }
411
412 impl ExpressionRenderer for TestDialect {}
413 impl DdlRenderer for TestDialect {}
414 impl DmlRenderer for TestDialect {}
415 impl ProcedureRenderer for TestDialect {}
416 impl FormulaRenderer for TestDialect {}
417 impl TaskRenderer for TestDialect {}
418 impl ReplicationRenderer for TestDialect {}
419 impl StorageRenderer for TestDialect {}
420
421 impl DialectCapabilities for TestDialect {
422 fn merge_shape(&self) -> razeri_plugin_contract::MergeShape {
426 razeri_plugin_contract::MergeShape::Merge(
427 razeri_plugin_contract::MergeRestrictions::none(),
428 )
429 }
430 fn supports_identity(&self) -> bool {
434 true
435 }
436 fn supports_sequences(&self) -> bool {
440 true
441 }
442 fn supports_stored_procedures(&self) -> bool {
446 true
447 }
448 }
449
450 impl PlatformDialect for TestDialect {
451 fn info(&self) -> DialectInfo {
455 DialectInfo {
456 name: "test",
457 display_name: "Test DB",
458 aliases: &[],
459 dialect_family: DialectFamily::PostgreSql,
460 family: "PostgreSQL",
461 is_cloud_native: false,
462 is_olap: false,
463 is_postgres_compatible: false,
464 }
465 }
466 }
467
468 struct TestStorageFactory;
469 #[async_trait]
470 impl StorageProviderFactory for TestStorageFactory {
471 fn name(&self) -> &'static str {
475 "test-storage"
476 }
477 fn display_name(&self) -> &'static str {
481 "Test Storage"
482 }
483 fn known_platform(&self) -> Option<StoragePlatform> {
487 None
488 }
489 async fn create(
490 &self,
491 _config: &HashMap<String, String>,
492 ) -> Result<Box<dyn crate::storage::StorageOps>, crate::error::StorageError> {
493 unimplemented!("test only")
494 }
495 fn validate_config(&self, _config: &HashMap<String, String>) -> Vec<String> {
500 vec![]
501 }
502 fn required_config_keys(&self) -> Vec<&str> {
506 vec![]
507 }
508 }
509
510 struct TestLlmFactory;
511 impl LlmClientFactory for TestLlmFactory {
512 fn name(&self) -> &'static str {
516 "test-llm"
517 }
518 fn display_name(&self) -> &'static str {
522 "Test LLM"
523 }
524 fn create(
529 &self,
530 _config: &HashMap<String, String>,
531 ) -> Result<Arc<dyn LlmClient>, LlmError> {
532 Ok(Arc::new(TestLlmClient))
533 }
534 fn validate_config(&self, _config: &HashMap<String, String>) -> Vec<String> {
539 vec![]
540 }
541 fn required_config_keys(&self) -> Vec<&str> {
545 vec![]
546 }
547 }
548
549 struct TestLlmClient;
550 #[async_trait]
551 impl LlmClient for TestLlmClient {
552 async fn chat(
553 &self,
554 _messages: &[Message],
555 _model: Option<&str>,
556 _tools: Option<Vec<ToolDefinition>>,
557 ) -> Result<ChatResponse, LlmError> {
558 Ok(ChatResponse { content: "ok".into(), model: "test".into(), usage: None })
559 }
560 async fn chat_json(
561 &self,
562 _messages: &[Message],
563 _model: Option<&str>,
564 _tools: Option<Vec<ToolDefinition>>,
565 ) -> Result<serde_json::Value, LlmError> {
566 Ok(serde_json::json!({}))
567 }
568 async fn list_models(&self) -> Result<Vec<ModelInfo>, LlmError> {
569 Ok(vec![])
570 }
571 async fn health_check(&self) -> Result<bool, LlmError> {
572 Ok(true)
573 }
574 }
575
576 struct TestModelAdapter;
577 impl LlmModelAdapter for TestModelAdapter {
578 fn model_id(&self) -> &'static str {
582 "test-model"
583 }
584 fn provider_name(&self) -> &'static str {
588 "test-llm"
589 }
590 fn display_name(&self) -> &'static str {
594 "Test Model"
595 }
596 fn capabilities(&self) -> ModelCapabilities {
600 ModelCapabilities {
601 max_context_tokens: 4096,
602 supports_json_mode: true,
603 supports_tools: false,
604 supports_streaming: false,
605 }
606 }
607 }
608
609 struct TestLanguageFactory;
610 impl LanguageRendererFactory for TestLanguageFactory {
611 fn language(&self) -> &'static str {
615 "test-lang"
616 }
617 fn display_name(&self) -> &'static str {
621 "Test Language"
622 }
623 fn create(
628 &self,
629 _config: &HashMap<String, String>,
630 ) -> Result<Arc<dyn crate::language::LanguageRenderer>, LanguageError> {
631 unimplemented!("test only")
632 }
633 }
634
635 struct TestEventSink;
636 #[async_trait]
637 impl crate::event::EventSink for TestEventSink {
638 fn name(&self) -> &'static str {
642 "test-sink"
643 }
644 async fn on_event(&self, _event: &RazeriEvent) -> Result<(), EventSinkError> {
645 Ok(())
646 }
647 async fn flush(&self) -> Result<(), EventSinkError> {
648 Ok(())
649 }
650 }
651
652 #[test]
658 fn builder_creates_registry_with_all_sub_registries() {
659 let registry = PluginRegistryBuilder::new()
660 .with_dialect("postgres", Arc::new(TestDialect))
661 .with_storage_extension("test".into(), Arc::new(TestStorageFactory))
662 .with_llm("test-llm".into(), Arc::new(TestLlmFactory))
663 .with_model(Arc::new(TestModelAdapter))
664 .with_language("test-lang".into(), Arc::new(TestLanguageFactory))
665 .with_event_sink(Arc::new(TestEventSink))
666 .build();
667
668 assert!(registry.dialects.get_by_name("postgres").is_some());
669 assert!(registry.llm.get("test-llm").is_some());
670 assert!(registry.models.get("test-llm", "test-model").is_some());
671 assert!(registry.language.get("test-lang").is_some());
672 assert!(!registry.events.is_empty());
673 }
674
675 #[test]
679 fn builder_chaining_is_ergonomic() {
680 let _registry = PluginRegistryBuilder::new()
682 .with_dialect("postgres", Arc::new(TestDialect))
683 .with_dialect("snowflake", Arc::new(TestDialect))
684 .with_llm("a".into(), Arc::new(TestLlmFactory))
685 .with_llm("b".into(), Arc::new(TestLlmFactory))
686 .with_event_sink(Arc::new(TestEventSink))
687 .with_event_sink(Arc::new(TestEventSink))
688 .build();
689 }
690
691 #[test]
695 fn builder_with_mixed_known_and_extension() {
696 let registry = PluginRegistryBuilder::new()
697 .with_dialect("postgres", Arc::new(TestDialect))
698 .with_dialect("teradata", Arc::new(TestDialect))
699 .with_storage(StoragePlatform::S3, Arc::new(TestStorageFactory))
700 .with_storage_extension("minio".into(), Arc::new(TestStorageFactory))
701 .build();
702
703 assert!(registry.dialects.get_by_name("postgres").is_some());
704 assert!(registry.dialects.get_by_name("teradata").is_some());
705 }
706
707 #[test]
712 fn builder_with_auto_discovered_finds_inventory_entries() {
713 let registry = PluginRegistryBuilder::new().with_auto_discovered().build();
716 assert_eq!(registry.dialects.registered_names(), vec!["outsidedb".to_owned()]);
717 }
718
719 #[test]
723 fn manual_registration_records_plugin_loaded_events() {
724 let builder = PluginRegistryBuilder::new()
725 .with_dialect("postgres", Arc::new(TestDialect))
726 .with_llm("test-llm".into(), Arc::new(TestLlmFactory));
727
728 builder.record_plugin_loaded("TestDialect", "dialect", "manual");
732 builder.record_plugin_loaded("TestLlm", "llm", "manual");
733
734 let registry = builder.build();
735 let events = registry.events.events();
736 assert_eq!(events.len(), 2);
737
738 assert_eq!(events[0].category(), crate::event::EventCategory::Plugin);
739 assert_eq!(events[0].summary().title, "Plugin loaded");
740 assert!(events[0].summary().detail.contains("dialect"));
741
742 assert_eq!(events[1].category(), crate::event::EventCategory::Plugin);
743 assert!(events[1].summary().detail.contains("llm"));
744 }
745
746 #[test]
750 fn auto_discovery_registers_built_in_sinks() {
751 let registry = PluginRegistryBuilder::new().with_auto_discovered().build();
752 assert!(
755 registry.events.len() >= 3,
756 "Expected at least 3 built-in sinks, found {}",
757 registry.events.len()
758 );
759
760 let events = registry.events.events();
762 let sink_events: Vec<_> =
763 events.iter().filter(|e| e.summary().detail.contains("event_sink")).collect();
764 assert!(
765 sink_events.len() >= 3,
766 "Expected at least 3 event_sink load events, found {}",
767 sink_events.len()
768 );
769 }
770}