razeri-plugin-api 0.22.0

Extension traits and registries for Razeri plugins
Documentation
// Copyright (C) 2026 Gabriel Shea
// Licensed under either of the Apache License, Version 2.0 or the MIT
// license, at your option.

#![warn(missing_docs)]

//! # razeri-plugin-api
//!
//! Extension point traits and registries for the Razeri platform.
//!
//! This crate defines the **plugin API surface** — the traits that third-party
//! developers implement to extend Razeri with new database dialects, storage
//! backends, LLM providers, language renderers, and event sinks.
//!
//! ## Extension Points
//!
//! | Trait | Registry | Key Type |
//! |-------|----------|----------|
//! | [`PlatformDialect`] | [`PlatformRegistry`] | string (canonical name) |
//! | [`StorageProviderFactory`] | [`StorageRegistry`] | `StoragePlatform` enum + string |
//! | [`LlmClientFactory`] / [`LlmClient`] | [`LlmRegistry`] | string |
//! | [`LlmModelAdapter`] | [`ModelRegistry`] | (provider, model) pair |
//! | [`LanguageRendererFactory`] | [`LanguageRegistry`] | string |
//! | [`EventSink`] | [`EventRegistry`] | fan-out (all sinks) |
//!
//! ## Beside the extension points
//!
//! | Module | What it holds |
//! |--------|---------------|
//! | [`conformance`] | the report that grades a dialect against everything it owes, run from the dialect's own tests |
//! | [`resolve`] | a platform name, an alias or `name@version`, turned into the dialect that renders it |
//! | [`process`] | the one dialect registry a process resolves names against, and the plugin directory that feeds it |
//! | [`dynamic`] | loading a plugin built as a shared library, and refusing one built differently from the razeri that loads it |
//! | [`alerting`] | alert rules and health summaries evaluated over the event stream |
//! | [`sinks`] | the event sinks razeri ships, written against the same [`EventSink`] trait a plugin implements |
//!
//! ## Composition Root
//!
//! Use [`PluginRegistryBuilder`] to wire everything together at application
//! startup, then pass the resulting [`PluginRegistry`] to subsystems. The
//! builder's own page shows it in use.

pub mod alerting;
pub mod builder;
pub mod conformance;
pub mod dialect;
pub mod dynamic;
pub mod error;
pub mod event;
pub mod language;
pub mod llm;
pub mod model;
pub mod process;
pub mod registry;
pub mod resolve;
pub mod sinks;
pub mod storage;

// ── Convenience re-exports ────────────────────────────────────────────

pub use builder::PluginRegistryBuilder;
pub use dialect::{
    BlockTerminator, DeployClient, DeployScript, DialectHandle, DialectInfo, PlatformDialect,
    PlatformRegistry, UnsupportedVersion,
};
pub use razeri_plugin_contract::{EngineVersion, EngineVersionError};

// The contract a plugin's types and traits are written against, re-exported so
// a dialect crate can depend on this one crate and reach everything it needs.
pub use dynamic::{
    BuildDeclaration, BuildDeclarationFn, BuildPart, PLUGIN_BUILD_SYMBOL, PLUGIN_REGISTER_SYMBOL,
    PluginLoadResult, PluginRefusal, RegisterPluginFn,
};
pub use razeri_plugin_contract;

/// **The named routine grammars, spelled.**
///
/// A dialect whose engine has a routine in a grammar Razeri does not carry
/// renders the whole `CREATE` itself. These three functions write out the
/// grammars Razeri does carry, from the same
/// [`RoutineSpec`](razeri_plugin_contract::RoutineSpec) a dialect receives, so a
/// grammar that is *"PL/pgSQL with one tweak"* starts from PL/pgSQL and says
/// what differs rather than being written from nothing.
pub use razeri_plugin_contract::routine_render;
pub use razeri_plugin_contract::routine_render::{
    render_procedure_as, render_scalar_function_as, render_table_function_as,
};

#[cfg(all(feature = "dynamic", not(target_arch = "wasm32")))]
pub use dynamic::load_plugins_from_dir;
pub use error::{EventSinkError, LanguageError, LlmError, StorageError};
pub use event::{
    EventCategory, EventEmitter, EventFilterKeys, EventPayload, EventQuery, EventRegistry,
    EventSeverity, EventSink, EventSubject, EventSubjectKind, EventSummary, EventValidationError,
    RazeriEvent, WorkflowContext,
};
pub use language::{LanguageRegistry, LanguageRenderer, LanguageRendererFactory};
pub use llm::{
    AdvisorTokenUsage, ChatResponse, LlmClient, LlmClientFactory, LlmRegistry, Message, ModelInfo,
    TokenUsage,
};
pub use model::{LlmModelAdapter, ModelCapabilities, ModelRegistry};
pub use process::{
    InstallError, PLUGIN_DIR_ENV, PluginDirReport, install_plugin_dir, plugin_dir_from_environment,
    plugin_dir_report, process_event_sinks, process_registry,
};
pub use registry::PluginRegistry;
pub use resolve::ResolveError;
pub use storage::{
    StorageListEntry, StorageMetadata, StorageOps, StorageProviderFactory, StorageRegistry,
};

// Re-export built-in sink types and factory. The JSONL sink is native-only
// (filesystem via `tokio::fs`), so its re-export is absent on wasm.
#[cfg(not(target_arch = "wasm32"))]
pub use sinks::jsonl::JsonlSink;
pub use sinks::stdout::StdoutSink;
pub use sinks::webhook::WebhookSink;
pub use sinks::{EventSinkConfig, create_sink};

// Re-export alerting and health model
pub use alerting::{
    AlertCondition, AlertMetric, AlertResult, AlertRule, AlertScope, ComparisonOp, HealthDomain,
    HealthStatus, HealthSummary, SinkStatus, discover_health, evaluate_rules, health_for_domain,
};

// Re-export inventory registration types
pub use builder::{
    DialectRegistration, EventSinkRegistration, LanguageRegistration, LlmRegistration,
    ModelRegistration, PluginRegistration, StorageRegistration,
};

/// **Make a crate a razeri plugin, in one line.**
///
/// Name a function that registers what the crate provides:
///
/// ```ignore
/// use razeri_plugin_api::PluginRegistryBuilder;
/// use std::sync::Arc;
///
/// fn register(builder: &mut PluginRegistryBuilder) {
///     builder.register_dialect(Arc::new(MyDb));
/// }
///
/// razeri_plugin_api::export_plugin!(register);
/// ```
///
/// The crate can then reach razeri two ways, and the same line serves both:
///
/// - **Loaded from a plugin directory**, built as a `cdylib`. The line exports
///   how the library was built and the function razeri calls to register it.
///   Razeri reads the first before it calls the second, and refuses a library
///   built with another compiler or against other plugin crates.
/// - **Linked into a razeri you build**, as an ordinary dependency. The line
///   records `register` so the registry finds it at start-up, the same way it
///   finds razeri's own dialects.
///
/// Use it once per crate, at the top level of `lib.rs`.
#[macro_export]
macro_rules! export_plugin {
    ($register:path) => {
        /// How this plugin was built. Razeri reads it before it calls
        /// anything else in this library.
        #[unsafe(no_mangle)]
        pub extern "C" fn razeri_plugin_build()
        -> *const $crate::dynamic::export::ExportedBuildDeclaration {
            &$crate::dynamic::export::THIS_BUILD
        }

        /// Registers this plugin with the razeri that loaded it.
        ///
        /// # Safety
        ///
        /// Called by razeri only after this library's build declaration
        /// matched its own; `builder` is razeri's and outlives the call.
        #[unsafe(no_mangle)]
        pub unsafe extern "Rust" fn razeri_plugin_register_v1(
            builder: &mut $crate::PluginRegistryBuilder,
        ) {
            $register(builder);
        }

        $crate::dynamic::export::inventory::submit! {
            $crate::PluginRegistration::new($register)
        }
    };
}

// ── Convenience helpers ───────────────────────────────────────────────

use std::sync::Arc;

/// Resolve a platform name to the dialect that renders it.
///
/// This is the **one resolver** — all service crates call it instead of
/// matching on names themselves. Each dialect declares the spellings it answers
/// to, so `"pg"` reaches `PostgreSQL` and `"mssql"` reaches SQL Server because
/// those dialects say so. Matching ignores case and surrounding whitespace, and
/// an unknown name comes back with near-miss suggestions.
///
/// A dialect the host loaded at run time resolves the same way as one linked
/// in: both are read off the registry.
///
/// # Errors
///
/// [`ResolveError::UnknownDialect`], with suggestions, if no loaded dialect
/// answers to the name; [`ResolveError::AmbiguousName`] if two do.
///
/// # Examples
///
/// ```no_run
/// use razeri_plugin_api::{ResolveError, resolve_dialect_by_name};
///
/// # fn main() -> Result<(), ResolveError> {
/// // Aliases, canonical names and casing all resolve to the same dialect.
/// let dialect = resolve_dialect_by_name("pg")?;
/// println!("{}", dialect.info().name);
///
/// let dialect = resolve_dialect_by_name("SNOWFLAKE")?;
/// let dialect = resolve_dialect_by_name("sqlite")?;
/// # Ok(())
/// # }
/// ```
pub fn resolve_dialect_by_name(name: &str) -> Result<Arc<dyn PlatformDialect>, ResolveError> {
    resolve::resolve_dialect_by_name(name)
}

/// Resolve a dialect by string name, returning `None` on failure.
///
/// Convenience wrapper around [`resolve_dialect_by_name`] for callers that
/// prefer `Option` over `Result`.
#[must_use]
pub fn try_resolve_dialect_by_name(name: &str) -> Option<Arc<dyn PlatformDialect>> {
    resolve_dialect_by_name(name).ok()
}