cpex_core/factory.rs
1// Location: ./crates/cpex-core/src/factory.rs
2// Copyright 2025
3// SPDX-License-Identifier: Apache-2.0
4// Authors: Teryl Taylor
5//
6// Plugin factory registry.
7//
8// Provides a factory pattern for creating plugin instances from
9// config. The host registers factories by `kind` name before
10// loading config. When the manager processes a config file, it
11// looks up the factory for each plugin's `kind` and calls create().
12//
13// This decouples plugin instantiation from the manager — the
14// manager doesn't know how to create a "builtin" vs "wasm" vs
15// "python" plugin. The factory does.
16//
17// Mirrors the Python framework's PluginLoader in
18// cpex/framework/loader/plugin.py.
19
20use std::collections::HashMap;
21use std::sync::Arc;
22
23use crate::error::PluginError;
24use crate::plugin::{Plugin, PluginConfig};
25use crate::registry::AnyHookHandler;
26
27// ---------------------------------------------------------------------------
28// Plugin Factory Trait
29// ---------------------------------------------------------------------------
30
31/// Factory for creating plugin instances from config.
32///
33/// The host registers factories by `kind` name before loading
34/// config. When the manager processes a config file, it looks up
35/// the factory for each plugin's `kind` and calls `create()`.
36///
37/// The factory returns both the plugin and its handler because it
38/// knows the concrete types — which handler traits the plugin
39/// implements and which hooks it handles.
40///
41/// # Examples
42///
43/// ```rust,ignore
44/// struct RateLimiterFactory;
45///
46/// impl PluginFactory for RateLimiterFactory {
47/// fn create(&self, config: &PluginConfig)
48/// -> Result<PluginInstance, Box<PluginError>>
49/// {
50/// let plugin = Arc::new(RateLimiter::from_config(config)?);
51/// let handler = Arc::new(TypedHandlerAdapter::<RequestHeadersReceived, _>::new(
52/// Arc::clone(&plugin),
53/// ));
54/// Ok(PluginInstance { plugin, handler })
55/// }
56/// }
57///
58/// let mut factories = PluginFactoryRegistry::new();
59/// factories.register("security/rate_limit", Box::new(RateLimiterFactory));
60/// ```
61pub trait PluginFactory: Send + Sync {
62 /// Create a plugin instance and its handler from config.
63 ///
64 /// The `config` is the plugin's entry from the YAML file.
65 fn create(&self, config: &PluginConfig) -> Result<PluginInstance, Box<PluginError>>;
66}
67
68/// A created plugin instance — the plugin and its type-erased handlers.
69///
70/// Each handler is paired with the hook name it handles. A plugin
71/// that implements multiple hook types (e.g., `ToolPreInvoke` and
72/// `ToolPostInvoke`) returns one entry per hook.
73pub struct PluginInstance {
74 /// The plugin implementation.
75 pub plugin: Arc<dyn Plugin>,
76
77 /// Type-erased handlers paired with their hook names.
78 /// Each entry maps a hook name to the adapter for that hook type.
79 pub handlers: Vec<(&'static str, Arc<dyn AnyHookHandler>)>,
80}
81
82// ---------------------------------------------------------------------------
83// Plugin Factory Registry
84// ---------------------------------------------------------------------------
85
86/// Registry of plugin factories keyed by `kind` name.
87///
88/// The host populates this before calling `PluginManager::from_config()`.
89/// Each factory knows how to create plugins of a specific kind.
90///
91/// # Examples
92///
93/// ```rust,ignore
94/// let mut factories = PluginFactoryRegistry::new();
95/// factories.register("builtin/rate_limit", Box::new(RateLimiterFactory));
96/// factories.register("builtin/identity", Box::new(IdentityFactory));
97///
98/// let manager = PluginManager::from_config(path, &factories)?;
99/// ```
100pub struct PluginFactoryRegistry {
101 factories: HashMap<String, Box<dyn PluginFactory>>,
102}
103
104impl PluginFactoryRegistry {
105 /// Create an empty factory registry.
106 pub fn new() -> Self {
107 Self {
108 factories: HashMap::new(),
109 }
110 }
111
112 /// Register a factory for a given `kind` name.
113 ///
114 /// Registration is last-writer-wins: re-registering an existing `kind`
115 /// overrides it (this is intentional — a host can swap a builtin's impl).
116 /// Because silent override is a footgun, a warning is logged when an
117 /// existing registration is replaced.
118 pub fn register(&mut self, kind: impl Into<String>, factory: Box<dyn PluginFactory>) {
119 let kind = kind.into();
120 if self.factories.insert(kind.clone(), factory).is_some() {
121 tracing::warn!(kind = %kind, "plugin factory overrides an existing registration");
122 }
123 }
124
125 /// Look up a factory by `kind` name.
126 pub fn get(&self, kind: &str) -> Option<&dyn PluginFactory> {
127 self.factories.get(kind).map(|f| f.as_ref())
128 }
129
130 /// Whether a factory exists for the given `kind`.
131 pub fn has(&self, kind: &str) -> bool {
132 self.factories.contains_key(kind)
133 }
134
135 /// All registered kind names.
136 pub fn kinds(&self) -> Vec<&str> {
137 self.factories.keys().map(|s| s.as_str()).collect()
138 }
139}
140
141impl Default for PluginFactoryRegistry {
142 fn default() -> Self {
143 Self::new()
144 }
145}