cpex_core/hooks/adapter.rs
1// Location: ./crates/cpex-core/src/hooks/adapter.rs
2// Copyright 2025
3// SPDX-License-Identifier: Apache-2.0
4// Authors: Teryl Taylor
5//
6// TypedHandlerAdapter — bridges typed HookHandler<H> to the
7// type-erased AnyHookHandler.
8//
9// This is framework plumbing that plugin authors never see. When a
10// plugin is registered via `manager.register_handler::<H, P>()`, the
11// manager creates a TypedHandlerAdapter internally. The adapter
12// translates between Box<dyn PluginPayload> (what the executor passes)
13// and the concrete payload type (what the handler expects), and awaits
14// the typed handler's future before re-erasing the result.
15//
16// `HookHandler<H>` is async-by-default (native AFIT). Plugins that
17// don't await anything still write `async fn handle(...)`; the
18// compiler emits a trivially-ready future that LLVM inlines, so the
19// `.await` here is a no-op for sync-style plugins.
20
21use std::marker::PhantomData;
22use std::sync::Arc;
23
24use crate::context::PluginContext;
25use crate::error::PluginError;
26use crate::executor::erase_result;
27use crate::hooks::payload::{Extensions, PluginPayload};
28use crate::hooks::trait_def::{HookHandler, HookTypeDef, PluginResult};
29use crate::plugin::Plugin;
30use crate::registry::AnyHookHandler;
31
32// ---------------------------------------------------------------------------
33// Typed Handler Adapter
34// ---------------------------------------------------------------------------
35
36/// Adapts a typed `HookHandler<H>` into the type-erased `AnyHookHandler`
37/// interface used by the executor.
38///
39/// Created automatically by `PluginManager::register_handler()`. Plugin
40/// authors never instantiate this directly.
41///
42/// `HookHandler<H>` is async (native AFIT), so the adapter awaits the
43/// returned future before re-erasing the result. Plugins that don't
44/// `.await` anything compile to a ready future that LLVM inlines, so
45/// they pay no observable cost over a plain function call.
46///
47/// # Type Parameters
48///
49/// - `H` — the hook type (implements `HookTypeDef`).
50/// - `P` — the plugin type (implements `Plugin + HookHandler<H>`).
51pub struct TypedHandlerAdapter<H, P>
52where
53 H: HookTypeDef,
54 H::Result: Into<PluginResult<H::Payload>>,
55 P: Plugin + HookHandler<H> + 'static,
56{
57 /// The plugin instance.
58 plugin: Arc<P>,
59
60 /// Phantom data to carry the hook type parameter.
61 _hook: PhantomData<H>,
62}
63
64impl<H, P> TypedHandlerAdapter<H, P>
65where
66 H: HookTypeDef,
67 H::Result: Into<PluginResult<H::Payload>>,
68 P: Plugin + HookHandler<H> + 'static,
69{
70 /// Create a new adapter wrapping the given plugin.
71 pub fn new(plugin: Arc<P>) -> Self {
72 Self {
73 plugin,
74 _hook: PhantomData,
75 }
76 }
77}
78
79#[async_trait::async_trait]
80impl<H, P> AnyHookHandler for TypedHandlerAdapter<H, P>
81where
82 H: HookTypeDef,
83 H::Result: Into<PluginResult<H::Payload>>,
84 P: Plugin + HookHandler<H> + 'static,
85{
86 /// Downcast the type-erased payload to the concrete type and call
87 /// the plugin's typed `handle()` method, awaiting the returned
88 /// future.
89 ///
90 /// The framework retains ownership of the payload — the handler
91 /// receives a borrow (`&H::Payload`) and clones only if it needs
92 /// to modify. The result is erased back to `ErasedResultFields`
93 /// for the executor.
94 ///
95 /// For plugins whose body contains no `.await`, the compiler emits
96 /// a trivially-ready future and LLVM inlines this `.await` to a
97 /// direct return — there is no observable runtime cost over a
98 /// plain function call.
99 async fn invoke(
100 &self,
101 payload: &dyn PluginPayload,
102 extensions: &Extensions,
103 ctx: &mut PluginContext,
104 ) -> Result<Box<dyn std::any::Any + Send + Sync>, Box<PluginError>> {
105 let typed_ref: &H::Payload =
106 payload
107 .as_any()
108 .downcast_ref::<H::Payload>()
109 .ok_or_else(|| PluginError::Config {
110 message: format!(
111 "payload type mismatch for hook '{}': expected {}",
112 H::NAME,
113 std::any::type_name::<H::Payload>()
114 ),
115 })?;
116
117 let result = self.plugin.handle(typed_ref, extensions, ctx).await;
118 let plugin_result: PluginResult<H::Payload> = result.into();
119
120 Ok(erase_result(plugin_result))
121 }
122
123 fn hook_type_name(&self) -> &'static str {
124 H::NAME
125 }
126}