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