1use crate::dialect::{PlatformDialect, PlatformRegistry};
8use crate::event::{EventRegistry, EventSink, RazeriEvent};
9use crate::language::{LanguageRegistry, LanguageRendererFactory};
10use crate::llm::{LlmClientFactory, LlmRegistry};
11use crate::model::{LlmModelAdapter, ModelRegistry};
12use crate::registry::PluginRegistry;
13use crate::storage::{StorageProviderFactory, StorageRegistry};
14use chrono::Utc;
15use razeri_plugin_contract::StoragePlatform;
16use std::sync::Arc;
17
18pub struct PluginRegistryBuilder {
30 dialects: PlatformRegistry,
31 storage: StorageRegistry,
32 llm: LlmRegistry,
33 models: ModelRegistry,
34 language: LanguageRegistry,
35 events: EventRegistry,
36}
37
38impl PluginRegistryBuilder {
39 #[must_use]
41 pub fn new() -> Self {
42 Self {
43 dialects: PlatformRegistry::new(),
44 storage: StorageRegistry::new(),
45 llm: LlmRegistry::new(),
46 models: ModelRegistry::new(),
47 language: LanguageRegistry::new(),
48 events: EventRegistry::new(),
49 }
50 }
51
52 #[must_use]
56 pub fn with_dialect(mut self, name: &str, dialect: Arc<dyn PlatformDialect>) -> Self {
57 self.dialects.register(name.to_string(), dialect);
58 self
59 }
60
61 #[must_use]
65 pub fn with_storage(
66 mut self,
67 platform: StoragePlatform,
68 factory: Arc<dyn StorageProviderFactory>,
69 ) -> Self {
70 self.storage.register_known(platform, factory);
71 self
72 }
73
74 #[must_use]
76 pub fn with_storage_extension(
77 mut self,
78 name: String,
79 factory: Arc<dyn StorageProviderFactory>,
80 ) -> Self {
81 self.storage.register_extension(name, factory);
82 self
83 }
84
85 #[must_use]
89 pub fn with_llm(mut self, name: String, factory: Arc<dyn LlmClientFactory>) -> Self {
90 self.llm.register(name, factory);
91 self
92 }
93
94 #[must_use]
98 pub fn with_model(mut self, adapter: Arc<dyn LlmModelAdapter>) -> Self {
99 self.models.register(adapter);
100 self
101 }
102
103 #[must_use]
107 pub fn with_language(
108 mut self,
109 language: String,
110 factory: Arc<dyn LanguageRendererFactory>,
111 ) -> Self {
112 self.language.register(language, factory);
113 self
114 }
115
116 #[must_use]
120 pub fn with_event_sink(mut self, sink: Arc<dyn EventSink>) -> Self {
121 self.events.register(sink);
122 self
123 }
124
125 #[must_use]
129 pub fn with_auto_discovered(mut self) -> Self {
130 for reg in inventory::iter::<DialectRegistration> {
131 let dialect = (reg.factory)();
132 let name = dialect.info().name.to_string();
133 self.dialects.register(name, dialect);
134 self.record_plugin_loaded(reg.name, "dialect", "inventory");
135 }
136 for reg in inventory::iter::<StorageRegistration> {
137 let factory = (reg.factory)();
138 if let Some(platform) = factory.known_platform() {
139 self.storage.register_known(platform, factory);
140 } else {
141 self.storage.register_extension(reg.name.to_string(), factory);
142 }
143 self.record_plugin_loaded(reg.name, "storage", "inventory");
144 }
145 for reg in inventory::iter::<LlmRegistration> {
146 let factory = (reg.factory)();
147 self.llm.register(reg.name.to_string(), factory);
148 self.record_plugin_loaded(reg.name, "llm", "inventory");
149 }
150 for reg in inventory::iter::<ModelRegistration> {
151 let adapter = (reg.factory)();
152 self.models.register(adapter);
153 self.record_plugin_loaded(reg.model_id, "model", "inventory");
154 }
155 for reg in inventory::iter::<LanguageRegistration> {
156 let factory = (reg.factory)();
157 self.language.register(reg.language.to_string(), factory);
158 self.record_plugin_loaded(reg.language, "language", "inventory");
159 }
160 for reg in inventory::iter::<EventSinkRegistration> {
161 let sink = (reg.factory)();
162 self.events.register(sink);
163 self.record_plugin_loaded(reg.name, "event_sink", "inventory");
164 }
165 self
166 }
167
168 fn record_plugin_loaded(&self, name: &str, plugin_type: &str, source: &str) {
170 if let Ok(event) = RazeriEvent::plugin_loaded(
171 format!("plugin_loaded:{plugin_type}:{name}"),
172 Utc::now(),
173 name,
174 plugin_type,
175 source,
176 ) {
177 self.events.record(event);
178 }
179 }
180
181 #[cfg(all(feature = "dynamic", not(target_arch = "wasm32")))]
192 #[must_use]
193 pub fn with_dynamic_plugins(mut self, plugin_dir: &std::path::Path) -> Self {
194 let results = crate::dynamic::load_plugins_from_dir(plugin_dir, &mut self);
195 for result in &results {
196 match result {
197 crate::dynamic::PluginLoadResult::Loaded { name, path } => {
198 tracing::info!("Loaded dynamic plugin '{name}' from {}", path.display());
199 self.record_plugin_loaded(name, "dynamic", &path.display().to_string());
200 }
201 crate::dynamic::PluginLoadResult::VersionMismatch { path, expected, found } => {
202 tracing::warn!(
203 "Plugin {} API version mismatch (expected {expected}, found {found})",
204 path.display()
205 );
206 }
207 crate::dynamic::PluginLoadResult::Failed { path, error } => {
208 tracing::warn!("Failed to load plugin from {}: {error}", path.display());
209 }
210 }
211 }
212 self
213 }
214
215 pub fn build(self) -> PluginRegistry {
219 PluginRegistry {
220 dialects: self.dialects,
221 storage: self.storage,
222 llm: self.llm,
223 models: self.models,
224 language: self.language,
225 events: self.events,
226 }
227 }
228}
229
230impl Default for PluginRegistryBuilder {
231 fn default() -> Self {
235 Self::new()
236 }
237}
238
239pub struct DialectRegistration {
245 pub name: &'static str,
247 pub factory: fn() -> Arc<dyn PlatformDialect>,
249}
250
251impl DialectRegistration {
252 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn PlatformDialect>) -> Self {
254 Self { name, factory }
255 }
256}
257
258inventory::collect!(DialectRegistration);
259
260pub struct StorageRegistration {
262 pub name: &'static str,
264 pub factory: fn() -> Arc<dyn StorageProviderFactory>,
266}
267
268impl StorageRegistration {
269 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn StorageProviderFactory>) -> Self {
271 Self { name, factory }
272 }
273}
274
275inventory::collect!(StorageRegistration);
276
277pub struct LlmRegistration {
279 pub name: &'static str,
281 pub factory: fn() -> Arc<dyn LlmClientFactory>,
283}
284
285impl LlmRegistration {
286 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn LlmClientFactory>) -> Self {
288 Self { name, factory }
289 }
290}
291
292inventory::collect!(LlmRegistration);
293
294pub struct ModelRegistration {
296 pub model_id: &'static str,
298 pub factory: fn() -> Arc<dyn LlmModelAdapter>,
300}
301
302impl ModelRegistration {
303 pub const fn new(model_id: &'static str, factory: fn() -> Arc<dyn LlmModelAdapter>) -> Self {
305 Self { model_id, factory }
306 }
307}
308
309inventory::collect!(ModelRegistration);
310
311pub struct LanguageRegistration {
313 pub language: &'static str,
315 pub factory: fn() -> Arc<dyn LanguageRendererFactory>,
317}
318
319impl LanguageRegistration {
320 pub const fn new(
322 language: &'static str,
323 factory: fn() -> Arc<dyn LanguageRendererFactory>,
324 ) -> Self {
325 Self { language, factory }
326 }
327}
328
329inventory::collect!(LanguageRegistration);
330
331pub struct EventSinkRegistration {
333 pub name: &'static str,
335 pub factory: fn() -> Arc<dyn EventSink>,
337}
338
339impl EventSinkRegistration {
340 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn EventSink>) -> Self {
342 Self { name, factory }
343 }
344}
345
346inventory::collect!(EventSinkRegistration);
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use crate::dialect::{DialectInfo, PlatformDialect};
352 use crate::error::{EventSinkError, LanguageError, LlmError};
353 use crate::event::RazeriEvent;
354 use crate::llm::{ChatResponse, LlmClient, Message, ModelInfo, ToolDefinition};
355 use crate::model::ModelCapabilities;
356 use async_trait::async_trait;
357 use razeri_plugin_contract::DataType;
358 use razeri_plugin_contract::DialectFamily;
359 use razeri_plugin_contract::{
360 DdlRenderer, DialectCapabilities, DmlRenderer, ExpressionRenderer, FormulaRenderer,
361 ProcedureRenderer, ReplicationRenderer, SqlRenderer, StorageRenderer, TaskRenderer,
362 };
363 use std::collections::HashMap;
364
365 struct TestDialect;
368
369 impl SqlRenderer for TestDialect {
370 fn render_data_type(&self, _dt: &DataType) -> String {
375 "TEXT".to_string()
376 }
377 fn quote_identifier(&self, name: &str) -> String {
382 format!("\"{name}\"")
383 }
384 fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
390 name.to_string()
391 }
392 fn render_current_timestamp(&self) -> String {
396 "NOW()".to_string()
397 }
398 }
399
400 impl ExpressionRenderer for TestDialect {}
401 impl DdlRenderer for TestDialect {}
402 impl DmlRenderer for TestDialect {}
403 impl ProcedureRenderer for TestDialect {}
404 impl FormulaRenderer for TestDialect {}
405 impl TaskRenderer for TestDialect {}
406 impl ReplicationRenderer for TestDialect {}
407 impl StorageRenderer for TestDialect {}
408
409 impl DialectCapabilities for TestDialect {
410 fn merge_shape(&self) -> razeri_plugin_contract::MergeShape {
414 razeri_plugin_contract::MergeShape::Merge(
415 razeri_plugin_contract::MergeRestrictions::none(),
416 )
417 }
418 fn supports_identity(&self) -> bool {
422 true
423 }
424 fn supports_sequences(&self) -> bool {
428 true
429 }
430 fn supports_stored_procedures(&self) -> bool {
434 true
435 }
436 }
437
438 impl PlatformDialect for TestDialect {
439 fn info(&self) -> DialectInfo {
443 DialectInfo {
444 name: "test",
445 display_name: "Test DB",
446 dialect_family: DialectFamily::PostgreSql,
447 family: "PostgreSQL",
448 is_cloud_native: false,
449 is_olap: false,
450 is_postgres_compatible: false,
451 }
452 }
453 }
454
455 struct TestStorageFactory;
456 #[async_trait]
457 impl StorageProviderFactory for TestStorageFactory {
458 fn name(&self) -> &'static str {
462 "test-storage"
463 }
464 fn display_name(&self) -> &'static str {
468 "Test Storage"
469 }
470 fn known_platform(&self) -> Option<StoragePlatform> {
474 None
475 }
476 async fn create(
477 &self,
478 _config: &HashMap<String, String>,
479 ) -> Result<Box<dyn crate::storage::StorageOps>, crate::error::StorageError> {
480 unimplemented!("test only")
481 }
482 fn validate_config(&self, _config: &HashMap<String, String>) -> Vec<String> {
487 vec![]
488 }
489 fn required_config_keys(&self) -> Vec<&str> {
493 vec![]
494 }
495 }
496
497 struct TestLlmFactory;
498 impl LlmClientFactory for TestLlmFactory {
499 fn name(&self) -> &'static str {
503 "test-llm"
504 }
505 fn display_name(&self) -> &'static str {
509 "Test LLM"
510 }
511 fn create(
516 &self,
517 _config: &HashMap<String, String>,
518 ) -> Result<Arc<dyn LlmClient>, LlmError> {
519 Ok(Arc::new(TestLlmClient))
520 }
521 fn validate_config(&self, _config: &HashMap<String, String>) -> Vec<String> {
526 vec![]
527 }
528 fn required_config_keys(&self) -> Vec<&str> {
532 vec![]
533 }
534 }
535
536 struct TestLlmClient;
537 #[async_trait]
538 impl LlmClient for TestLlmClient {
539 async fn chat(
540 &self,
541 _messages: &[Message],
542 _model: Option<&str>,
543 _tools: Option<Vec<ToolDefinition>>,
544 ) -> Result<ChatResponse, LlmError> {
545 Ok(ChatResponse { content: "ok".into(), model: "test".into(), usage: None })
546 }
547 async fn chat_json(
548 &self,
549 _messages: &[Message],
550 _model: Option<&str>,
551 _tools: Option<Vec<ToolDefinition>>,
552 ) -> Result<serde_json::Value, LlmError> {
553 Ok(serde_json::json!({}))
554 }
555 async fn list_models(&self) -> Result<Vec<ModelInfo>, LlmError> {
556 Ok(vec![])
557 }
558 async fn health_check(&self) -> Result<bool, LlmError> {
559 Ok(true)
560 }
561 }
562
563 struct TestModelAdapter;
564 impl LlmModelAdapter for TestModelAdapter {
565 fn model_id(&self) -> &'static str {
569 "test-model"
570 }
571 fn provider_name(&self) -> &'static str {
575 "test-llm"
576 }
577 fn display_name(&self) -> &'static str {
581 "Test Model"
582 }
583 fn capabilities(&self) -> ModelCapabilities {
587 ModelCapabilities {
588 max_context_tokens: 4096,
589 supports_json_mode: true,
590 supports_tools: false,
591 supports_streaming: false,
592 }
593 }
594 }
595
596 struct TestLanguageFactory;
597 impl LanguageRendererFactory for TestLanguageFactory {
598 fn language(&self) -> &'static str {
602 "test-lang"
603 }
604 fn display_name(&self) -> &'static str {
608 "Test Language"
609 }
610 fn create(
615 &self,
616 _config: &HashMap<String, String>,
617 ) -> Result<Arc<dyn crate::language::LanguageRenderer>, LanguageError> {
618 unimplemented!("test only")
619 }
620 }
621
622 struct TestEventSink;
623 #[async_trait]
624 impl crate::event::EventSink for TestEventSink {
625 fn name(&self) -> &'static str {
629 "test-sink"
630 }
631 async fn on_event(&self, _event: &RazeriEvent) -> Result<(), EventSinkError> {
632 Ok(())
633 }
634 async fn flush(&self) -> Result<(), EventSinkError> {
635 Ok(())
636 }
637 }
638
639 #[test]
645 fn builder_creates_registry_with_all_sub_registries() {
646 let registry = PluginRegistryBuilder::new()
647 .with_dialect("postgres", Arc::new(TestDialect))
648 .with_storage_extension("test".into(), Arc::new(TestStorageFactory))
649 .with_llm("test-llm".into(), Arc::new(TestLlmFactory))
650 .with_model(Arc::new(TestModelAdapter))
651 .with_language("test-lang".into(), Arc::new(TestLanguageFactory))
652 .with_event_sink(Arc::new(TestEventSink))
653 .build();
654
655 assert!(registry.dialects.get_by_name("postgres").is_some());
656 assert!(registry.llm.get("test-llm").is_some());
657 assert!(registry.models.get("test-llm", "test-model").is_some());
658 assert!(registry.language.get("test-lang").is_some());
659 assert!(!registry.events.is_empty());
660 }
661
662 #[test]
666 fn builder_chaining_is_ergonomic() {
667 let _registry = PluginRegistryBuilder::new()
669 .with_dialect("postgres", Arc::new(TestDialect))
670 .with_dialect("snowflake", Arc::new(TestDialect))
671 .with_llm("a".into(), Arc::new(TestLlmFactory))
672 .with_llm("b".into(), Arc::new(TestLlmFactory))
673 .with_event_sink(Arc::new(TestEventSink))
674 .with_event_sink(Arc::new(TestEventSink))
675 .build();
676 }
677
678 #[test]
682 fn builder_with_mixed_known_and_extension() {
683 let registry = PluginRegistryBuilder::new()
684 .with_dialect("postgres", Arc::new(TestDialect))
685 .with_dialect("teradata", Arc::new(TestDialect))
686 .with_storage(StoragePlatform::S3, Arc::new(TestStorageFactory))
687 .with_storage_extension("minio".into(), Arc::new(TestStorageFactory))
688 .build();
689
690 assert!(registry.dialects.get_by_name("postgres").is_some());
691 assert!(registry.dialects.get_by_name("teradata").is_some());
692 }
693
694 #[test]
698 fn builder_with_auto_discovered_finds_inventory_entries() {
699 let registry = PluginRegistryBuilder::new().with_auto_discovered().build();
702 assert_eq!(registry.dialects.list().len(), 0);
704 }
705
706 #[test]
710 fn manual_registration_records_plugin_loaded_events() {
711 let builder = PluginRegistryBuilder::new()
712 .with_dialect("postgres", Arc::new(TestDialect))
713 .with_llm("test-llm".into(), Arc::new(TestLlmFactory));
714
715 builder.record_plugin_loaded("TestDialect", "dialect", "manual");
719 builder.record_plugin_loaded("TestLlm", "llm", "manual");
720
721 let registry = builder.build();
722 let events = registry.events.events();
723 assert_eq!(events.len(), 2);
724
725 assert_eq!(events[0].category(), crate::event::EventCategory::Plugin);
726 assert_eq!(events[0].summary().title, "Plugin loaded");
727 assert!(events[0].summary().detail.contains("dialect"));
728
729 assert_eq!(events[1].category(), crate::event::EventCategory::Plugin);
730 assert!(events[1].summary().detail.contains("llm"));
731 }
732
733 #[test]
737 fn auto_discovery_registers_built_in_sinks() {
738 let registry = PluginRegistryBuilder::new().with_auto_discovered().build();
739 assert!(
742 registry.events.len() >= 3,
743 "Expected at least 3 built-in sinks, found {}",
744 registry.events.len()
745 );
746
747 let events = registry.events.events();
749 let sink_events: Vec<_> =
750 events.iter().filter(|e| e.summary().detail.contains("event_sink")).collect();
751 assert!(
752 sink_events.len() >= 3,
753 "Expected at least 3 event_sink load events, found {}",
754 sink_events.len()
755 );
756 }
757}