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 for reg in inventory::iter::<PluginRegistration> {
179 (reg.register)(&mut self);
180 }
181 self
182 }
183
184 fn record_plugin_loaded(&self, name: &str, plugin_type: &str, source: &str) {
186 if let Ok(event) = RazeriEvent::plugin_loaded(
187 format!("plugin_loaded:{plugin_type}:{name}"),
188 Utc::now(),
189 name,
190 plugin_type,
191 source,
192 ) {
193 self.events.record(event);
194 }
195 }
196
197 #[cfg(all(feature = "dynamic", not(target_arch = "wasm32")))]
208 #[must_use]
209 pub fn with_dynamic_plugins(mut self, plugin_dir: &std::path::Path) -> Self {
210 let results = crate::dynamic::load_plugins_from_dir(plugin_dir, &mut self);
211 for result in &results {
212 match result {
213 crate::dynamic::PluginLoadResult::Loaded { name, path } => {
214 tracing::info!("Loaded dynamic plugin '{name}' from {}", path.display());
215 self.record_plugin_loaded(name, "dynamic", &path.display().to_string());
216 }
217 crate::dynamic::PluginLoadResult::Refused { path, refusal } => {
218 tracing::warn!("Plugin {} was not loaded: {refusal}", path.display());
219 }
220 crate::dynamic::PluginLoadResult::Failed { path, error } => {
221 tracing::warn!("Failed to load plugin from {}: {error}", path.display());
222 }
223 }
224 }
225 self
226 }
227
228 pub fn build(self) -> PluginRegistry {
232 PluginRegistry {
233 dialects: self.dialects,
234 storage: self.storage,
235 llm: self.llm,
236 models: self.models,
237 language: self.language,
238 events: self.events,
239 }
240 }
241}
242
243impl Default for PluginRegistryBuilder {
244 fn default() -> Self {
248 Self::new()
249 }
250}
251
252pub struct DialectRegistration {
258 pub name: &'static str,
260 pub factory: fn() -> Arc<dyn PlatformDialect>,
262}
263
264impl DialectRegistration {
265 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn PlatformDialect>) -> Self {
267 Self { name, factory }
268 }
269}
270
271inventory::collect!(DialectRegistration);
272
273pub struct PluginRegistration {
279 pub register: fn(&mut PluginRegistryBuilder),
281}
282
283impl PluginRegistration {
284 pub const fn new(register: fn(&mut PluginRegistryBuilder)) -> Self {
286 Self { register }
287 }
288}
289
290inventory::collect!(PluginRegistration);
291
292pub struct StorageRegistration {
294 pub name: &'static str,
296 pub factory: fn() -> Arc<dyn StorageProviderFactory>,
298}
299
300impl StorageRegistration {
301 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn StorageProviderFactory>) -> Self {
303 Self { name, factory }
304 }
305}
306
307inventory::collect!(StorageRegistration);
308
309pub struct LlmRegistration {
311 pub name: &'static str,
313 pub factory: fn() -> Arc<dyn LlmClientFactory>,
315}
316
317impl LlmRegistration {
318 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn LlmClientFactory>) -> Self {
320 Self { name, factory }
321 }
322}
323
324inventory::collect!(LlmRegistration);
325
326pub struct ModelRegistration {
328 pub model_id: &'static str,
330 pub factory: fn() -> Arc<dyn LlmModelAdapter>,
332}
333
334impl ModelRegistration {
335 pub const fn new(model_id: &'static str, factory: fn() -> Arc<dyn LlmModelAdapter>) -> Self {
337 Self { model_id, factory }
338 }
339}
340
341inventory::collect!(ModelRegistration);
342
343pub struct LanguageRegistration {
345 pub language: &'static str,
347 pub factory: fn() -> Arc<dyn LanguageRendererFactory>,
349}
350
351impl LanguageRegistration {
352 pub const fn new(
354 language: &'static str,
355 factory: fn() -> Arc<dyn LanguageRendererFactory>,
356 ) -> Self {
357 Self { language, factory }
358 }
359}
360
361inventory::collect!(LanguageRegistration);
362
363pub struct EventSinkRegistration {
365 pub name: &'static str,
367 pub factory: fn() -> Arc<dyn EventSink>,
369}
370
371impl EventSinkRegistration {
372 pub const fn new(name: &'static str, factory: fn() -> Arc<dyn EventSink>) -> Self {
374 Self { name, factory }
375 }
376}
377
378inventory::collect!(EventSinkRegistration);
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use crate::dialect::{DialectInfo, PlatformDialect};
384 use crate::error::{EventSinkError, LanguageError, LlmError};
385 use crate::event::RazeriEvent;
386 use crate::llm::{ChatResponse, LlmClient, Message, ModelInfo, ToolDefinition};
387 use crate::model::ModelCapabilities;
388 use async_trait::async_trait;
389 use razeri_plugin_contract::DataType;
390 use razeri_plugin_contract::DialectFamily;
391 use razeri_plugin_contract::{
392 DdlRenderer, DialectCapabilities, DmlRenderer, ExpressionRenderer, FormulaRenderer,
393 ProcedureRenderer, ReplicationRenderer, SqlRenderer, StorageRenderer, TaskRenderer,
394 };
395 use std::collections::HashMap;
396
397 struct TestDialect;
400
401 impl SqlRenderer for TestDialect {
402 fn render_data_type(&self, _dt: &DataType) -> String {
407 "TEXT".to_string()
408 }
409 fn quote_identifier(&self, name: &str) -> String {
414 format!("\"{name}\"")
415 }
416 fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
422 name.to_string()
423 }
424 fn render_current_timestamp(&self) -> String {
428 "NOW()".to_string()
429 }
430 }
431
432 impl ExpressionRenderer for TestDialect {}
433 impl DdlRenderer for TestDialect {}
434 impl DmlRenderer for TestDialect {}
435 impl ProcedureRenderer for TestDialect {}
436 impl FormulaRenderer for TestDialect {}
437 impl TaskRenderer for TestDialect {}
438 impl ReplicationRenderer for TestDialect {}
439 impl StorageRenderer for TestDialect {}
440
441 impl DialectCapabilities for TestDialect {
442 fn merge_shape(&self) -> razeri_plugin_contract::MergeShape {
446 razeri_plugin_contract::MergeShape::Merge(
447 razeri_plugin_contract::MergeRestrictions::none(),
448 )
449 }
450 fn supports_identity(&self) -> bool {
454 true
455 }
456 fn supports_sequences(&self) -> bool {
460 true
461 }
462 fn supports_stored_procedures(&self) -> bool {
466 true
467 }
468 }
469
470 impl PlatformDialect for TestDialect {
471 fn info(&self) -> DialectInfo {
475 DialectInfo {
476 name: "test",
477 display_name: "Test DB",
478 aliases: &[],
479 dialect_family: DialectFamily::PostgreSql,
480 family: "PostgreSQL",
481 is_cloud_native: false,
482 is_olap: false,
483 is_postgres_compatible: false,
484 }
485 }
486 }
487
488 struct TestStorageFactory;
489 #[async_trait]
490 impl StorageProviderFactory for TestStorageFactory {
491 fn name(&self) -> &'static str {
495 "test-storage"
496 }
497 fn display_name(&self) -> &'static str {
501 "Test Storage"
502 }
503 fn known_platform(&self) -> Option<StoragePlatform> {
507 None
508 }
509 async fn create(
510 &self,
511 _config: &HashMap<String, String>,
512 ) -> Result<Box<dyn crate::storage::StorageOps>, crate::error::StorageError> {
513 unimplemented!("test only")
514 }
515 fn validate_config(&self, _config: &HashMap<String, String>) -> Vec<String> {
520 vec![]
521 }
522 fn required_config_keys(&self) -> Vec<&str> {
526 vec![]
527 }
528 }
529
530 struct TestLlmFactory;
531 impl LlmClientFactory for TestLlmFactory {
532 fn name(&self) -> &'static str {
536 "test-llm"
537 }
538 fn display_name(&self) -> &'static str {
542 "Test LLM"
543 }
544 fn create(
549 &self,
550 _config: &HashMap<String, String>,
551 ) -> Result<Arc<dyn LlmClient>, LlmError> {
552 Ok(Arc::new(TestLlmClient))
553 }
554 fn validate_config(&self, _config: &HashMap<String, String>) -> Vec<String> {
559 vec![]
560 }
561 fn required_config_keys(&self) -> Vec<&str> {
565 vec![]
566 }
567 }
568
569 struct TestLlmClient;
570 #[async_trait]
571 impl LlmClient for TestLlmClient {
572 async fn chat(
573 &self,
574 _messages: &[Message],
575 _model: Option<&str>,
576 _tools: Option<Vec<ToolDefinition>>,
577 ) -> Result<ChatResponse, LlmError> {
578 Ok(ChatResponse { content: "ok".into(), model: "test".into(), usage: None })
579 }
580 async fn chat_json(
581 &self,
582 _messages: &[Message],
583 _model: Option<&str>,
584 _tools: Option<Vec<ToolDefinition>>,
585 ) -> Result<serde_json::Value, LlmError> {
586 Ok(serde_json::json!({}))
587 }
588 async fn list_models(&self) -> Result<Vec<ModelInfo>, LlmError> {
589 Ok(vec![])
590 }
591 async fn health_check(&self) -> Result<bool, LlmError> {
592 Ok(true)
593 }
594 }
595
596 struct TestModelAdapter;
597 impl LlmModelAdapter for TestModelAdapter {
598 fn model_id(&self) -> &'static str {
602 "test-model"
603 }
604 fn provider_name(&self) -> &'static str {
608 "test-llm"
609 }
610 fn display_name(&self) -> &'static str {
614 "Test Model"
615 }
616 fn capabilities(&self) -> ModelCapabilities {
620 ModelCapabilities {
621 max_context_tokens: 4096,
622 supports_json_mode: true,
623 supports_tools: false,
624 supports_streaming: false,
625 }
626 }
627 }
628
629 struct TestLanguageFactory;
630 impl LanguageRendererFactory for TestLanguageFactory {
631 fn language(&self) -> &'static str {
635 "test-lang"
636 }
637 fn display_name(&self) -> &'static str {
641 "Test Language"
642 }
643 fn create(
648 &self,
649 _config: &HashMap<String, String>,
650 ) -> Result<Arc<dyn crate::language::LanguageRenderer>, LanguageError> {
651 unimplemented!("test only")
652 }
653 }
654
655 struct TestEventSink;
656 #[async_trait]
657 impl crate::event::EventSink for TestEventSink {
658 fn name(&self) -> &'static str {
662 "test-sink"
663 }
664 async fn on_event(&self, _event: &RazeriEvent) -> Result<(), EventSinkError> {
665 Ok(())
666 }
667 async fn flush(&self) -> Result<(), EventSinkError> {
668 Ok(())
669 }
670 }
671
672 #[test]
678 fn builder_creates_registry_with_all_sub_registries() {
679 let registry = PluginRegistryBuilder::new()
680 .with_dialect("postgres", Arc::new(TestDialect))
681 .with_storage_extension("test".into(), Arc::new(TestStorageFactory))
682 .with_llm("test-llm".into(), Arc::new(TestLlmFactory))
683 .with_model(Arc::new(TestModelAdapter))
684 .with_language("test-lang".into(), Arc::new(TestLanguageFactory))
685 .with_event_sink(Arc::new(TestEventSink))
686 .build();
687
688 assert!(registry.dialects.get_by_name("postgres").is_some());
689 assert!(registry.llm.get("test-llm").is_some());
690 assert!(registry.models.get("test-llm", "test-model").is_some());
691 assert!(registry.language.get("test-lang").is_some());
692 assert!(!registry.events.is_empty());
693 }
694
695 #[test]
699 fn builder_chaining_is_ergonomic() {
700 let _registry = PluginRegistryBuilder::new()
702 .with_dialect("postgres", Arc::new(TestDialect))
703 .with_dialect("snowflake", Arc::new(TestDialect))
704 .with_llm("a".into(), Arc::new(TestLlmFactory))
705 .with_llm("b".into(), Arc::new(TestLlmFactory))
706 .with_event_sink(Arc::new(TestEventSink))
707 .with_event_sink(Arc::new(TestEventSink))
708 .build();
709 }
710
711 #[test]
715 fn builder_with_mixed_known_and_extension() {
716 let registry = PluginRegistryBuilder::new()
717 .with_dialect("postgres", Arc::new(TestDialect))
718 .with_dialect("teradata", Arc::new(TestDialect))
719 .with_storage(StoragePlatform::S3, Arc::new(TestStorageFactory))
720 .with_storage_extension("minio".into(), Arc::new(TestStorageFactory))
721 .build();
722
723 assert!(registry.dialects.get_by_name("postgres").is_some());
724 assert!(registry.dialects.get_by_name("teradata").is_some());
725 }
726
727 #[test]
732 fn builder_with_auto_discovered_finds_inventory_entries() {
733 let registry = PluginRegistryBuilder::new().with_auto_discovered().build();
737 let mut names = registry.dialects.registered_names();
738 names.sort();
739 assert_eq!(names, vec!["linked_by_export".to_owned(), "outsidedb".to_owned()]);
740 }
741
742 #[test]
746 fn manual_registration_records_plugin_loaded_events() {
747 let builder = PluginRegistryBuilder::new()
748 .with_dialect("postgres", Arc::new(TestDialect))
749 .with_llm("test-llm".into(), Arc::new(TestLlmFactory));
750
751 builder.record_plugin_loaded("TestDialect", "dialect", "manual");
755 builder.record_plugin_loaded("TestLlm", "llm", "manual");
756
757 let registry = builder.build();
758 let events = registry.events.events();
759 assert_eq!(events.len(), 2);
760
761 assert_eq!(events[0].category(), crate::event::EventCategory::Plugin);
762 assert_eq!(events[0].summary().title, "Plugin loaded");
763 assert!(events[0].summary().detail.contains("dialect"));
764
765 assert_eq!(events[1].category(), crate::event::EventCategory::Plugin);
766 assert!(events[1].summary().detail.contains("llm"));
767 }
768
769 #[test]
773 fn auto_discovery_registers_built_in_sinks() {
774 let registry = PluginRegistryBuilder::new().with_auto_discovered().build();
775 assert!(
778 registry.events.len() >= 3,
779 "Expected at least 3 built-in sinks, found {}",
780 registry.events.len()
781 );
782
783 let events = registry.events.events();
785 let sink_events: Vec<_> =
786 events.iter().filter(|e| e.summary().detail.contains("event_sink")).collect();
787 assert!(
788 sink_events.len() >= 3,
789 "Expected at least 3 event_sink load events, found {}",
790 sink_events.len()
791 );
792 }
793
794 struct Linked;
796
797 impl SqlRenderer for Linked {
798 fn render_data_type(&self, _dt: &DataType) -> String {
799 "TEXT".to_string()
800 }
801 fn quote_identifier(&self, name: &str) -> String {
802 format!("\"{name}\"")
803 }
804 fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
805 name.to_string()
806 }
807 fn render_current_timestamp(&self) -> String {
808 "NOW()".to_string()
809 }
810 }
811 impl ExpressionRenderer for Linked {}
812 impl DdlRenderer for Linked {}
813 impl DmlRenderer for Linked {}
814 impl ProcedureRenderer for Linked {}
815 impl FormulaRenderer for Linked {}
816 impl TaskRenderer for Linked {}
817 impl ReplicationRenderer for Linked {}
818 impl StorageRenderer for Linked {}
819 impl DialectCapabilities for Linked {}
820 impl PlatformDialect for Linked {
821 fn info(&self) -> DialectInfo {
822 DialectInfo {
823 name: "linked_by_export",
824 display_name: "Linked by export",
825 aliases: &[],
826 dialect_family: DialectFamily::PostgreSql,
827 family: "PostgreSQL",
828 is_cloud_native: false,
829 is_olap: false,
830 is_postgres_compatible: false,
831 }
832 }
833 }
834
835 fn register_linked(builder: &mut PluginRegistryBuilder) {
836 builder.register_dialect(Arc::new(Linked));
837 }
838
839 crate::export_plugin!(register_linked);
840
841 #[test]
845 fn a_plugin_exported_with_the_macro_and_linked_in_is_discovered() {
846 let registry = PluginRegistryBuilder::new().with_auto_discovered().build();
847 assert!(
848 registry.dialects.get_by_name("linked_by_export").is_some(),
849 "the builder did not find the linked plugin: {:?}",
850 registry.dialects.registered_names()
851 );
852 assert!(
853 PlatformRegistry::with_auto_discovered().get_by_name("linked_by_export").is_some(),
854 "the dialect registry did not find the linked plugin"
855 );
856 }
857
858 #[test]
860 fn the_macro_exports_this_build() {
861 let exported = unsafe { crate::dynamic::read_declaration(razeri_plugin_build()) };
863 assert_eq!(exported, Ok(crate::dynamic::BuildDeclaration::this_build()));
864 }
865}