Skip to main content

camel_bean_macros/
lib.rs

1mod handler;
2
3#[cfg(test)]
4mod expansion_baselines;
5
6use handler::find_handler_methods;
7use proc_macro::TokenStream;
8use quote::quote;
9use syn::{ItemImpl, parse_macro_input};
10
11/// Derive macro for `Bean` — **not yet implemented**.
12///
13/// This macro is a work-in-progress placeholder. It emits a compile-time error
14/// directing users to use `#[bean_impl]` on an impl block instead.
15///
16/// # Status
17///
18/// This derive is `#[doc(hidden)]` because it is non-functional. It will be
19/// fully implemented in a future release. Use [`bean_impl`] instead.
20#[doc(hidden)]
21#[proc_macro_derive(Bean)]
22pub fn derive_bean(_input: TokenStream) -> TokenStream {
23    quote! {
24        compile_error!("Bean derive macro is not yet implemented. Use #[bean_impl] on an impl block instead.");
25    }
26    .into()
27}
28
29/// Marker attribute for handler methods
30/// This attribute does not transform the method - it's detected by `#[bean_impl]`
31/// to identify which methods should be exposed as bean handlers
32#[proc_macro_attribute]
33pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
34    // Pass through the method unchanged
35    // `#[bean_impl]` will detect this attribute
36    item
37}
38
39/// Attribute macro for generating BeanProcessor implementation from an impl block
40///
41/// # Example
42///
43/// ```ignore
44/// use camel_bean::bean_impl;
45/// use camel_bean::handler;
46///
47/// struct OrderService;
48///
49/// #[bean_impl]
50/// impl OrderService {
51///     #[handler]
52///     pub async fn process(&self, body: Order) -> Result<ProcessedOrder, String> {
53///         // Process order
54///         Ok(ProcessedOrder { ... })
55///     }
56/// }
57/// ```
58#[proc_macro_attribute]
59pub fn bean_impl(_attr: TokenStream, item: TokenStream) -> TokenStream {
60    let input = parse_macro_input!(item as ItemImpl);
61
62    match bean_impl_gen(input) {
63        Ok(tokens) => tokens.into(),
64        Err(err) => err.to_compile_error().into(),
65    }
66}
67
68/// Generate BeanProcessor implementation from an impl block
69fn bean_impl_gen(item: ItemImpl) -> Result<proc_macro2::TokenStream, syn::Error> {
70    // BEAN-MACROS-003: Reject generic impl blocks at compile time
71    if !item.generics.params.is_empty() {
72        return Err(syn::Error::new_spanned(
73            &item.generics,
74            "bean_impl does not support generic types or lifetimes on the impl block",
75        ));
76    }
77
78    let self_ty = &item.self_ty;
79
80    // Find handler methods
81    let handlers = find_handler_methods(&item.items)?;
82
83    if handlers.is_empty() {
84        return Err(syn::Error::new_spanned(
85            self_ty,
86            "No #[handler] methods found in impl block",
87        ));
88    }
89
90    // Generate match arms for each handler
91    let match_arms: Vec<_> = handlers
92        .iter()
93        .map(|handler| {
94            let method_name = &handler.name;
95
96            // Generate parameter extraction and method call
97            let (param_extraction, method_call) = generate_handler_invocation(handler)?;
98
99            // Generate result handling
100            let result_handling = generate_result_handling(handler)?;
101
102            Ok(quote! {
103                #method_name => {
104                    #param_extraction
105                    let result = #method_call;
106                    #result_handling
107                }
108            })
109        })
110        .collect::<Result<Vec<_>, syn::Error>>()?;
111
112    // Generate method names list
113    let method_names: Vec<_> = handlers.iter().map(|h| h.name.as_str()).collect();
114
115    let expanded = quote! {
116        #item
117
118        #[::camel_bean::async_trait]
119        impl ::camel_bean::BeanProcessor for #self_ty {
120            async fn call(
121                &self,
122                method: &str,
123                exchange: &mut ::camel_api::Exchange,
124            ) -> Result<(), ::camel_api::CamelError> {
125                match method {
126                    #(#match_arms)*
127                    _ => Err(::camel_api::CamelError::ProcessorError(
128                        format!("Method '{}' not found", method)
129                    ))
130                }
131            }
132
133            fn methods(&self) -> Vec<String> {
134                vec![#(#method_names.to_string()),*]
135            }
136        }
137    };
138
139    Ok(expanded)
140}
141
142/// Generate parameter extraction code for a handler method
143fn generate_handler_invocation(
144    handler: &handler::HandlerMethod,
145) -> Result<(proc_macro2::TokenStream, proc_macro2::TokenStream), syn::Error> {
146    let method_ident = &handler.ident;
147
148    // Build parameter list
149    let mut params = Vec::new();
150    let mut extraction = Vec::new();
151
152    // Body parameter
153    if let Some(body_type) = &handler.body_type {
154        // TODO(BEAN-MACROS-004): Body extraction is currently JSON-only.
155        // We should add broader type coercion (e.g., from raw bytes, plain text, etc.)
156        // and a proper trait-based extraction strategy. For now, we provide a basic
157        // String fallback for String-typed parameters.
158        let is_string_type = match body_type {
159            syn::Type::Path(type_path) => type_path
160                .path
161                .segments
162                .last()
163                .map(|seg| seg.ident == "String")
164                .unwrap_or(false),
165            _ => false,
166        };
167
168        if is_string_type {
169            extraction.push(quote! {
170                let body: #body_type = match &exchange.input.body {
171                    ::camel_api::Body::Json(value) => {
172                        ::serde_json::from_value(value.clone())
173                            .map_err(|e| ::camel_api::CamelError::TypeConversionFailed(
174                                format!("Failed to deserialize body: {}", e)
175                            ))?
176                    }
177                    ::camel_api::Body::Text(s) => s.clone(),
178                    other => return Err(::camel_api::CamelError::TypeConversionFailed(
179                        format!("Expected JSON or text body, got {:?}", other)
180                    )),
181                };
182            });
183        } else {
184            extraction.push(quote! {
185                let body_json = match &exchange.input.body {
186                    ::camel_api::Body::Json(value) => value.clone(),
187                    other => return Err(::camel_api::CamelError::TypeConversionFailed(
188                        format!("Expected JSON body, got {:?}", other)
189                    )),
190                };
191                let body: #body_type = ::serde_json::from_value(body_json)
192                    .map_err(|e| ::camel_api::CamelError::TypeConversionFailed(
193                        format!("Failed to deserialize body: {}", e)
194                    ))?;
195            });
196        }
197        params.push(quote! { body });
198    }
199
200    // Headers parameter
201    if handler.has_headers {
202        extraction.push(quote! {
203            let headers = exchange.input.headers.clone();
204        });
205        params.push(quote! { headers });
206    }
207
208    // Exchange parameter
209    if handler.has_exchange {
210        params.push(quote! { exchange });
211    }
212
213    let extraction_code = quote! { #(#extraction)* };
214    let method_call = quote! { self.#method_ident(#(#params),*).await };
215
216    Ok((extraction_code, method_call))
217}
218
219/// Generate result handling code based on return type
220fn generate_result_handling(
221    handler: &handler::HandlerMethod,
222) -> Result<proc_macro2::TokenStream, syn::Error> {
223    // If no return type or exchange-only handler, don't set body
224    if handler.return_type.is_none() {
225        return Ok(quote! {
226            result.map_err(|e| ::camel_api::CamelError::ProcessorError(e.to_string()))?;
227            Ok(())
228        });
229    }
230
231    // Check if this is an exchange-only handler (has exchange but no body type)
232    if handler.has_exchange && handler.body_type.is_none() {
233        // Exchange handlers manage the exchange themselves, just propagate errors
234        return Ok(quote! {
235            result.map_err(|e| ::camel_api::CamelError::ProcessorError(e.to_string()))?;
236            Ok(())
237        });
238    }
239
240    // For handlers with return values, extract the value and set it as body
241    // We need to handle Result<T> return types
242    Ok(quote! {
243        let value = result.map_err(|e| ::camel_api::CamelError::ProcessorError(e.to_string()))?;
244        exchange.input.body = ::camel_api::Body::Json(::serde_json::to_value(value)
245            .map_err(|e| ::camel_api::CamelError::TypeConversionFailed(
246                format!("Failed to serialize result: {}", e)
247            ))?);
248        Ok(())
249    })
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use syn::parse_quote;
256
257    /// BEAN-MACROS-005: Verify bean_impl_gen succeeds for a valid simple impl block
258    #[test]
259    fn test_bean_impl_gen_simple_valid() {
260        let item: ItemImpl = parse_quote! {
261            impl MyService {
262                #[handler]
263                pub async fn process(&self, body: String) -> Result<String, String> {
264                    Ok(body)
265                }
266            }
267        };
268        let result = bean_impl_gen(item);
269        assert!(
270            result.is_ok(),
271            "bean_impl_gen should succeed for valid input"
272        );
273        let tokens = result.unwrap().to_string();
274        assert!(
275            tokens.contains("BeanProcessor"),
276            "should generate BeanProcessor impl"
277        );
278        assert!(
279            tokens.contains("process"),
280            "should contain handler method name"
281        );
282    }
283
284    /// BEAN-MACROS-005: Verify bean_impl_gen rejects impl blocks with no #[handler] methods
285    #[test]
286    fn test_bean_impl_gen_no_handlers() {
287        let item: ItemImpl = parse_quote! {
288            impl MyService {
289                pub async fn process(&self) {}
290            }
291        };
292        let result = bean_impl_gen(item);
293        assert!(
294            result.is_err(),
295            "bean_impl_gen should fail when no handlers found"
296        );
297        let err = result.unwrap_err();
298        assert!(
299            err.to_string().contains("No #[handler] methods found"),
300            "error should mention missing handlers, got: {}",
301            err
302        );
303    }
304
305    /// BEAN-MACROS-003/005: Verify bean_impl_gen rejects generic impl blocks
306    #[test]
307    fn test_bean_impl_gen_rejects_generics() {
308        let item: ItemImpl = parse_quote! {
309            impl<T> GenericService<T> {
310                #[handler]
311                pub async fn process(&self, body: T) -> Result<T, String> {
312                    Ok(body)
313                }
314            }
315        };
316        let result = bean_impl_gen(item);
317        assert!(
318            result.is_err(),
319            "bean_impl_gen should reject generic impl blocks"
320        );
321        let err = result.unwrap_err();
322        assert!(
323            err.to_string().contains("generic"),
324            "error should mention generics, got: {}",
325            err
326        );
327    }
328
329    /// BEAN-MACROS-003/005: Verify bean_impl_gen rejects impl blocks with lifetimes
330    #[test]
331    fn test_bean_impl_gen_rejects_lifetimes() {
332        let item: ItemImpl = parse_quote! {
333            impl<'a> Service<'a> {
334                #[handler]
335                pub async fn process(&self) -> Result<(), String> {
336                    Ok(())
337                }
338            }
339        };
340        let result = bean_impl_gen(item);
341        assert!(
342            result.is_err(),
343            "bean_impl_gen should reject lifetime impl blocks"
344        );
345        let err = result.unwrap_err();
346        assert!(
347            err.to_string().contains("generic") || err.to_string().contains("lifetime"),
348            "error should mention generics/lifetimes, got: {}",
349            err
350        );
351    }
352
353    /// BEAN-MACROS-005: Verify bean_impl_gen works with multiple handlers
354    #[test]
355    fn test_bean_impl_gen_multiple_handlers() {
356        let item: ItemImpl = parse_quote! {
357            impl MyService {
358                #[handler]
359                pub async fn process(&self, body: String) -> Result<String, String> {
360                    Ok(body)
361                }
362                #[handler]
363                pub async fn validate(&self, body: String) -> Result<bool, String> {
364                    Ok(true)
365                }
366            }
367        };
368        let result = bean_impl_gen(item);
369        assert!(
370            result.is_ok(),
371            "bean_impl_gen should succeed with multiple handlers"
372        );
373        let tokens = result.unwrap().to_string();
374        assert!(tokens.contains("process"), "should contain first handler");
375        assert!(tokens.contains("validate"), "should contain second handler");
376    }
377
378    /// BEAN-MACROS-005: Verify derive_bean logic produces compile_error
379    /// (We test the internal quote logic since the proc_macro entry point
380    /// cannot be called outside of a proc-macro invocation context.)
381    #[test]
382    fn test_derive_bean_logic_emits_compile_error() {
383        let tokens: proc_macro2::TokenStream = quote! {
384            compile_error!("Bean derive macro is not yet implemented. Use #[bean_impl] on an impl block instead.");
385        };
386        let token_str = tokens.to_string();
387        assert!(
388            token_str.contains("compile_error"),
389            "derive_bean logic should emit compile_error!, got: {}",
390            token_str
391        );
392    }
393}