1mod handler;
2
3use handler::find_handler_methods;
4use proc_macro::TokenStream;
5use quote::quote;
6use syn::{ItemImpl, parse_macro_input};
7
8#[doc(hidden)]
18#[proc_macro_derive(Bean)]
19pub fn derive_bean(_input: TokenStream) -> TokenStream {
20 quote! {
21 compile_error!("Bean derive macro is not yet implemented. Use #[bean_impl] on an impl block instead.");
22 }
23 .into()
24}
25
26#[proc_macro_attribute]
30pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
31 item
34}
35
36#[proc_macro_attribute]
56pub fn bean_impl(_attr: TokenStream, item: TokenStream) -> TokenStream {
57 let input = parse_macro_input!(item as ItemImpl);
58
59 match bean_impl_gen(input) {
60 Ok(tokens) => tokens.into(),
61 Err(err) => err.to_compile_error().into(),
62 }
63}
64
65fn bean_impl_gen(item: ItemImpl) -> Result<proc_macro2::TokenStream, syn::Error> {
67 if !item.generics.params.is_empty() {
69 return Err(syn::Error::new_spanned(
70 &item.generics,
71 "bean_impl does not support generic types or lifetimes on the impl block",
72 ));
73 }
74
75 let self_ty = &item.self_ty;
76
77 let handlers = find_handler_methods(&item.items)?;
79
80 if handlers.is_empty() {
81 return Err(syn::Error::new_spanned(
82 self_ty,
83 "No #[handler] methods found in impl block",
84 ));
85 }
86
87 let match_arms: Vec<_> = handlers
89 .iter()
90 .map(|handler| {
91 let method_name = &handler.name;
92
93 let (param_extraction, method_call) = generate_handler_invocation(handler)?;
95
96 let result_handling = generate_result_handling(handler)?;
98
99 Ok(quote! {
100 #method_name => {
101 #param_extraction
102 let result = #method_call;
103 #result_handling
104 }
105 })
106 })
107 .collect::<Result<Vec<_>, syn::Error>>()?;
108
109 let method_names: Vec<_> = handlers.iter().map(|h| h.name.as_str()).collect();
111
112 let expanded = quote! {
113 #item
114
115 #[::camel_bean::async_trait]
116 impl ::camel_bean::BeanProcessor for #self_ty {
117 async fn call(
118 &self,
119 method: &str,
120 exchange: &mut ::camel_api::Exchange,
121 ) -> Result<(), ::camel_api::CamelError> {
122 match method {
123 #(#match_arms)*
124 _ => Err(::camel_api::CamelError::ProcessorError(
125 format!("Method '{}' not found", method)
126 ))
127 }
128 }
129
130 fn methods(&self) -> Vec<String> {
131 vec![#(#method_names.to_string()),*]
132 }
133 }
134 };
135
136 Ok(expanded)
137}
138
139fn generate_handler_invocation(
141 handler: &handler::HandlerMethod,
142) -> Result<(proc_macro2::TokenStream, proc_macro2::TokenStream), syn::Error> {
143 let method_ident = &handler.ident;
144
145 let mut params = Vec::new();
147 let mut extraction = Vec::new();
148
149 if let Some(body_type) = &handler.body_type {
151 let is_string_type = match body_type {
156 syn::Type::Path(type_path) => type_path
157 .path
158 .segments
159 .last()
160 .map(|seg| seg.ident == "String")
161 .unwrap_or(false),
162 _ => false,
163 };
164
165 if is_string_type {
166 extraction.push(quote! {
167 let body: #body_type = match &exchange.input.body {
168 ::camel_api::Body::Json(value) => {
169 ::serde_json::from_value(value.clone())
170 .map_err(|e| ::camel_api::CamelError::TypeConversionFailed(
171 format!("Failed to deserialize body: {}", e)
172 ))?
173 }
174 ::camel_api::Body::Text(s) => s.clone(),
175 other => return Err(::camel_api::CamelError::TypeConversionFailed(
176 format!("Expected JSON or text body, got {:?}", other)
177 )),
178 };
179 });
180 } else {
181 extraction.push(quote! {
182 let body_json = match &exchange.input.body {
183 ::camel_api::Body::Json(value) => value.clone(),
184 other => return Err(::camel_api::CamelError::TypeConversionFailed(
185 format!("Expected JSON body, got {:?}", other)
186 )),
187 };
188 let body: #body_type = ::serde_json::from_value(body_json)
189 .map_err(|e| ::camel_api::CamelError::TypeConversionFailed(
190 format!("Failed to deserialize body: {}", e)
191 ))?;
192 });
193 }
194 params.push(quote! { body });
195 }
196
197 if handler.has_headers {
199 extraction.push(quote! {
200 let headers = exchange.input.headers.clone();
201 });
202 params.push(quote! { headers });
203 }
204
205 if handler.has_exchange {
207 params.push(quote! { exchange });
208 }
209
210 let extraction_code = quote! { #(#extraction)* };
211 let method_call = quote! { self.#method_ident(#(#params),*).await };
212
213 Ok((extraction_code, method_call))
214}
215
216fn generate_result_handling(
218 handler: &handler::HandlerMethod,
219) -> Result<proc_macro2::TokenStream, syn::Error> {
220 if handler.return_type.is_none() {
222 return Ok(quote! {
223 result.map_err(|e| ::camel_api::CamelError::ProcessorError(e.to_string()))?;
224 Ok(())
225 });
226 }
227
228 if handler.has_exchange && handler.body_type.is_none() {
230 return Ok(quote! {
232 result.map_err(|e| ::camel_api::CamelError::ProcessorError(e.to_string()))?;
233 Ok(())
234 });
235 }
236
237 Ok(quote! {
240 let value = result.map_err(|e| ::camel_api::CamelError::ProcessorError(e.to_string()))?;
241 exchange.input.body = ::camel_api::Body::Json(::serde_json::to_value(value)
242 .map_err(|e| ::camel_api::CamelError::TypeConversionFailed(
243 format!("Failed to serialize result: {}", e)
244 ))?);
245 Ok(())
246 })
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use syn::parse_quote;
253
254 #[test]
256 fn test_bean_impl_gen_simple_valid() {
257 let item: ItemImpl = parse_quote! {
258 impl MyService {
259 #[handler]
260 pub async fn process(&self, body: String) -> Result<String, String> {
261 Ok(body)
262 }
263 }
264 };
265 let result = bean_impl_gen(item);
266 assert!(
267 result.is_ok(),
268 "bean_impl_gen should succeed for valid input"
269 );
270 let tokens = result.unwrap().to_string();
271 assert!(
272 tokens.contains("BeanProcessor"),
273 "should generate BeanProcessor impl"
274 );
275 assert!(
276 tokens.contains("process"),
277 "should contain handler method name"
278 );
279 }
280
281 #[test]
283 fn test_bean_impl_gen_no_handlers() {
284 let item: ItemImpl = parse_quote! {
285 impl MyService {
286 pub async fn process(&self) {}
287 }
288 };
289 let result = bean_impl_gen(item);
290 assert!(
291 result.is_err(),
292 "bean_impl_gen should fail when no handlers found"
293 );
294 let err = result.unwrap_err();
295 assert!(
296 err.to_string().contains("No #[handler] methods found"),
297 "error should mention missing handlers, got: {}",
298 err
299 );
300 }
301
302 #[test]
304 fn test_bean_impl_gen_rejects_generics() {
305 let item: ItemImpl = parse_quote! {
306 impl<T> GenericService<T> {
307 #[handler]
308 pub async fn process(&self, body: T) -> Result<T, String> {
309 Ok(body)
310 }
311 }
312 };
313 let result = bean_impl_gen(item);
314 assert!(
315 result.is_err(),
316 "bean_impl_gen should reject generic impl blocks"
317 );
318 let err = result.unwrap_err();
319 assert!(
320 err.to_string().contains("generic"),
321 "error should mention generics, got: {}",
322 err
323 );
324 }
325
326 #[test]
328 fn test_bean_impl_gen_rejects_lifetimes() {
329 let item: ItemImpl = parse_quote! {
330 impl<'a> Service<'a> {
331 #[handler]
332 pub async fn process(&self) -> Result<(), String> {
333 Ok(())
334 }
335 }
336 };
337 let result = bean_impl_gen(item);
338 assert!(
339 result.is_err(),
340 "bean_impl_gen should reject lifetime impl blocks"
341 );
342 let err = result.unwrap_err();
343 assert!(
344 err.to_string().contains("generic") || err.to_string().contains("lifetime"),
345 "error should mention generics/lifetimes, got: {}",
346 err
347 );
348 }
349
350 #[test]
352 fn test_bean_impl_gen_multiple_handlers() {
353 let item: ItemImpl = parse_quote! {
354 impl MyService {
355 #[handler]
356 pub async fn process(&self, body: String) -> Result<String, String> {
357 Ok(body)
358 }
359 #[handler]
360 pub async fn validate(&self, body: String) -> Result<bool, String> {
361 Ok(true)
362 }
363 }
364 };
365 let result = bean_impl_gen(item);
366 assert!(
367 result.is_ok(),
368 "bean_impl_gen should succeed with multiple handlers"
369 );
370 let tokens = result.unwrap().to_string();
371 assert!(tokens.contains("process"), "should contain first handler");
372 assert!(tokens.contains("validate"), "should contain second handler");
373 }
374
375 #[test]
379 fn test_derive_bean_logic_emits_compile_error() {
380 let tokens: proc_macro2::TokenStream = quote! {
381 compile_error!("Bean derive macro is not yet implemented. Use #[bean_impl] on an impl block instead.");
382 };
383 let token_str = tokens.to_string();
384 assert!(
385 token_str.contains("compile_error"),
386 "derive_bean logic should emit compile_error!, got: {}",
387 token_str
388 );
389 }
390}