razeri_plugin_api/lib.rs
1// Copyright (C) 2026 Gabriel Shea
2// Licensed under either of the Apache License, Version 2.0 or the MIT
3// license, at your option.
4
5#![warn(missing_docs)]
6
7//! # razeri-plugin-api
8//!
9//! Extension point traits and registries for the Razeri platform.
10//!
11//! This crate defines the **plugin API surface** — the traits that third-party
12//! developers implement to extend Razeri with new database dialects, storage
13//! backends, LLM providers, language renderers, and event sinks.
14//!
15//! ## Extension Points
16//!
17//! | Trait | Registry | Key Type |
18//! |-------|----------|----------|
19//! | [`PlatformDialect`] | [`PlatformRegistry`] | string (canonical name) |
20//! | [`StorageProviderFactory`] | [`StorageRegistry`] | `StoragePlatform` enum + string |
21//! | [`LlmClientFactory`] / [`LlmClient`] | [`LlmRegistry`] | string |
22//! | [`LlmModelAdapter`] | [`ModelRegistry`] | (provider, model) pair |
23//! | [`LanguageRendererFactory`] | [`LanguageRegistry`] | string |
24//! | [`EventSink`] | [`EventRegistry`] | fan-out (all sinks) |
25//!
26//! ## Composition Root
27//!
28//! Use [`PluginRegistryBuilder`] to wire everything together at application
29//! startup, then pass the resulting [`PluginRegistry`] to subsystems:
30//!
31//! ```ignore
32//! let registry = PluginRegistryBuilder::new()
33//! .with_dialect("postgres", Arc::new(pg_dialect))
34//! .with_llm("ollama".into(), Arc::new(ollama_factory))
35//! .with_auto_discovered()
36//! .build();
37//! ```
38
39pub mod alerting;
40pub mod builder;
41pub mod conformance;
42pub mod dialect;
43pub mod dynamic;
44pub mod error;
45pub mod event;
46pub mod language;
47pub mod llm;
48pub mod model;
49pub mod process;
50pub mod registry;
51pub mod resolve;
52pub mod sinks;
53pub mod storage;
54
55// ── Convenience re-exports ────────────────────────────────────────────
56
57pub use builder::PluginRegistryBuilder;
58pub use dialect::{
59 DeployScript, DialectHandle, DialectInfo, PlatformDialect, PlatformRegistry, UnsupportedVersion,
60};
61pub use razeri_plugin_contract::{EngineVersion, EngineVersionError};
62
63// The contract a plugin's types and traits are written against, re-exported so
64// a dialect crate can depend on this one crate and reach everything it needs.
65pub use dynamic::{
66 BuildDeclaration, BuildDeclarationFn, BuildPart, PLUGIN_BUILD_SYMBOL, PLUGIN_REGISTER_SYMBOL,
67 PluginLoadResult, PluginRefusal, RegisterPluginFn,
68};
69pub use razeri_plugin_contract;
70
71/// **The named routine grammars, spelled.**
72///
73/// A dialect whose engine has a routine in a grammar Razeri does not carry
74/// renders the whole `CREATE` itself. These three functions write out the
75/// grammars Razeri does carry, from the same
76/// [`RoutineSpec`](razeri_plugin_contract::RoutineSpec) a dialect receives, so a
77/// grammar that is *"PL/pgSQL with one tweak"* starts from PL/pgSQL and says
78/// what differs rather than being written from nothing.
79pub use razeri_plugin_contract::routine_render;
80pub use razeri_plugin_contract::routine_render::{
81 render_procedure_as, render_scalar_function_as, render_table_function_as,
82};
83
84#[cfg(all(feature = "dynamic", not(target_arch = "wasm32")))]
85pub use dynamic::load_plugins_from_dir;
86pub use error::{EventSinkError, LanguageError, LlmError, StorageError};
87pub use event::{
88 EventCategory, EventEmitter, EventFilterKeys, EventPayload, EventQuery, EventRegistry,
89 EventSeverity, EventSink, EventSubject, EventSubjectKind, EventSummary, EventValidationError,
90 RazeriEvent, WorkflowContext,
91};
92pub use language::{LanguageRegistry, LanguageRenderer, LanguageRendererFactory};
93pub use llm::{
94 ChatResponse, LlmClient, LlmClientFactory, LlmRegistry, Message, ModelInfo, TokenUsage,
95};
96pub use model::{LlmModelAdapter, ModelCapabilities, ModelRegistry};
97pub use process::{
98 InstallError, PLUGIN_DIR_ENV, PluginDirReport, install_plugin_dir, plugin_dir_from_environment,
99 plugin_dir_report, process_registry,
100};
101pub use registry::PluginRegistry;
102pub use resolve::ResolveError;
103pub use storage::{
104 StorageListEntry, StorageMetadata, StorageOps, StorageProviderFactory, StorageRegistry,
105};
106
107// Re-export built-in sink types and factory. The JSONL sink is native-only
108// (filesystem via `tokio::fs`), so its re-export is absent on wasm.
109#[cfg(not(target_arch = "wasm32"))]
110pub use sinks::jsonl::JsonlSink;
111pub use sinks::stdout::StdoutSink;
112pub use sinks::webhook::WebhookSink;
113pub use sinks::{EventSinkConfig, create_sink};
114
115// Re-export alerting and health model
116pub use alerting::{
117 AlertCondition, AlertMetric, AlertResult, AlertRule, AlertScope, ComparisonOp, HealthDomain,
118 HealthStatus, HealthSummary, SinkStatus, discover_health, evaluate_rules, health_for_domain,
119};
120
121// Re-export inventory registration types
122pub use builder::{
123 DialectRegistration, EventSinkRegistration, LanguageRegistration, LlmRegistration,
124 ModelRegistration, PluginRegistration, StorageRegistration,
125};
126
127/// **Make a crate a razeri plugin, in one line.**
128///
129/// Name a function that registers what the crate provides:
130///
131/// ```ignore
132/// use razeri_plugin_api::PluginRegistryBuilder;
133/// use std::sync::Arc;
134///
135/// fn register(builder: &mut PluginRegistryBuilder) {
136/// builder.register_dialect(Arc::new(MyDb));
137/// }
138///
139/// razeri_plugin_api::export_plugin!(register);
140/// ```
141///
142/// The crate can then reach razeri two ways, and the same line serves both:
143///
144/// - **Loaded from a plugin directory**, built as a `cdylib`. The line exports
145/// how the library was built and the function razeri calls to register it.
146/// Razeri reads the first before it calls the second, and refuses a library
147/// built with another compiler or against other plugin crates.
148/// - **Linked into a razeri you build**, as an ordinary dependency. The line
149/// records `register` so the registry finds it at start-up, the same way it
150/// finds razeri's own dialects.
151///
152/// Use it once per crate, at the top level of `lib.rs`.
153#[macro_export]
154macro_rules! export_plugin {
155 ($register:path) => {
156 /// How this plugin was built. Razeri reads it before it calls
157 /// anything else in this library.
158 #[unsafe(no_mangle)]
159 pub extern "C" fn razeri_plugin_build()
160 -> *const $crate::dynamic::export::ExportedBuildDeclaration {
161 &$crate::dynamic::export::THIS_BUILD
162 }
163
164 /// Registers this plugin with the razeri that loaded it.
165 ///
166 /// # Safety
167 ///
168 /// Called by razeri only after this library's build declaration
169 /// matched its own; `builder` is razeri's and outlives the call.
170 #[unsafe(no_mangle)]
171 pub unsafe extern "Rust" fn razeri_plugin_register_v1(
172 builder: &mut $crate::PluginRegistryBuilder,
173 ) {
174 $register(builder);
175 }
176
177 $crate::dynamic::export::inventory::submit! {
178 $crate::PluginRegistration::new($register)
179 }
180 };
181}
182
183// ── Convenience helpers ───────────────────────────────────────────────
184
185use std::sync::Arc;
186
187/// Resolve a platform name to the dialect that renders it.
188///
189/// This is the **one resolver** — all service crates call it instead of
190/// matching on names themselves. Each dialect declares the spellings it answers
191/// to, so `"pg"` reaches `PostgreSQL` and `"mssql"` reaches SQL Server because
192/// those dialects say so. Matching ignores case and surrounding whitespace, and
193/// an unknown name comes back with near-miss suggestions.
194///
195/// A dialect the host loaded at run time resolves the same way as one linked
196/// in: both are read off the registry.
197///
198/// # Errors
199///
200/// [`ResolveError::UnknownDialect`], with suggestions, if no loaded dialect
201/// answers to the name; [`ResolveError::AmbiguousName`] if two do.
202///
203/// # Examples
204///
205/// ```no_run
206/// use razeri_plugin_api::{ResolveError, resolve_dialect_by_name};
207///
208/// # fn main() -> Result<(), ResolveError> {
209/// // Aliases, canonical names and casing all resolve to the same dialect.
210/// let dialect = resolve_dialect_by_name("pg")?;
211/// println!("{}", dialect.info().name);
212///
213/// let dialect = resolve_dialect_by_name("SNOWFLAKE")?;
214/// let dialect = resolve_dialect_by_name("sqlite")?;
215/// # Ok(())
216/// # }
217/// ```
218pub fn resolve_dialect_by_name(name: &str) -> Result<Arc<dyn PlatformDialect>, ResolveError> {
219 resolve::resolve_dialect_by_name(name)
220}
221
222/// Resolve a dialect by string name, returning `None` on failure.
223///
224/// Convenience wrapper around [`resolve_dialect_by_name`] for callers that
225/// prefer `Option` over `Result`.
226#[must_use]
227pub fn try_resolve_dialect_by_name(name: &str) -> Option<Arc<dyn PlatformDialect>> {
228 resolve_dialect_by_name(name).ok()
229}