Skip to main content

camel_bean_macros/
lib.rs

1mod handler;
2
3use handler::find_handler_methods;
4use proc_macro::TokenStream;
5use quote::quote;
6use syn::{ItemImpl, parse_macro_input};
7
8/// Derive macro for `Bean` — **not yet implemented**.
9///
10/// This macro is a work-in-progress placeholder. It emits a compile-time error
11/// directing users to use `#[bean_impl]` on an impl block instead.
12///
13/// # Status
14///
15/// This derive is `#[doc(hidden)]` because it is non-functional. It will be
16/// fully implemented in a future release. Use [`bean_impl`] instead.
17#[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/// Marker attribute for handler methods
27/// This attribute does not transform the method - it's detected by the Bean derive macro
28/// to identify which methods should be exposed as bean handlers
29#[proc_macro_attribute]
30pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
31    // Pass through the method unchanged
32    // The Bean derive macro (or bean_impl attribute in Task 2.2) will detect this attribute
33    item
34}
35
36/// Attribute macro for generating BeanProcessor implementation from an impl block
37///
38/// # Example
39///
40/// ```ignore
41/// use camel_bean::bean_impl;
42/// use camel_bean::handler;
43///
44/// struct OrderService;
45///
46/// #[bean_impl]
47/// impl OrderService {
48///     #[handler]
49///     pub async fn process(&self, body: Order) -> Result<ProcessedOrder, String> {
50///         // Process order
51///         Ok(ProcessedOrder { ... })
52///     }
53/// }
54/// ```
55#[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
65/// Generate BeanProcessor implementation from an impl block
66fn bean_impl_gen(item: ItemImpl) -> Result<proc_macro2::TokenStream, syn::Error> {
67    // BEAN-MACROS-003: Reject generic impl blocks at compile time
68    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    // Find handler methods
78    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    // Generate match arms for each handler
88    let match_arms: Vec<_> = handlers
89        .iter()
90        .map(|handler| {
91            let method_name = &handler.name;
92
93            // Generate parameter extraction and method call
94            let (param_extraction, method_call) = generate_handler_invocation(handler)?;
95
96            // Generate result handling
97            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    // Generate method names list
110    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
139/// Generate parameter extraction code for a handler method
140fn 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    // Build parameter list
146    let mut params = Vec::new();
147    let mut extraction = Vec::new();
148
149    // Body parameter
150    if let Some(body_type) = &handler.body_type {
151        // TODO(BEAN-MACROS-004): Body extraction is currently JSON-only.
152        // We should add broader type coercion (e.g., from raw bytes, plain text, etc.)
153        // and a proper trait-based extraction strategy. For now, we provide a basic
154        // String fallback for String-typed parameters.
155        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    // Headers parameter
198    if handler.has_headers {
199        extraction.push(quote! {
200            let headers = exchange.input.headers.clone();
201        });
202        params.push(quote! { headers });
203    }
204
205    // Exchange parameter
206    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
216/// Generate result handling code based on return type
217fn generate_result_handling(
218    handler: &handler::HandlerMethod,
219) -> Result<proc_macro2::TokenStream, syn::Error> {
220    // If no return type or exchange-only handler, don't set body
221    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    // Check if this is an exchange-only handler (has exchange but no body type)
229    if handler.has_exchange && handler.body_type.is_none() {
230        // Exchange handlers manage the exchange themselves, just propagate errors
231        return Ok(quote! {
232            result.map_err(|e| ::camel_api::CamelError::ProcessorError(e.to_string()))?;
233            Ok(())
234        });
235    }
236
237    // For handlers with return values, extract the value and set it as body
238    // We need to handle Result<T> return types
239    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    /// BEAN-MACROS-005: Verify bean_impl_gen succeeds for a valid simple impl block
255    #[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    /// BEAN-MACROS-005: Verify bean_impl_gen rejects impl blocks with no #[handler] methods
282    #[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    /// BEAN-MACROS-003/005: Verify bean_impl_gen rejects generic impl blocks
303    #[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    /// BEAN-MACROS-003/005: Verify bean_impl_gen rejects impl blocks with lifetimes
327    #[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    /// BEAN-MACROS-005: Verify bean_impl_gen works with multiple handlers
351    #[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    /// BEAN-MACROS-005: Verify derive_bean logic produces compile_error
376    /// (We test the internal quote logic since the proc_macro entry point
377    /// cannot be called outside of a proc-macro invocation context.)
378    #[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}