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 #[must_use]
67 pub fn with_storage(
68 mut self,
69 platform: StoragePlatform,
70 factory: Arc<dyn StorageProviderFactory>,
71 ) -> Self {
72 self.storage.register_known(platform, factory);
73 self
74 }
75
76 #[must_use]
78 pub fn with_storage_extension(
79 mut self,
80 name: String,
81 factory: Arc<dyn StorageProviderFactory>,
82 ) -> Self {
83 self.storage.register_extension(name, factory);
84 self
85 }
86
87 #[must_use]
91 pub fn with_llm(mut self, name: String, factory: Arc<dyn LlmClientFactory>) -> Self {
92 self.llm.register(name, factory);
93 self
94 }
95
96 #[must_use]
100 pub fn with_model(mut self, adapter: Arc<dyn LlmModelAdapter>) -> Self {
101 self.models.register(adapter);
102 self
103 }
104
105 #[must_use]
109 pub fn with_language(
110 mut self,
111 language: String,
112 factory: Arc<dyn LanguageRendererFactory>,
113 ) -> Self {
114 self.language.register(language, factory);
115 self
116 }
117
118 #[must_use]
122 pub fn with_event_sink(mut self, sink: Arc<dyn EventSink>) -> Self {
123 self.events.register(sink);
124 self
125 }
126
127 #[must_use]
131 pub fn with_auto_discovered(mut self) -> Self {
132 for reg in inventory::iter::<DialectRegistration> {
133 let dialect = (reg.factory)();
134 let name = dialect.info().name.to_string();
135 self.dialects.register(name, dialect);
136 self.record_plugin_loaded(reg.name, "dialect", "inventory");
137 }
138 for reg in inventory::iter::<StorageRegistration> {
139 let factory = (reg.factory)();
140 if let Some(platform) = factory.known_platform() {
141 self.storage.register_known(platform, factory);
142 } else {
143 self.storage.register_extension(reg.name.to_string(), factory);
144 }
145 self.record_plugin_loaded(reg.name, "storage", "inventory");
146 }
147 for reg in inventory::iter::<LlmRegistration> {
148 let factory = (reg.factory)();
149 self.llm.register(reg.name.to_string(), factory);
150 self.record_plugin_loaded(reg.name, "llm", "inventory");
151 }
152 for reg in inventory::iter::<ModelRegistration> {
153 let adapter = (reg.factory)();
154 self.models.register(adapter);
155 self.record_plugin_loaded(reg.model_id, "model", "inventory");
156 }
157 for reg in inventory::iter::<LanguageRegistration> {
158 let factory = (reg.factory)();
159 self.language.register(reg.language.to_string(), factory);
160 self.record_plugin_loaded(reg.language, "language", "inventory");
161 }
162 for reg in inventory::iter::<EventSinkRegistration> {
163 let sink = (reg.factory)();
164 self.events.register(sink);
165 self.record_plugin_loaded(reg.name, "event_sink", "inventory");
166 }
167 self
168 }
169
170 fn record_plugin_loaded(&self, name: &str, plugin_type: &str, source: &str) {
172 if let Ok(event) = RazeriEvent::plugin_loaded(
173 format!("plugin_loaded:{plugin_type}:{name}"),
174 Utc::now(),
175 name,
176 plugin_type,
177 source,
178 ) {
179 self.events.record(event);
180 }
181 }
182
183 #[cfg(all(feature = "dynamic", not(target_arch = "wasm32")))]
194 #[must_use]
195 pub fn with_dynamic_plugins(mut self, plugin_dir: &std::path::Path) -> Self {
196 let results = crate::dynamic::load_plugins_from_dir(plugin_dir, &mut self);
197 for result in &results {
198 match result {
199 crate::dynamic::PluginLoadResult::Loaded { name, path } => {
200 tracing::info!("Loaded dynamic plugin '{name}' from {}", path.display());
201 self.record_plugin_loaded(name, "dynamic", &path.display().to_string());
202 }
203 crate::dynamic::PluginLoadResult::VersionMismatch { path, expected, found } => {
204 tracing::warn!(
205 "Plugin {} API version mismatch (expected {expected}, found {found})",
206 path.display()
207 );
208 }
209 crate::dynamic::PluginLoadResult::Failed { path, error } => {
210 tracing::warn!("Failed to load plugin from {}: {error}", path.display());
211 }
212 }
213 }
214 self
215 }
216
217 pub fn build(self) -> PluginRegistry {
221 PluginRegistry {
222 dialects: self.dialects,
223 storage: self.storage,
224 llm: self.llm,
225 models: self.models,
226 language: self.language,
227 events: self.events,
228 }
229 }
230}
231
232impl Default for PluginRegistryBuilder {
233 fn default() -> Self {
237 Self::new()
238 }
239}
240
241pub struct DialectRegistration {
247 pub name: &'static str,
249 pub factory: fn() -> Arc<dyn PlatformDialect>,
251}
252
253impl DialectRegistration {
254 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn PlatformDialect>) -> Self {
256 Self { name, factory }
257 }
258}
259
260inventory::collect!(DialectRegistration);
261
262pub struct StorageRegistration {
264 pub name: &'static str,
266 pub factory: fn() -> Arc<dyn StorageProviderFactory>,
268}
269
270impl StorageRegistration {
271 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn StorageProviderFactory>) -> Self {
273 Self { name, factory }
274 }
275}
276
277inventory::collect!(StorageRegistration);
278
279pub struct LlmRegistration {
281 pub name: &'static str,
283 pub factory: fn() -> Arc<dyn LlmClientFactory>,
285}
286
287impl LlmRegistration {
288 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn LlmClientFactory>) -> Self {
290 Self { name, factory }
291 }
292}
293
294inventory::collect!(LlmRegistration);
295
296pub struct ModelRegistration {
298 pub model_id: &'static str,
300 pub factory: fn() -> Arc<dyn LlmModelAdapter>,
302}
303
304impl ModelRegistration {
305 pub const fn new(model_id: &'static str, factory: fn() -> Arc<dyn LlmModelAdapter>) -> Self {
307 Self { model_id, factory }
308 }
309}
310
311inventory::collect!(ModelRegistration);
312
313pub struct LanguageRegistration {
315 pub language: &'static str,
317 pub factory: fn() -> Arc<dyn LanguageRendererFactory>,
319}
320
321impl LanguageRegistration {
322 pub const fn new(
324 language: &'static str,
325 factory: fn() -> Arc<dyn LanguageRendererFactory>,
326 ) -> Self {
327 Self { language, factory }
328 }
329}
330
331inventory::collect!(LanguageRegistration);
332
333pub struct EventSinkRegistration {
335 pub name: &'static str,
337 pub factory: fn() -> Arc<dyn EventSink>,
339}
340
341impl EventSinkRegistration {
342 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn EventSink>) -> Self {
344 Self { name, factory }
345 }
346}
347
348inventory::collect!(EventSinkRegistration);
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::dialect::{DialectInfo, PlatformDialect};
354 use crate::error::{EventSinkError, LanguageError, LlmError};
355 use crate::event::RazeriEvent;
356 use crate::llm::{ChatResponse, LlmClient, Message, ModelInfo, ToolDefinition};
357 use crate::model::ModelCapabilities;
358 use async_trait::async_trait;
359 use razeri_plugin_contract::DataType;
360 use razeri_plugin_contract::DialectFamily;
361 use razeri_plugin_contract::{
362 DdlRenderer, DialectCapabilities, DmlRenderer, ExpressionRenderer, FormulaRenderer,
363 ProcedureRenderer, ReplicationRenderer, SqlRenderer, StorageRenderer, TaskRenderer,
364 };
365 use std::collections::HashMap;
366
367 struct TestDialect;
370
371 impl SqlRenderer for TestDialect {
372 fn render_data_type(&self, _dt: &DataType) -> String {
377 "TEXT".to_string()
378 }
379 fn quote_identifier(&self, name: &str) -> String {
384 format!("\"{name}\"")
385 }
386 fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
392 name.to_string()
393 }
394 fn render_current_timestamp(&self) -> String {
398 "NOW()".to_string()
399 }
400 }
401
402 impl ExpressionRenderer for TestDialect {}
403 impl DdlRenderer for TestDialect {}
404 impl DmlRenderer for TestDialect {}
405 impl ProcedureRenderer for TestDialect {}
406 impl FormulaRenderer for TestDialect {}
407 impl TaskRenderer for TestDialect {}
408 impl ReplicationRenderer for TestDialect {}
409 impl StorageRenderer for TestDialect {}
410
411 impl DialectCapabilities for TestDialect {
412 fn merge_shape(&self) -> razeri_plugin_contract::MergeShape {
416 razeri_plugin_contract::MergeShape::Merge(
417 razeri_plugin_contract::MergeRestrictions::none(),
418 )
419 }
420 fn supports_identity(&self) -> bool {
424 true
425 }
426 fn supports_sequences(&self) -> bool {
430 true
431 }
432 fn supports_stored_procedures(&self) -> bool {
436 true
437 }
438 }
439
440 impl PlatformDialect for TestDialect {
441 fn info(&self) -> DialectInfo {
445 DialectInfo {
446 name: "test",
447 display_name: "Test DB",
448 aliases: &[],
449 dialect_family: DialectFamily::PostgreSql,
450 family: "PostgreSQL",
451 is_cloud_native: false,
452 is_olap: false,
453 is_postgres_compatible: false,
454 }
455 }
456 }
457
458 struct TestStorageFactory;
459 #[async_trait]
460 impl StorageProviderFactory for TestStorageFactory {
461 fn name(&self) -> &'static str {
465 "test-storage"
466 }
467 fn display_name(&self) -> &'static str {
471 "Test Storage"
472 }
473 fn known_platform(&self) -> Option<StoragePlatform> {
477 None
478 }
479 async fn create(
480 &self,
481 _config: &HashMap<String, String>,
482 ) -> Result<Box<dyn crate::storage::StorageOps>, crate::error::StorageError> {
483 unimplemented!("test only")
484 }
485 fn validate_config(&self, _config: &HashMap<String, String>) -> Vec<String> {
490 vec![]
491 }
492 fn required_config_keys(&self) -> Vec<&str> {
496 vec![]
497 }
498 }
499
500 struct TestLlmFactory;
501 impl LlmClientFactory for TestLlmFactory {
502 fn name(&self) -> &'static str {
506 "test-llm"
507 }
508 fn display_name(&self) -> &'static str {
512 "Test LLM"
513 }
514 fn create(
519 &self,
520 _config: &HashMap<String, String>,
521 ) -> Result<Arc<dyn LlmClient>, LlmError> {
522 Ok(Arc::new(TestLlmClient))
523 }
524 fn validate_config(&self, _config: &HashMap<String, String>) -> Vec<String> {
529 vec![]
530 }
531 fn required_config_keys(&self) -> Vec<&str> {
535 vec![]
536 }
537 }
538
539 struct TestLlmClient;
540 #[async_trait]
541 impl LlmClient for TestLlmClient {
542 async fn chat(
543 &self,
544 _messages: &[Message],
545 _model: Option<&str>,
546 _tools: Option<Vec<ToolDefinition>>,
547 ) -> Result<ChatResponse, LlmError> {
548 Ok(ChatResponse { content: "ok".into(), model: "test".into(), usage: None })
549 }
550 async fn chat_json(
551 &self,
552 _messages: &[Message],
553 _model: Option<&str>,
554 _tools: Option<Vec<ToolDefinition>>,
555 ) -> Result<serde_json::Value, LlmError> {
556 Ok(serde_json::json!({}))
557 }
558 async fn list_models(&self) -> Result<Vec<ModelInfo>, LlmError> {
559 Ok(vec![])
560 }
561 async fn health_check(&self) -> Result<bool, LlmError> {
562 Ok(true)
563 }
564 }
565
566 struct TestModelAdapter;
567 impl LlmModelAdapter for TestModelAdapter {
568 fn model_id(&self) -> &'static str {
572 "test-model"
573 }
574 fn provider_name(&self) -> &'static str {
578 "test-llm"
579 }
580 fn display_name(&self) -> &'static str {
584 "Test Model"
585 }
586 fn capabilities(&self) -> ModelCapabilities {
590 ModelCapabilities {
591 max_context_tokens: 4096,
592 supports_json_mode: true,
593 supports_tools: false,
594 supports_streaming: false,
595 }
596 }
597 }
598
599 struct TestLanguageFactory;
600 impl LanguageRendererFactory for TestLanguageFactory {
601 fn language(&self) -> &'static str {
605 "test-lang"
606 }
607 fn display_name(&self) -> &'static str {
611 "Test Language"
612 }
613 fn create(
618 &self,
619 _config: &HashMap<String, String>,
620 ) -> Result<Arc<dyn crate::language::LanguageRenderer>, LanguageError> {
621 unimplemented!("test only")
622 }
623 }
624
625 struct TestEventSink;
626 #[async_trait]
627 impl crate::event::EventSink for TestEventSink {
628 fn name(&self) -> &'static str {
632 "test-sink"
633 }
634 async fn on_event(&self, _event: &RazeriEvent) -> Result<(), EventSinkError> {
635 Ok(())
636 }
637 async fn flush(&self) -> Result<(), EventSinkError> {
638 Ok(())
639 }
640 }
641
642 #[test]
648 fn builder_creates_registry_with_all_sub_registries() {
649 let registry = PluginRegistryBuilder::new()
650 .with_dialect("postgres", Arc::new(TestDialect))
651 .with_storage_extension("test".into(), Arc::new(TestStorageFactory))
652 .with_llm("test-llm".into(), Arc::new(TestLlmFactory))
653 .with_model(Arc::new(TestModelAdapter))
654 .with_language("test-lang".into(), Arc::new(TestLanguageFactory))
655 .with_event_sink(Arc::new(TestEventSink))
656 .build();
657
658 assert!(registry.dialects.get_by_name("postgres").is_some());
659 assert!(registry.llm.get("test-llm").is_some());
660 assert!(registry.models.get("test-llm", "test-model").is_some());
661 assert!(registry.language.get("test-lang").is_some());
662 assert!(!registry.events.is_empty());
663 }
664
665 #[test]
669 fn builder_chaining_is_ergonomic() {
670 let _registry = PluginRegistryBuilder::new()
672 .with_dialect("postgres", Arc::new(TestDialect))
673 .with_dialect("snowflake", Arc::new(TestDialect))
674 .with_llm("a".into(), Arc::new(TestLlmFactory))
675 .with_llm("b".into(), Arc::new(TestLlmFactory))
676 .with_event_sink(Arc::new(TestEventSink))
677 .with_event_sink(Arc::new(TestEventSink))
678 .build();
679 }
680
681 #[test]
685 fn builder_with_mixed_known_and_extension() {
686 let registry = PluginRegistryBuilder::new()
687 .with_dialect("postgres", Arc::new(TestDialect))
688 .with_dialect("teradata", Arc::new(TestDialect))
689 .with_storage(StoragePlatform::S3, Arc::new(TestStorageFactory))
690 .with_storage_extension("minio".into(), Arc::new(TestStorageFactory))
691 .build();
692
693 assert!(registry.dialects.get_by_name("postgres").is_some());
694 assert!(registry.dialects.get_by_name("teradata").is_some());
695 }
696
697 #[test]
702 fn builder_with_auto_discovered_finds_inventory_entries() {
703 let registry = PluginRegistryBuilder::new().with_auto_discovered().build();
706 assert_eq!(registry.dialects.registered_names(), vec!["outsidedb".to_owned()]);
707 }
708
709 #[test]
713 fn manual_registration_records_plugin_loaded_events() {
714 let builder = PluginRegistryBuilder::new()
715 .with_dialect("postgres", Arc::new(TestDialect))
716 .with_llm("test-llm".into(), Arc::new(TestLlmFactory));
717
718 builder.record_plugin_loaded("TestDialect", "dialect", "manual");
722 builder.record_plugin_loaded("TestLlm", "llm", "manual");
723
724 let registry = builder.build();
725 let events = registry.events.events();
726 assert_eq!(events.len(), 2);
727
728 assert_eq!(events[0].category(), crate::event::EventCategory::Plugin);
729 assert_eq!(events[0].summary().title, "Plugin loaded");
730 assert!(events[0].summary().detail.contains("dialect"));
731
732 assert_eq!(events[1].category(), crate::event::EventCategory::Plugin);
733 assert!(events[1].summary().detail.contains("llm"));
734 }
735
736 #[test]
740 fn auto_discovery_registers_built_in_sinks() {
741 let registry = PluginRegistryBuilder::new().with_auto_discovered().build();
742 assert!(
745 registry.events.len() >= 3,
746 "Expected at least 3 built-in sinks, found {}",
747 registry.events.len()
748 );
749
750 let events = registry.events.events();
752 let sink_events: Vec<_> =
753 events.iter().filter(|e| e.summary().detail.contains("event_sink")).collect();
754 assert!(
755 sink_events.len() >= 3,
756 "Expected at least 3 event_sink load events, found {}",
757 sink_events.len()
758 );
759 }
760}