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 dialect_family: DialectFamily::PostgreSql,
449 family: "PostgreSQL",
450 is_cloud_native: false,
451 is_olap: false,
452 is_postgres_compatible: false,
453 }
454 }
455 }
456
457 struct TestStorageFactory;
458 #[async_trait]
459 impl StorageProviderFactory for TestStorageFactory {
460 fn name(&self) -> &'static str {
464 "test-storage"
465 }
466 fn display_name(&self) -> &'static str {
470 "Test Storage"
471 }
472 fn known_platform(&self) -> Option<StoragePlatform> {
476 None
477 }
478 async fn create(
479 &self,
480 _config: &HashMap<String, String>,
481 ) -> Result<Box<dyn crate::storage::StorageOps>, crate::error::StorageError> {
482 unimplemented!("test only")
483 }
484 fn validate_config(&self, _config: &HashMap<String, String>) -> Vec<String> {
489 vec![]
490 }
491 fn required_config_keys(&self) -> Vec<&str> {
495 vec![]
496 }
497 }
498
499 struct TestLlmFactory;
500 impl LlmClientFactory for TestLlmFactory {
501 fn name(&self) -> &'static str {
505 "test-llm"
506 }
507 fn display_name(&self) -> &'static str {
511 "Test LLM"
512 }
513 fn create(
518 &self,
519 _config: &HashMap<String, String>,
520 ) -> Result<Arc<dyn LlmClient>, LlmError> {
521 Ok(Arc::new(TestLlmClient))
522 }
523 fn validate_config(&self, _config: &HashMap<String, String>) -> Vec<String> {
528 vec![]
529 }
530 fn required_config_keys(&self) -> Vec<&str> {
534 vec![]
535 }
536 }
537
538 struct TestLlmClient;
539 #[async_trait]
540 impl LlmClient for TestLlmClient {
541 async fn chat(
542 &self,
543 _messages: &[Message],
544 _model: Option<&str>,
545 _tools: Option<Vec<ToolDefinition>>,
546 ) -> Result<ChatResponse, LlmError> {
547 Ok(ChatResponse { content: "ok".into(), model: "test".into(), usage: None })
548 }
549 async fn chat_json(
550 &self,
551 _messages: &[Message],
552 _model: Option<&str>,
553 _tools: Option<Vec<ToolDefinition>>,
554 ) -> Result<serde_json::Value, LlmError> {
555 Ok(serde_json::json!({}))
556 }
557 async fn list_models(&self) -> Result<Vec<ModelInfo>, LlmError> {
558 Ok(vec![])
559 }
560 async fn health_check(&self) -> Result<bool, LlmError> {
561 Ok(true)
562 }
563 }
564
565 struct TestModelAdapter;
566 impl LlmModelAdapter for TestModelAdapter {
567 fn model_id(&self) -> &'static str {
571 "test-model"
572 }
573 fn provider_name(&self) -> &'static str {
577 "test-llm"
578 }
579 fn display_name(&self) -> &'static str {
583 "Test Model"
584 }
585 fn capabilities(&self) -> ModelCapabilities {
589 ModelCapabilities {
590 max_context_tokens: 4096,
591 supports_json_mode: true,
592 supports_tools: false,
593 supports_streaming: false,
594 }
595 }
596 }
597
598 struct TestLanguageFactory;
599 impl LanguageRendererFactory for TestLanguageFactory {
600 fn language(&self) -> &'static str {
604 "test-lang"
605 }
606 fn display_name(&self) -> &'static str {
610 "Test Language"
611 }
612 fn create(
617 &self,
618 _config: &HashMap<String, String>,
619 ) -> Result<Arc<dyn crate::language::LanguageRenderer>, LanguageError> {
620 unimplemented!("test only")
621 }
622 }
623
624 struct TestEventSink;
625 #[async_trait]
626 impl crate::event::EventSink for TestEventSink {
627 fn name(&self) -> &'static str {
631 "test-sink"
632 }
633 async fn on_event(&self, _event: &RazeriEvent) -> Result<(), EventSinkError> {
634 Ok(())
635 }
636 async fn flush(&self) -> Result<(), EventSinkError> {
637 Ok(())
638 }
639 }
640
641 #[test]
647 fn builder_creates_registry_with_all_sub_registries() {
648 let registry = PluginRegistryBuilder::new()
649 .with_dialect("postgres", Arc::new(TestDialect))
650 .with_storage_extension("test".into(), Arc::new(TestStorageFactory))
651 .with_llm("test-llm".into(), Arc::new(TestLlmFactory))
652 .with_model(Arc::new(TestModelAdapter))
653 .with_language("test-lang".into(), Arc::new(TestLanguageFactory))
654 .with_event_sink(Arc::new(TestEventSink))
655 .build();
656
657 assert!(registry.dialects.get_by_name("postgres").is_some());
658 assert!(registry.llm.get("test-llm").is_some());
659 assert!(registry.models.get("test-llm", "test-model").is_some());
660 assert!(registry.language.get("test-lang").is_some());
661 assert!(!registry.events.is_empty());
662 }
663
664 #[test]
668 fn builder_chaining_is_ergonomic() {
669 let _registry = PluginRegistryBuilder::new()
671 .with_dialect("postgres", Arc::new(TestDialect))
672 .with_dialect("snowflake", Arc::new(TestDialect))
673 .with_llm("a".into(), Arc::new(TestLlmFactory))
674 .with_llm("b".into(), Arc::new(TestLlmFactory))
675 .with_event_sink(Arc::new(TestEventSink))
676 .with_event_sink(Arc::new(TestEventSink))
677 .build();
678 }
679
680 #[test]
684 fn builder_with_mixed_known_and_extension() {
685 let registry = PluginRegistryBuilder::new()
686 .with_dialect("postgres", Arc::new(TestDialect))
687 .with_dialect("teradata", Arc::new(TestDialect))
688 .with_storage(StoragePlatform::S3, Arc::new(TestStorageFactory))
689 .with_storage_extension("minio".into(), Arc::new(TestStorageFactory))
690 .build();
691
692 assert!(registry.dialects.get_by_name("postgres").is_some());
693 assert!(registry.dialects.get_by_name("teradata").is_some());
694 }
695
696 #[test]
700 fn builder_with_auto_discovered_finds_inventory_entries() {
701 let registry = PluginRegistryBuilder::new().with_auto_discovered().build();
704 assert_eq!(registry.dialects.list().len(), 0);
706 }
707
708 #[test]
712 fn manual_registration_records_plugin_loaded_events() {
713 let builder = PluginRegistryBuilder::new()
714 .with_dialect("postgres", Arc::new(TestDialect))
715 .with_llm("test-llm".into(), Arc::new(TestLlmFactory));
716
717 builder.record_plugin_loaded("TestDialect", "dialect", "manual");
721 builder.record_plugin_loaded("TestLlm", "llm", "manual");
722
723 let registry = builder.build();
724 let events = registry.events.events();
725 assert_eq!(events.len(), 2);
726
727 assert_eq!(events[0].category(), crate::event::EventCategory::Plugin);
728 assert_eq!(events[0].summary().title, "Plugin loaded");
729 assert!(events[0].summary().detail.contains("dialect"));
730
731 assert_eq!(events[1].category(), crate::event::EventCategory::Plugin);
732 assert!(events[1].summary().detail.contains("llm"));
733 }
734
735 #[test]
739 fn auto_discovery_registers_built_in_sinks() {
740 let registry = PluginRegistryBuilder::new().with_auto_discovered().build();
741 assert!(
744 registry.events.len() >= 3,
745 "Expected at least 3 built-in sinks, found {}",
746 registry.events.len()
747 );
748
749 let events = registry.events.events();
751 let sink_events: Vec<_> =
752 events.iter().filter(|e| e.summary().detail.contains("event_sink")).collect();
753 assert!(
754 sink_events.len() >= 3,
755 "Expected at least 3 event_sink load events, found {}",
756 sink_events.len()
757 );
758 }
759}