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#[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#[proc_macro_attribute]
33pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
34 item
37}
38
39#[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
68fn bean_impl_gen(item: ItemImpl) -> Result<proc_macro2::TokenStream, syn::Error> {
70 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 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 let match_arms: Vec<_> = handlers
92 .iter()
93 .map(|handler| {
94 let method_name = &handler.name;
95
96 let (param_extraction, method_call) = generate_handler_invocation(handler)?;
98
99 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 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
142fn 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 let mut params = Vec::new();
150 let mut extraction = Vec::new();
151
152 if let Some(body_type) = &handler.body_type {
154 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 if handler.has_headers {
202 extraction.push(quote! {
203 let headers = exchange.input.headers.clone();
204 });
205 params.push(quote! { headers });
206 }
207
208 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
219fn generate_result_handling(
221 handler: &handler::HandlerMethod,
222) -> Result<proc_macro2::TokenStream, syn::Error> {
223 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 if handler.has_exchange && handler.body_type.is_none() {
233 return Ok(quote! {
235 result.map_err(|e| ::camel_api::CamelError::ProcessorError(e.to_string()))?;
236 Ok(())
237 });
238 }
239
240 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 #[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 #[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 #[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 #[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 #[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 #[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}