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 auth: Default::default(),
385 }
386 }
387
388 #[test]
389 fn register_plugin() {
390 let mut registry = PluginRegistry::new(test_manifest());
391 let plugin = Arc::new(TestPlugin::new());
392 registry.register(plugin.clone());
393 assert_eq!(registry.plugins().len(), 1);
394 assert_eq!(registry.plugins()[0].name(), "test");
395 }
396
397 #[test]
398 fn before_insert_hook_allows() {
399 let mut registry = PluginRegistry::new(test_manifest());
400 registry.register(Arc::new(TestPlugin::new()));
401
402 let mut data = serde_json::json!({"title": "test"});
403 let auth = AuthContext::anonymous();
404 let result = registry.run_before_insert("Todo", &mut data, &auth);
405 assert!(result.is_ok());
406 }
407
408 #[test]
409 fn before_insert_hook_rejects() {
410 let mut registry = PluginRegistry::new(test_manifest());
411 registry.register(Arc::new(TestPlugin::new()));
412
413 let mut data = serde_json::json!({"title": "test"});
414 let auth = AuthContext::anonymous();
415 let result = registry.run_before_insert("Blocked", &mut data, &auth);
416 assert!(result.is_err());
417 assert_eq!(result.unwrap_err().code, "BLOCKED");
418 }
419
420 #[test]
421 fn after_insert_hook_fires() {
422 let mut registry = PluginRegistry::new(test_manifest());
423 let plugin = Arc::new(TestPlugin::new());
424 registry.register(plugin.clone());
425
426 let data = serde_json::json!({"title": "test"});
427 let auth = AuthContext::anonymous();
428 registry.run_after_insert("Todo", "1", &data, &auth);
429 assert_eq!(plugin.count(), 1);
430
431 registry.run_after_insert("Todo", "2", &data, &auth);
432 assert_eq!(plugin.count(), 2);
433 }
434
435 #[test]
436 fn on_request_middleware() {
437 struct BlockAdmin;
438 impl Plugin for BlockAdmin {
439 fn name(&self) -> &str {
440 "block-admin"
441 }
442 fn on_request(
443 &self,
444 _method: &str,
445 path: &str,
446 _auth: &AuthContext,
447 ) -> Result<(), PluginError> {
448 if path.starts_with("/api/admin") {
449 Err(PluginError {
450 code: "FORBIDDEN".into(),
451 message: "Admin access denied".into(),
452 status: 403,
453 })
454 } else {
455 Ok(())
456 }
457 }
458 }
459
460 let mut registry = PluginRegistry::new(test_manifest());
461 registry.register(Arc::new(BlockAdmin));
462
463 let auth = AuthContext::anonymous();
464 assert!(registry
465 .run_on_request("GET", "/api/entities/Todo", &auth)
466 .is_ok());
467 assert!(registry
468 .run_on_request("GET", "/api/admin/users", &auth)
469 .is_err());
470 }
471
472 #[test]
473 fn plugin_context_data() {
474 let ctx = PluginContext::new(test_manifest());
475 ctx.set("key", serde_json::json!("value"));
476 assert_eq!(ctx.get("key"), Some(serde_json::json!("value")));
477 assert_eq!(ctx.get("missing"), None);
478 }
479
480 #[test]
481 fn plugin_error_display() {
482 let err = PluginError {
483 code: "TEST".into(),
484 message: "msg".into(),
485 status: 400,
486 };
487 assert_eq!(format!("{err}"), "[TEST] msg");
488 }
489}