cpex_core/hooks/payload.rs
1// Location: ./crates/cpex-core/src/hooks/payload.rs
2// Copyright 2025
3// SPDX-License-Identifier: Apache-2.0
4// Authors: Teryl Taylor
5//
6// PluginPayload trait and Extensions stub.
7//
8// PluginPayload is the base trait for all hook payloads, mirroring
9// Python's PluginPayload(BaseModel, frozen=True). All payloads in
10// the framework implement this trait, giving the executor and
11// registry a common bound for type safety.
12//
13// The trait is object-safe — the executor works with `Box<dyn PluginPayload>`
14// instead of `Box<dyn Any>`, catching type errors at compile time.
15// Downcasting to concrete types uses the `as_any()` method.
16//
17// Extensions is the typed container for all message extensions
18// (security, delegation, HTTP, meta, etc.). It is always passed
19// as a separate parameter to handlers — never inside the payload.
20// This allows per-plugin capability filtering and independent
21// modification without copying the payload.
22
23use std::any::Any;
24use std::fmt;
25
26// Re-export Extensions and OwnedExtensions from the extensions module.
27// These are the typed containers for all extension data. They live in
28// extensions/container.rs but are re-exported here for backward
29// compatibility with existing code that imports from hooks::payload.
30pub use crate::extensions::{Extensions, Guarded, MetaExtension, OwnedExtensions, WriteToken};
31
32// ---------------------------------------------------------------------------
33// PluginPayload Trait
34// ---------------------------------------------------------------------------
35
36/// Base trait for all hook payloads.
37///
38/// Mirrors Python's `PluginPayload(BaseModel, frozen=True)`. Every
39/// payload type in the framework implements this trait. The executor
40/// and registry use `Box<dyn PluginPayload>` (not `Box<dyn Any>`)
41/// for type-safe dispatch.
42///
43/// The trait is **object-safe** — it can be used behind `Box`, `&`,
44/// and `Arc` without knowing the concrete type. This is achieved by
45/// providing `clone_boxed()` instead of requiring `Clone` directly
46/// (which is not object-safe), and `as_any()` / `as_any_mut()` for
47/// downcasting to the concrete type when needed.
48///
49/// Payloads are:
50/// - Cloneable via `clone_boxed()` — the executor uses this for COW
51/// when a modifying plugin (Sequential or Transform) needs ownership.
52/// - `Send + Sync` — payloads may be shared across threads for
53/// Concurrent mode plugins.
54/// - `'static` — payloads must be owned types (no borrowed references).
55///
56/// Extensions are **not** part of the payload. They are passed as a
57/// separate `&Extensions` parameter to handlers.
58///
59/// # Examples
60///
61/// ```
62/// use cpex_core::hooks::payload::PluginPayload;
63///
64/// #[derive(Debug, Clone)]
65/// struct RateLimitPayload {
66/// client_id: String,
67/// request_count: u64,
68/// }
69///
70/// impl PluginPayload for RateLimitPayload {
71/// fn clone_boxed(&self) -> Box<dyn PluginPayload> {
72/// Box::new(self.clone())
73/// }
74/// fn as_any(&self) -> &dyn std::any::Any { self }
75/// fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
76/// }
77/// ```
78pub trait PluginPayload: Send + Sync + 'static {
79 /// Clone this payload into a new `Box<dyn PluginPayload>`.
80 ///
81 /// Used by the executor for copy-on-write: read-only modes borrow
82 /// the payload, modifying modes receive a clone via this method.
83 fn clone_boxed(&self) -> Box<dyn PluginPayload>;
84
85 /// Downcast to a concrete type via `&dyn Any`.
86 ///
87 /// Used by typed handler wrappers to recover the concrete payload
88 /// type from `Box<dyn PluginPayload>`.
89 fn as_any(&self) -> &dyn Any;
90
91 /// Downcast to a concrete type via `&mut dyn Any`.
92 fn as_any_mut(&mut self) -> &mut dyn Any;
93}
94
95impl fmt::Debug for dyn PluginPayload {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 f.write_str("dyn PluginPayload")
98 }
99}
100
101// ---------------------------------------------------------------------------
102// Blanket helper macro for implementing PluginPayload
103// ---------------------------------------------------------------------------
104
105/// Implements `PluginPayload` for a type that is `Clone + Send + Sync + 'static`.
106///
107/// Saves boilerplate — instead of writing the three methods manually,
108/// just invoke this macro:
109///
110/// ```
111/// use cpex_core::impl_plugin_payload;
112///
113/// #[derive(Debug, Clone)]
114/// struct MyPayload { value: i32 }
115///
116/// impl_plugin_payload!(MyPayload);
117/// ```
118#[macro_export]
119macro_rules! impl_plugin_payload {
120 ($ty:ty) => {
121 impl $crate::hooks::payload::PluginPayload for $ty {
122 fn clone_boxed(&self) -> Box<dyn $crate::hooks::payload::PluginPayload> {
123 Box::new(self.clone())
124 }
125 fn as_any(&self) -> &dyn std::any::Any {
126 self
127 }
128 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
129 self
130 }
131 }
132 };
133}