pub mod builtin;
pub mod config;
use proc_macro2::TokenStream;
use quote::quote;
use prax_schema::ast::{CompositeType, Enum, Model, Schema, View};
pub use config::PluginConfig;
#[derive(Debug, Default, Clone)]
pub struct PluginOutput {
pub tokens: TokenStream,
pub root_items: TokenStream,
pub imports: Vec<String>,
}
#[allow(dead_code)]
impl PluginOutput {
pub fn new() -> Self {
Self::default()
}
pub fn with_tokens(tokens: TokenStream) -> Self {
Self {
tokens,
..Default::default()
}
}
pub fn add_tokens(&mut self, tokens: TokenStream) {
self.tokens.extend(tokens);
}
pub fn add_root_items(&mut self, tokens: TokenStream) {
self.root_items.extend(tokens);
}
pub fn add_import(&mut self, import: impl Into<String>) {
self.imports.push(import.into());
}
pub fn merge(&mut self, other: PluginOutput) {
self.tokens.extend(other.tokens);
self.root_items.extend(other.root_items);
self.imports.extend(other.imports);
}
pub fn is_empty(&self) -> bool {
self.tokens.is_empty() && self.root_items.is_empty() && self.imports.is_empty()
}
}
#[derive(Debug)]
pub struct PluginContext<'a> {
pub schema: &'a Schema,
pub config: &'a PluginConfig,
}
impl<'a> PluginContext<'a> {
pub fn new(schema: &'a Schema, config: &'a PluginConfig) -> Self {
Self { schema, config }
}
}
pub trait Plugin: Send + Sync {
fn name(&self) -> &'static str;
fn env_var(&self) -> &'static str;
fn description(&self) -> &'static str {
"No description provided"
}
fn on_start(&self, _ctx: &PluginContext) -> PluginOutput {
PluginOutput::new()
}
fn on_model(&self, _ctx: &PluginContext, _model: &Model) -> PluginOutput {
PluginOutput::new()
}
fn on_enum(&self, _ctx: &PluginContext, _enum_def: &Enum) -> PluginOutput {
PluginOutput::new()
}
fn on_type(&self, _ctx: &PluginContext, _type_def: &CompositeType) -> PluginOutput {
PluginOutput::new()
}
fn on_view(&self, _ctx: &PluginContext, _view: &View) -> PluginOutput {
PluginOutput::new()
}
fn on_finish(&self, _ctx: &PluginContext) -> PluginOutput {
PluginOutput::new()
}
}
#[derive(Default)]
pub struct PluginRegistry {
plugins: Vec<Box<dyn Plugin>>,
}
impl PluginRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn with_builtins() -> Self {
let mut registry = Self::new();
registry.register(Box::new(builtin::DebugPlugin));
registry.register(Box::new(builtin::JsonSchemaPlugin));
registry.register(Box::new(builtin::GraphQLPlugin));
registry.register(Box::new(builtin::SerdePlugin));
registry.register(Box::new(builtin::ValidatorPlugin));
registry
}
pub fn register(&mut self, plugin: Box<dyn Plugin>) {
self.plugins.push(plugin);
}
pub fn plugins(&self) -> &[Box<dyn Plugin>] {
&self.plugins
}
pub fn enabled_plugins(&self, config: &PluginConfig) -> Vec<&dyn Plugin> {
self.plugins
.iter()
.filter(|p| config.is_enabled(p.env_var()))
.map(|p| p.as_ref())
.collect()
}
pub fn run_start(&self, ctx: &PluginContext) -> PluginOutput {
let mut output = PluginOutput::new();
for plugin in self.enabled_plugins(ctx.config) {
output.merge(plugin.on_start(ctx));
}
output
}
pub fn run_model(&self, ctx: &PluginContext, model: &Model) -> PluginOutput {
let mut output = PluginOutput::new();
for plugin in self.enabled_plugins(ctx.config) {
output.merge(plugin.on_model(ctx, model));
}
output
}
pub fn run_enum(&self, ctx: &PluginContext, enum_def: &Enum) -> PluginOutput {
let mut output = PluginOutput::new();
for plugin in self.enabled_plugins(ctx.config) {
output.merge(plugin.on_enum(ctx, enum_def));
}
output
}
pub fn run_type(&self, ctx: &PluginContext, type_def: &CompositeType) -> PluginOutput {
let mut output = PluginOutput::new();
for plugin in self.enabled_plugins(ctx.config) {
output.merge(plugin.on_type(ctx, type_def));
}
output
}
pub fn run_view(&self, ctx: &PluginContext, view: &View) -> PluginOutput {
let mut output = PluginOutput::new();
for plugin in self.enabled_plugins(ctx.config) {
output.merge(plugin.on_view(ctx, view));
}
output
}
pub fn run_finish(&self, ctx: &PluginContext) -> PluginOutput {
let mut output = PluginOutput::new();
for plugin in self.enabled_plugins(ctx.config) {
output.merge(plugin.on_finish(ctx));
}
output
}
}
pub fn generate_plugin_docs(registry: &PluginRegistry) -> TokenStream {
let mut doc_lines = vec![
"# Available Plugins".to_string(),
String::new(),
"The following plugins can be enabled via environment variables:".to_string(),
String::new(),
];
for plugin in registry.plugins() {
doc_lines.push(format!("## {}", plugin.name()));
doc_lines.push(format!("- **Env var**: `{}`", plugin.env_var()));
doc_lines.push(format!("- **Description**: {}", plugin.description()));
doc_lines.push(String::new());
}
let doc_string = doc_lines.join("\n");
quote! {
#[doc = #doc_string]
pub mod _plugin_docs {}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestPlugin;
impl Plugin for TestPlugin {
fn name(&self) -> &'static str {
"test"
}
fn env_var(&self) -> &'static str {
"PRAX_PLUGIN_TEST"
}
fn description(&self) -> &'static str {
"A test plugin"
}
fn on_start(&self, _ctx: &PluginContext) -> PluginOutput {
PluginOutput::with_tokens(quote! {
const TEST_PLUGIN_ACTIVE: bool = true;
})
}
}
#[test]
fn test_plugin_output_merge() {
let mut output1 = PluginOutput::with_tokens(quote! { const A: i32 = 1; });
let output2 = PluginOutput::with_tokens(quote! { const B: i32 = 2; });
output1.merge(output2);
let code = output1.tokens.to_string();
assert!(code.contains("const A"));
assert!(code.contains("const B"));
}
#[test]
fn test_plugin_registry() {
let mut registry = PluginRegistry::new();
registry.register(Box::new(TestPlugin));
assert_eq!(registry.plugins().len(), 1);
assert_eq!(registry.plugins()[0].name(), "test");
}
#[test]
fn test_enabled_plugins() {
let mut registry = PluginRegistry::new();
registry.register(Box::new(TestPlugin));
let config = PluginConfig::from_env();
let enabled = registry.enabled_plugins(&config);
assert!(enabled.len() <= 1);
}
#[test]
fn test_plugin_output_new() {
let output = PluginOutput::new();
assert!(output.tokens.is_empty());
assert!(output.root_items.is_empty());
assert!(output.imports.is_empty());
assert!(output.is_empty());
}
#[test]
fn test_plugin_output_with_tokens() {
let output = PluginOutput::with_tokens(quote! { const X: i32 = 42; });
assert!(!output.tokens.is_empty());
assert!(!output.is_empty());
}
#[test]
fn test_plugin_output_add_tokens() {
let mut output = PluginOutput::new();
output.add_tokens(quote! { const A: i32 = 1; });
assert!(!output.is_empty());
assert!(output.tokens.to_string().contains("const A"));
}
#[test]
fn test_plugin_output_add_root_items() {
let mut output = PluginOutput::new();
output.add_root_items(quote! { pub mod root_module {} });
assert!(!output.root_items.is_empty());
assert!(output.root_items.to_string().contains("root_module"));
}
#[test]
fn test_plugin_output_add_import() {
let mut output = PluginOutput::new();
output.add_import("use std::collections::HashMap");
assert_eq!(output.imports.len(), 1);
assert_eq!(output.imports[0], "use std::collections::HashMap");
}
#[test]
fn test_plugin_output_is_empty() {
let empty = PluginOutput::new();
assert!(empty.is_empty());
let with_tokens = PluginOutput::with_tokens(quote! { const X: i32 = 1; });
assert!(!with_tokens.is_empty());
let mut with_imports = PluginOutput::new();
with_imports.add_import("use std::fmt");
assert!(!with_imports.is_empty());
let mut with_root_items = PluginOutput::new();
with_root_items.add_root_items(quote! { mod root {} });
assert!(!with_root_items.is_empty());
}
#[test]
fn test_plugin_output_merge_imports() {
let mut output1 = PluginOutput::new();
output1.add_import("import1");
output1.add_root_items(quote! { mod a {} });
let mut output2 = PluginOutput::new();
output2.add_import("import2");
output2.add_root_items(quote! { mod b {} });
output1.merge(output2);
assert_eq!(output1.imports.len(), 2);
assert!(output1.imports.contains(&"import1".to_string()));
assert!(output1.imports.contains(&"import2".to_string()));
assert!(output1.root_items.to_string().contains("mod a"));
assert!(output1.root_items.to_string().contains("mod b"));
}
#[test]
fn test_plugin_trait_name() {
let plugin = TestPlugin;
assert_eq!(plugin.name(), "test");
}
#[test]
fn test_plugin_trait_env_var() {
let plugin = TestPlugin;
assert_eq!(plugin.env_var(), "PRAX_PLUGIN_TEST");
}
#[test]
fn test_plugin_trait_description() {
let plugin = TestPlugin;
assert_eq!(plugin.description(), "A test plugin");
}
#[test]
fn test_plugin_registry_new() {
let registry = PluginRegistry::new();
assert!(registry.plugins().is_empty());
}
#[test]
fn test_plugin_registry_register_multiple() {
let mut registry = PluginRegistry::new();
registry.register(Box::new(TestPlugin));
registry.register(Box::new(TestPlugin));
assert_eq!(registry.plugins().len(), 2);
}
#[test]
fn test_plugin_registry_with_builtins() {
let registry = PluginRegistry::with_builtins();
assert!(!registry.plugins().is_empty());
}
#[test]
fn test_plugin_context_new() {
let schema = Schema::new();
let config = PluginConfig::new();
let ctx = PluginContext::new(&schema, &config);
assert_eq!(ctx.schema.models.len(), 0);
}
#[test]
fn test_plugin_context_schema_access() {
let schema = Schema::new();
let config = PluginConfig::new();
let ctx = PluginContext::new(&schema, &config);
assert!(ctx.schema.models.is_empty());
assert!(ctx.schema.enums.is_empty());
}
}