1use pylon_auth::AuthContext;
2use serde_json::Value;
3use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5
6pub trait Plugin: Send + Sync {
12 fn name(&self) -> &str;
14
15 fn on_init(&self, _ctx: &PluginContext) {}
17
18 fn routes(&self) -> Vec<PluginRoute> {
20 vec![]
21 }
22
23 fn before_insert(
25 &self,
26 _entity: &str,
27 _data: &mut Value,
28 _auth: &AuthContext,
29 ) -> Result<(), PluginError> {
30 Ok(())
31 }
32
33 fn after_insert(&self, _entity: &str, _id: &str, _data: &Value, _auth: &AuthContext) {}
35
36 fn before_update(
38 &self,
39 _entity: &str,
40 _id: &str,
41 _data: &mut Value,
42 _auth: &AuthContext,
43 ) -> Result<(), PluginError> {
44 Ok(())
45 }
46
47 fn after_update(&self, _entity: &str, _id: &str, _data: &Value, _auth: &AuthContext) {}
49
50 fn before_delete(
52 &self,
53 _entity: &str,
54 _id: &str,
55 _auth: &AuthContext,
56 ) -> Result<(), PluginError> {
57 Ok(())
58 }
59
60 fn after_delete(&self, _entity: &str, _id: &str, _auth: &AuthContext) {}
62
63 fn on_request(
65 &self,
66 _method: &str,
67 _path: &str,
68 _auth: &AuthContext,
69 ) -> Result<(), PluginError> {
70 Ok(())
71 }
72
73 fn on_request_with_meta(
79 &self,
80 method: &str,
81 path: &str,
82 auth: &AuthContext,
83 _meta: &RequestMeta<'_>,
84 ) -> Result<(), PluginError> {
85 self.on_request(method, path, auth)
86 }
87
88 fn on_session_create(&self, _user_id: &str, _token: &str) {}
90
91 fn entities(&self) -> Vec<pylon_kernel::ManifestEntity> {
93 vec![]
94 }
95}
96
97#[derive(Debug, Clone)]
107pub struct RequestMeta<'a> {
108 pub peer_ip: &'a str,
113}
114
115#[derive(Debug, Clone)]
116pub struct PluginError {
117 pub code: String,
118 pub message: String,
119 pub status: u16,
120}
121
122impl std::fmt::Display for PluginError {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 write!(f, "[{}] {}", self.code, self.message)
125 }
126}
127
128pub type RouteHandler = Box<dyn Fn(&str, &str, &AuthContext) -> (u16, String) + Send + Sync>;
130
131pub struct PluginRoute {
133 pub method: String,
134 pub path: String,
135 pub handler: RouteHandler,
136}
137
138pub struct PluginContext {
140 pub manifest: pylon_kernel::AppManifest,
141 pub data: Mutex<HashMap<String, Value>>,
142}
143
144impl PluginContext {
145 pub fn new(manifest: pylon_kernel::AppManifest) -> Self {
146 Self {
147 manifest,
148 data: Mutex::new(HashMap::new()),
149 }
150 }
151
152 pub fn set(&self, key: &str, value: Value) {
154 self.data.lock().unwrap().insert(key.to_string(), value);
155 }
156
157 pub fn get(&self, key: &str) -> Option<Value> {
159 self.data.lock().unwrap().get(key).cloned()
160 }
161}
162
163pub struct PluginRegistry {
168 plugins: Vec<Arc<dyn Plugin>>,
169 context: Arc<PluginContext>,
170}
171
172impl PluginRegistry {
173 pub fn new(manifest: pylon_kernel::AppManifest) -> Self {
174 Self {
175 plugins: Vec::new(),
176 context: Arc::new(PluginContext::new(manifest)),
177 }
178 }
179
180 pub fn register(&mut self, plugin: Arc<dyn Plugin>) {
182 plugin.on_init(&self.context);
183 self.plugins.push(plugin);
184 }
185
186 pub fn plugins(&self) -> &[Arc<dyn Plugin>] {
188 &self.plugins
189 }
190
191 pub fn all_routes(&self) -> Vec<&PluginRoute> {
193 vec![]
196 }
197
198 pub fn run_before_insert(
200 &self,
201 entity: &str,
202 data: &mut Value,
203 auth: &AuthContext,
204 ) -> Result<(), PluginError> {
205 for plugin in &self.plugins {
206 plugin.before_insert(entity, data, auth)?;
207 }
208 Ok(())
209 }
210
211 pub fn run_after_insert(&self, entity: &str, id: &str, data: &Value, auth: &AuthContext) {
213 for plugin in &self.plugins {
214 plugin.after_insert(entity, id, data, auth);
215 }
216 }
217
218 pub fn run_before_update(
220 &self,
221 entity: &str,
222 id: &str,
223 data: &mut Value,
224 auth: &AuthContext,
225 ) -> Result<(), PluginError> {
226 for plugin in &self.plugins {
227 plugin.before_update(entity, id, data, auth)?;
228 }
229 Ok(())
230 }
231
232 pub fn run_after_update(&self, entity: &str, id: &str, data: &Value, auth: &AuthContext) {
234 for plugin in &self.plugins {
235 plugin.after_update(entity, id, data, auth);
236 }
237 }
238
239 pub fn run_before_delete(
241 &self,
242 entity: &str,
243 id: &str,
244 auth: &AuthContext,
245 ) -> Result<(), PluginError> {
246 for plugin in &self.plugins {
247 plugin.before_delete(entity, id, auth)?;
248 }
249 Ok(())
250 }
251
252 pub fn run_after_delete(&self, entity: &str, id: &str, auth: &AuthContext) {
254 for plugin in &self.plugins {
255 plugin.after_delete(entity, id, auth);
256 }
257 }
258
259 pub fn run_on_request(
266 &self,
267 method: &str,
268 path: &str,
269 auth: &AuthContext,
270 ) -> Result<(), PluginError> {
271 for plugin in &self.plugins {
272 plugin.on_request(method, path, auth)?;
273 }
274 Ok(())
275 }
276
277 pub fn run_on_request_with_meta(
282 &self,
283 method: &str,
284 path: &str,
285 auth: &AuthContext,
286 meta: &RequestMeta<'_>,
287 ) -> Result<(), PluginError> {
288 for plugin in &self.plugins {
289 plugin.on_request_with_meta(method, path, auth, meta)?;
290 }
291 Ok(())
292 }
293
294 pub fn try_handle_route(
296 &self,
297 method: &str,
298 path: &str,
299 body: &str,
300 auth: &AuthContext,
301 ) -> Option<(u16, String)> {
302 for plugin in &self.plugins {
303 for route in plugin.routes() {
304 if route.method == method && path.starts_with(&route.path) {
305 return Some((route.handler)(body, path, auth));
306 }
307 }
308 }
309 None
310 }
311}
312
313pub mod builtin;
318
319pub mod registry;
324
325#[cfg(test)]
330mod tests {
331 use super::*;
332
333 struct TestPlugin {
334 insert_count: Mutex<u32>,
335 }
336
337 impl TestPlugin {
338 fn new() -> Self {
339 Self {
340 insert_count: Mutex::new(0),
341 }
342 }
343 fn count(&self) -> u32 {
344 *self.insert_count.lock().unwrap()
345 }
346 }
347
348 impl Plugin for TestPlugin {
349 fn name(&self) -> &str {
350 "test"
351 }
352
353 fn after_insert(&self, _entity: &str, _id: &str, _data: &Value, _auth: &AuthContext) {
354 *self.insert_count.lock().unwrap() += 1;
355 }
356
357 fn before_insert(
358 &self,
359 entity: &str,
360 _data: &mut Value,
361 _auth: &AuthContext,
362 ) -> Result<(), PluginError> {
363 if entity == "Blocked" {
364 return Err(PluginError {
365 code: "BLOCKED".into(),
366 message: "Inserts to Blocked are not allowed".into(),
367 status: 403,
368 });
369 }
370 Ok(())
371 }
372 }
373
374 fn test_manifest() -> pylon_kernel::AppManifest {
375 pylon_kernel::AppManifest {
376 manifest_version: pylon_kernel::MANIFEST_VERSION,
377 name: "test".into(),
378 version: "0.1.0".into(),
379 entities: vec![],
380 routes: vec![],
381 queries: vec![],
382 actions: vec![],
383 policies: vec![],
384 }
385 }
386
387 #[test]
388 fn register_plugin() {
389 let mut registry = PluginRegistry::new(test_manifest());
390 let plugin = Arc::new(TestPlugin::new());
391 registry.register(plugin.clone());
392 assert_eq!(registry.plugins().len(), 1);
393 assert_eq!(registry.plugins()[0].name(), "test");
394 }
395
396 #[test]
397 fn before_insert_hook_allows() {
398 let mut registry = PluginRegistry::new(test_manifest());
399 registry.register(Arc::new(TestPlugin::new()));
400
401 let mut data = serde_json::json!({"title": "test"});
402 let auth = AuthContext::anonymous();
403 let result = registry.run_before_insert("Todo", &mut data, &auth);
404 assert!(result.is_ok());
405 }
406
407 #[test]
408 fn before_insert_hook_rejects() {
409 let mut registry = PluginRegistry::new(test_manifest());
410 registry.register(Arc::new(TestPlugin::new()));
411
412 let mut data = serde_json::json!({"title": "test"});
413 let auth = AuthContext::anonymous();
414 let result = registry.run_before_insert("Blocked", &mut data, &auth);
415 assert!(result.is_err());
416 assert_eq!(result.unwrap_err().code, "BLOCKED");
417 }
418
419 #[test]
420 fn after_insert_hook_fires() {
421 let mut registry = PluginRegistry::new(test_manifest());
422 let plugin = Arc::new(TestPlugin::new());
423 registry.register(plugin.clone());
424
425 let data = serde_json::json!({"title": "test"});
426 let auth = AuthContext::anonymous();
427 registry.run_after_insert("Todo", "1", &data, &auth);
428 assert_eq!(plugin.count(), 1);
429
430 registry.run_after_insert("Todo", "2", &data, &auth);
431 assert_eq!(plugin.count(), 2);
432 }
433
434 #[test]
435 fn on_request_middleware() {
436 struct BlockAdmin;
437 impl Plugin for BlockAdmin {
438 fn name(&self) -> &str {
439 "block-admin"
440 }
441 fn on_request(
442 &self,
443 _method: &str,
444 path: &str,
445 _auth: &AuthContext,
446 ) -> Result<(), PluginError> {
447 if path.starts_with("/api/admin") {
448 Err(PluginError {
449 code: "FORBIDDEN".into(),
450 message: "Admin access denied".into(),
451 status: 403,
452 })
453 } else {
454 Ok(())
455 }
456 }
457 }
458
459 let mut registry = PluginRegistry::new(test_manifest());
460 registry.register(Arc::new(BlockAdmin));
461
462 let auth = AuthContext::anonymous();
463 assert!(registry
464 .run_on_request("GET", "/api/entities/Todo", &auth)
465 .is_ok());
466 assert!(registry
467 .run_on_request("GET", "/api/admin/users", &auth)
468 .is_err());
469 }
470
471 #[test]
472 fn plugin_context_data() {
473 let ctx = PluginContext::new(test_manifest());
474 ctx.set("key", serde_json::json!("value"));
475 assert_eq!(ctx.get("key"), Some(serde_json::json!("value")));
476 assert_eq!(ctx.get("missing"), None);
477 }
478
479 #[test]
480 fn plugin_error_display() {
481 let err = PluginError {
482 code: "TEST".into(),
483 message: "msg".into(),
484 status: 400,
485 };
486 assert_eq!(format!("{err}"), "[TEST] msg");
487 }
488}