spikard_http/lifecycle/
adapter.rs1use crate::lifecycle::LifecycleHook;
8use axum::body::Body;
9use axum::http::{Request, Response};
10use std::sync::Arc;
11
12pub mod error {
15 use std::fmt::Display;
16
17 pub fn call_failed(hook_name: &str, reason: impl Display) -> String {
19 format!("Hook '{}' call failed: {}", hook_name, reason)
20 }
21
22 pub fn task_error(hook_name: &str, reason: impl Display) -> String {
24 format!("Hook '{}' task error: {}", hook_name, reason)
25 }
26
27 pub fn promise_failed(hook_name: &str, reason: impl Display) -> String {
29 format!("Hook '{}' promise failed: {}", hook_name, reason)
30 }
31
32 pub fn python_error(hook_name: &str, reason: impl Display) -> String {
34 format!("Hook '{}' Python error: {}", hook_name, reason)
35 }
36
37 pub fn body_read_failed(direction: &str, reason: impl Display) -> String {
39 format!("Failed to read {} body: {}", direction, reason)
40 }
41
42 pub fn body_write_failed(reason: impl Display) -> String {
44 format!("Failed to write body: {}", reason)
45 }
46
47 pub fn serialize_failed(context: &str, reason: impl Display) -> String {
49 format!("Failed to serialize {}: {}", context, reason)
50 }
51
52 pub fn deserialize_failed(context: &str, reason: impl Display) -> String {
54 format!("Failed to deserialize {}: {}", context, reason)
55 }
56
57 pub fn build_failed(what: &str, reason: impl Display) -> String {
59 format!("Failed to build {}: {}", what, reason)
60 }
61}
62
63pub mod serial {
65 use super::*;
66
67 pub async fn extract_body(body: Body) -> Result<bytes::Bytes, String> {
69 use axum::body::to_bytes;
70 to_bytes(body, usize::MAX)
71 .await
72 .map_err(|e| error::body_read_failed("request/response", e))
73 }
74
75 pub fn json_response_body(json: &serde_json::Value) -> Result<Body, String> {
77 serde_json::to_string(json)
78 .map(Body::from)
79 .map_err(|e| error::serialize_failed("response JSON", e))
80 }
81
82 pub fn parse_json(bytes: &[u8]) -> Result<serde_json::Value, String> {
84 if bytes.is_empty() {
85 return Ok(serde_json::Value::Null);
86 }
87 serde_json::from_slice(bytes)
88 .or_else(|_| Ok(serde_json::Value::String(String::from_utf8_lossy(bytes).to_string())))
89 }
90}
91
92pub use super::LifecycleHooks as HttpLifecycleHooks;
94
95pub struct HookRegistry;
97
98impl HookRegistry {
99 pub fn register_from_list<F>(
102 hooks: &mut HttpLifecycleHooks,
103 hook_list: Vec<Arc<dyn LifecycleHook<Request<Body>, Response<Body>>>>,
104 _hook_type: &str,
105 register_fn: F,
106 ) where
107 F: Fn(&mut HttpLifecycleHooks, Arc<dyn LifecycleHook<Request<Body>, Response<Body>>>),
108 {
109 for hook in hook_list {
110 register_fn(hooks, hook);
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::lifecycle::HookResult;
119 use axum::body::Body;
120 use axum::http::{Request, Response, StatusCode};
121 use std::future::Future;
122 use std::pin::Pin;
123
124 #[test]
125 fn test_error_messages() {
126 let call_err = error::call_failed("test_hook", "test reason");
127 assert!(call_err.contains("test_hook"));
128 assert!(call_err.contains("test reason"));
129
130 let task_err = error::task_error("task_hook", "spawn failed");
131 assert!(task_err.contains("task_hook"));
132
133 let promise_err = error::promise_failed("promise_hook", "rejected");
134 assert!(promise_err.contains("promise_hook"));
135 }
136
137 #[test]
138 fn test_body_error_messages() {
139 let read_err = error::body_read_failed("request", "stream closed");
140 assert!(read_err.contains("request"));
141
142 let write_err = error::body_write_failed("allocation failed");
143 assert!(write_err.contains("allocation"));
144 }
145
146 #[test]
147 fn test_json_error_messages() {
148 let ser_err = error::serialize_failed("request body", "invalid type");
149 assert!(ser_err.contains("request body"));
150
151 let deser_err = error::deserialize_failed("response", "malformed");
152 assert!(deser_err.contains("response"));
153 }
154
155 #[tokio::test]
156 async fn serial_extract_body_roundtrips_bytes() {
157 let body = Body::from("hello");
158 let bytes = serial::extract_body(body).await.expect("extract body");
159 assert_eq!(&bytes[..], b"hello");
160 }
161
162 #[test]
163 fn serial_parse_json_handles_empty_valid_and_invalid_json() {
164 let empty = serial::parse_json(&[]).expect("parse empty");
165 assert_eq!(empty, serde_json::Value::Null);
166
167 let valid = serial::parse_json(br#"{"ok":true}"#).expect("parse json");
168 assert_eq!(valid["ok"], true);
169
170 let invalid = serial::parse_json(b"not-json").expect("parse fallback");
171 assert_eq!(invalid, serde_json::Value::String("not-json".to_string()));
172 }
173
174 #[test]
175 fn hook_registry_registers_all_hooks_via_callback() {
176 struct NoopHook {
177 hook_name: String,
178 }
179
180 impl LifecycleHook<Request<Body>, Response<Body>> for NoopHook {
181 fn name(&self) -> &str {
182 &self.hook_name
183 }
184
185 fn execute_request<'a>(
186 &self,
187 req: Request<Body>,
188 ) -> Pin<Box<dyn Future<Output = Result<HookResult<Request<Body>, Response<Body>>, String>> + Send + 'a>>
189 {
190 Box::pin(async move { Ok(HookResult::Continue(req)) })
191 }
192
193 fn execute_response<'a>(
194 &self,
195 resp: Response<Body>,
196 ) -> Pin<Box<dyn Future<Output = Result<HookResult<Response<Body>, Response<Body>>, String>> + Send + 'a>>
197 {
198 Box::pin(async move { Ok(HookResult::Continue(resp)) })
199 }
200 }
201
202 let mut hooks = HttpLifecycleHooks::new();
203 assert!(hooks.is_empty());
204
205 let hook_list: Vec<Arc<dyn LifecycleHook<Request<Body>, Response<Body>>>> = vec![
206 Arc::new(NoopHook {
207 hook_name: "one".to_string(),
208 }),
209 Arc::new(NoopHook {
210 hook_name: "two".to_string(),
211 }),
212 ];
213
214 HookRegistry::register_from_list(&mut hooks, hook_list, "on_request", |hooks, hook| {
215 hooks.add_on_request(hook);
216 });
217
218 let dbg = format!("{:?}", hooks);
219 assert!(dbg.contains("on_request_count"));
220 assert!(dbg.contains("2"));
221
222 let req = Request::builder().body(Body::empty()).unwrap();
223 let result = futures::executor::block_on(hooks.execute_on_request(req)).expect("hook run");
224 assert!(matches!(result, HookResult::Continue(_)));
225
226 let resp = Response::builder().status(StatusCode::OK).body(Body::empty()).unwrap();
227 let resp = futures::executor::block_on(hooks.execute_on_response(resp)).expect("hook run");
228 assert_eq!(resp.status(), StatusCode::OK);
229 }
230}