1use proc_macro2::TokenStream;
15use quote::quote;
16use server_less_parse::{MethodInfo, ParamInfo};
17
18pub fn generate_param_extraction(param: &ParamInfo) -> TokenStream {
20 let name = ¶m.name;
21 let name_str = param.name_str();
22 let ty = ¶m.ty;
23
24 if param.is_optional {
25 let inner_ty: syn::Type = if let syn::Type::Path(ref type_path) = *ty {
27 if let Some(seg) = type_path.path.segments.last() {
28 if seg.ident == "Option" {
29 if let syn::PathArguments::AngleBracketed(ref args) = seg.arguments {
30 if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
31 inner.clone()
32 } else {
33 ty.clone()
34 }
35 } else {
36 ty.clone()
37 }
38 } else {
39 ty.clone()
40 }
41 } else {
42 ty.clone()
43 }
44 } else {
45 ty.clone()
46 };
47 let inner_ty_str = quote!(#inner_ty).to_string().replace(" ", "");
48 quote! {
52 let #name: #ty = match args.get(#name_str) {
53 None => None,
54 Some(__v) if __v.is_null() => None,
55 Some(__v) => match ::server_less::serde_json::from_value(__v.clone()) {
56 Ok(__val) => Some(__val),
57 Err(__e) => return Err(format!(
58 "Optional parameter '{}' has invalid type (expected {}): {}", #name_str, #inner_ty_str, __e
59 )),
60 },
61 };
62 }
63 } else {
64 let ty_str = quote!(#ty).to_string().replace(" ", "");
65 quote! {
67 let __val = args.get(#name_str)
68 .ok_or_else(|| format!("Missing required parameter: {} (expected {})", #name_str, #ty_str))?
69 .clone();
70 let #name: #ty = ::server_less::serde_json::from_value::<#ty>(__val)
71 .map_err(|e| format!("Invalid parameter {} (expected {}): {}", #name_str, #ty_str, e))?;
72 }
73 }
74}
75
76pub fn generate_all_param_extractions(method: &MethodInfo) -> Vec<TokenStream> {
78 method
79 .params
80 .iter()
81 .map(generate_param_extraction)
82 .collect()
83}
84
85pub fn generate_param_extractions_for(params: &[&ParamInfo]) -> Vec<TokenStream> {
90 params
91 .iter()
92 .map(|p| generate_param_extraction(p))
93 .collect()
94}
95
96pub fn generate_unknown_param_warning(
103 method_name_str: &str,
104 params: &[&ParamInfo],
105) -> TokenStream {
106 let known: Vec<String> = params.iter().map(|p| p.name_str()).collect();
107 let expected_display = known.join(", ");
108 if known.is_empty() {
109 quote! {
111 if let Some(__obj) = args.as_object() {
112 for __key in __obj.keys() {
113 eprintln!(
114 "[server-less] warning: unknown parameter `{}` in call to `{}` (expected: {})",
115 __key, #method_name_str, #expected_display,
116 );
117 }
118 }
119 }
120 } else {
121 quote! {
122 if let Some(__obj) = args.as_object() {
123 for __key in __obj.keys() {
124 match __key.as_str() {
125 #(#known)|* => {}
126 __unknown => {
127 eprintln!(
128 "[server-less] warning: unknown parameter `{}` in call to `{}` (expected: {})",
129 __unknown, #method_name_str, #expected_display,
130 );
131 }
132 }
133 }
134 }
135 }
136 }
137}
138
139pub fn generate_method_call(method: &MethodInfo, handle_async: AsyncHandling) -> TokenStream {
144 let method_name = &method.name;
145 let arg_names: Vec<_> = method.params.iter().map(|p| &p.name).collect();
146
147 match (method.is_async, handle_async) {
148 (true, AsyncHandling::Error) => {
149 quote! {
150 return Err("Async methods not supported in sync context".to_string());
151 }
152 }
153 (true, AsyncHandling::Await) => {
154 quote! {
155 let result = self.#method_name(#(#arg_names),*).await;
156 }
157 }
158 (true, AsyncHandling::BlockOn) => {
159 quote! {
160 let result = ::tokio::runtime::Runtime::new()
161 .expect("Failed to create Tokio runtime")
162 .block_on(self.#method_name(#(#arg_names),*));
163 }
164 }
165 (false, _) => {
166 quote! {
167 let result = self.#method_name(#(#arg_names),*);
168 }
169 }
170 }
171}
172
173pub fn generate_method_call_with_args(
178 method: &MethodInfo,
179 arg_exprs: Vec<TokenStream>,
180 handle_async: AsyncHandling,
181) -> TokenStream {
182 let method_name = &method.name;
183
184 match (method.is_async, handle_async) {
185 (true, AsyncHandling::Error) => {
186 quote! {
187 return Err("Async methods not supported in sync context".to_string());
188 }
189 }
190 (true, AsyncHandling::Await) => {
191 quote! {
192 let result = self.#method_name(#(#arg_exprs),*).await;
193 }
194 }
195 (true, AsyncHandling::BlockOn) => {
196 quote! {
197 let result = ::tokio::runtime::Runtime::new()
198 .expect("Failed to create Tokio runtime")
199 .block_on(self.#method_name(#(#arg_exprs),*));
200 }
201 }
202 (false, _) => {
203 quote! {
204 let result = self.#method_name(#(#arg_exprs),*);
205 }
206 }
207 }
208}
209
210#[derive(Debug, Clone, Copy)]
212pub enum AsyncHandling {
213 Error,
215 Await,
217 BlockOn,
219}
220
221pub fn generate_json_response(method: &MethodInfo) -> TokenStream {
229 let ret = &method.return_info;
230
231 if ret.is_unit {
232 quote! {
233 Ok(::server_less::serde_json::json!({"success": true}))
234 }
235 } else if ret.is_stream {
236 quote! {
238 {
239 use ::server_less::futures::StreamExt;
240 let collected: Vec<_> = result.collect().await;
241 Ok(::server_less::serde_json::to_value(collected)
242 .map_err(|e| format!("Serialization error: {}", e))?)
243 }
244 }
245 } else if ret.is_iterator {
246 quote! {
248 {
249 let __collected: Vec<_> = result.collect();
250 Ok(::server_less::serde_json::to_value(&__collected)
251 .map_err(|e| format!("Serialization error: {}", e))?)
252 }
253 }
254 } else if ret.is_result {
255 quote! {
256 match result {
257 Ok(value) => Ok(::server_less::serde_json::to_value(value)
258 .map_err(|e| format!("Serialization error: {}", e))?),
259 Err(err) => Err(format!("{:?}", err)),
260 }
261 }
262 } else if ret.is_option {
263 quote! {
264 match result {
265 Some(value) => Ok(::server_less::serde_json::to_value(value)
266 .map_err(|e| format!("Serialization error: {}", e))?),
267 None => Ok(::server_less::serde_json::Value::Null),
268 }
269 }
270 } else {
271 quote! {
273 Ok(::server_less::serde_json::to_value(result)
274 .map_err(|e| format!("Serialization error: {}", e))?)
275 }
276 }
277}
278
279pub fn generate_dispatch_arm(
283 method: &MethodInfo,
284 method_name_override: Option<&str>,
285 async_handling: AsyncHandling,
286) -> TokenStream {
287 let method_name_str = method_name_override
288 .map(String::from)
289 .unwrap_or_else(|| method.name.to_string());
290
291 let requires_async = method.is_async || method.return_info.is_stream;
293
294 if requires_async && matches!(async_handling, AsyncHandling::Error) {
296 return quote! {
297 #method_name_str => {
298 return Err("Async methods and streaming methods not supported in sync context".to_string());
299 }
300 };
301 }
302
303 let all_param_refs: Vec<&ParamInfo> = method.params.iter().collect();
304 let unknown_warn = generate_unknown_param_warning(&method_name_str, &all_param_refs);
305 let param_extractions = generate_all_param_extractions(method);
306 let call = generate_method_call(method, async_handling);
307 let response = generate_json_response(method);
308
309 quote! {
310 #method_name_str => {
311 #unknown_warn
312 #(#param_extractions)*
313 #call
314 #response
315 }
316 }
317}
318
319pub fn generate_dispatch_arm_with_injections(
325 method: &MethodInfo,
326 method_name_override: Option<&str>,
327 async_handling: AsyncHandling,
328 injected_params: &[(usize, TokenStream)],
329) -> TokenStream {
330 let method_name_str = method_name_override
331 .map(String::from)
332 .unwrap_or_else(|| method.name.to_string());
333
334 let requires_async = method.is_async || method.return_info.is_stream;
336
337 if requires_async && matches!(async_handling, AsyncHandling::Error) {
339 return quote! {
340 #method_name_str => {
341 return Err("Async methods and streaming methods not supported in sync context".to_string());
342 }
343 };
344 }
345
346 let param_extractions: Vec<TokenStream> = method
348 .params
349 .iter()
350 .enumerate()
351 .map(|(i, p)| {
352 if let Some((_, injection)) = injected_params.iter().find(|(idx, _)| *idx == i) {
353 let name = &p.name;
354 quote! { let #name = #injection; }
355 } else {
356 generate_param_extraction(p)
357 }
358 })
359 .collect();
360
361 let injected_indices: std::collections::HashSet<usize> =
363 injected_params.iter().map(|(i, _)| *i).collect();
364 let json_param_refs: Vec<&ParamInfo> = method
365 .params
366 .iter()
367 .enumerate()
368 .filter_map(|(i, p)| {
369 if injected_indices.contains(&i) {
370 None
371 } else {
372 Some(p)
373 }
374 })
375 .collect();
376 let unknown_warn = generate_unknown_param_warning(&method_name_str, &json_param_refs);
377
378 let call = generate_method_call(method, async_handling);
379 let response = generate_json_response(method);
380
381 quote! {
382 #method_name_str => {
383 #unknown_warn
384 #(#param_extractions)*
385 #call
386 #response
387 }
388 }
389}
390
391pub fn infer_json_type(ty: &syn::Type) -> &'static str {
396 use syn::{GenericArgument, PathArguments, Type};
397 match ty {
398 Type::Path(type_path) => {
399 if let Some(segment) = type_path.path.segments.last() {
400 match segment.ident.to_string().as_str() {
401 "String" => "string",
402 "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize"
403 | "usize" => "integer",
404 "f32" | "f64" => "number",
405 "bool" => "boolean",
406 "Vec" => "array",
407 "HashMap" | "BTreeMap" | "IndexMap" => "object",
408 "Option" => {
409 if let PathArguments::AngleBracketed(args) = &segment.arguments
411 && let Some(GenericArgument::Type(inner)) = args.args.first()
412 {
413 return infer_json_type(inner);
414 }
415 "object"
416 }
417 _ => "object",
418 }
419 } else {
420 "object"
421 }
422 }
423 Type::Reference(r) => {
425 if let Type::Path(tp) = r.elem.as_ref()
426 && tp.path.is_ident("str")
427 {
428 "string"
429 } else {
430 infer_json_type(&r.elem)
431 }
432 }
433 Type::Slice(_) => "array",
434 _ => "object",
435 }
436}
437
438pub fn generate_param_schema(params: &[ParamInfo]) -> (Vec<TokenStream>, Vec<String>) {
440 let properties: Vec<_> = params
441 .iter()
442 .map(|p| {
443 let param_name = p.name_str();
444 let param_type = infer_json_type(&p.ty);
445 let description = p
446 .help_text
447 .clone()
448 .unwrap_or_else(|| format!("Parameter: {}", param_name));
449
450 quote! {
451 (#param_name, #param_type, #description)
452 }
453 })
454 .collect();
455
456 let required: Vec<_> = params
457 .iter()
458 .filter(|p| !p.is_optional)
459 .map(|p| p.name_str())
460 .collect();
461
462 (properties, required)
463}
464
465pub fn generate_param_schema_for(params: &[&ParamInfo]) -> (Vec<TokenStream>, Vec<String>) {
467 let properties: Vec<_> = params
468 .iter()
469 .map(|p| {
470 let param_name = p.name_str();
471 let param_type = infer_json_type(&p.ty);
472 let description = p
473 .help_text
474 .clone()
475 .unwrap_or_else(|| format!("Parameter: {}", param_name));
476
477 quote! {
478 (#param_name, #param_type, #description)
479 }
480 })
481 .collect();
482
483 let required: Vec<_> = params
484 .iter()
485 .filter(|p| !p.is_optional)
486 .map(|p| p.name_str())
487 .collect();
488
489 (properties, required)
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use quote::quote;
496 use syn::ImplItemFn;
497
498 fn parse_method(tokens: proc_macro2::TokenStream) -> MethodInfo {
501 let method: ImplItemFn = syn::parse2(tokens).expect("failed to parse method");
502 MethodInfo::parse(&method)
503 .expect("MethodInfo::parse failed")
504 .expect("method was skipped (no self receiver?)")
505 }
506
507 #[test]
512 fn infer_json_type_string() {
513 let ty: syn::Type = syn::parse_quote!(String);
514 assert_eq!(infer_json_type(&ty), "string");
515 }
516
517 #[test]
518 fn infer_json_type_str_ref() {
519 let ty: syn::Type = syn::parse_quote!(&str);
520 assert_eq!(infer_json_type(&ty), "string");
521 }
522
523 #[test]
524 fn infer_json_type_integers() {
525 for type_str in &[
526 "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "isize", "usize",
527 ] {
528 let ty: syn::Type =
529 syn::parse_str(type_str).unwrap_or_else(|_| panic!("parse {}", type_str));
530 assert_eq!(
531 infer_json_type(&ty),
532 "integer",
533 "expected 'integer' for {}",
534 type_str
535 );
536 }
537 }
538
539 #[test]
540 fn infer_json_type_floats() {
541 let ty_f32: syn::Type = syn::parse_quote!(f32);
542 assert_eq!(infer_json_type(&ty_f32), "number");
543
544 let ty_f64: syn::Type = syn::parse_quote!(f64);
545 assert_eq!(infer_json_type(&ty_f64), "number");
546 }
547
548 #[test]
549 fn infer_json_type_bool() {
550 let ty: syn::Type = syn::parse_quote!(bool);
551 assert_eq!(infer_json_type(&ty), "boolean");
552 }
553
554 #[test]
555 fn infer_json_type_vec() {
556 let ty: syn::Type = syn::parse_quote!(Vec<MyItem>);
558 assert_eq!(infer_json_type(&ty), "array");
559 }
560
561 #[test]
562 fn infer_json_type_vec_string_is_array() {
563 let ty: syn::Type = syn::parse_quote!(Vec<String>);
565 assert_eq!(infer_json_type(&ty), "array");
566 }
567
568 #[test]
569 fn infer_json_type_custom_struct() {
570 let ty: syn::Type = syn::parse_quote!(MyCustomStruct);
571 assert_eq!(infer_json_type(&ty), "object");
572 }
573
574 #[test]
579 fn param_schema_required_params() {
580 let method = parse_method(quote! {
581 fn greet(&self, name: String, age: u32) {}
582 });
583
584 let (properties, required) = generate_param_schema(&method.params);
585
586 assert_eq!(properties.len(), 2);
587 assert_eq!(required, vec!["name", "age"]);
588 }
589
590 #[test]
591 fn param_schema_optional_params_excluded_from_required() {
592 let method = parse_method(quote! {
593 fn search(&self, query: String, limit: Option<u32>) {}
594 });
595
596 let (properties, required) = generate_param_schema(&method.params);
597
598 assert_eq!(properties.len(), 2);
599 assert_eq!(required, vec!["query"]);
600 assert!(!required.contains(&"limit".to_string()));
601 }
602
603 #[test]
604 fn param_schema_all_optional() {
605 let method = parse_method(quote! {
606 fn list(&self, offset: Option<u32>, limit: Option<u32>) {}
607 });
608
609 let (_properties, required) = generate_param_schema(&method.params);
610 assert!(required.is_empty());
611 }
612
613 #[test]
614 fn param_schema_no_params() {
615 let method = parse_method(quote! {
616 fn ping(&self) {}
617 });
618
619 let (properties, required) = generate_param_schema(&method.params);
620 assert!(properties.is_empty());
621 assert!(required.is_empty());
622 }
623
624 #[test]
629 fn param_extraction_optional_uses_match() {
630 let method = parse_method(quote! {
631 fn search(&self, limit: Option<u32>) {}
632 });
633
634 let tokens = generate_param_extraction(&method.params[0]);
635 let code = tokens.to_string();
636
637 assert!(
639 code.contains("is_null"),
640 "optional param should handle null, got: {}",
641 code
642 );
643 assert!(
644 code.contains("Optional parameter"),
645 "optional param should return an error for wrong type, got: {}",
646 code
647 );
648 assert!(
649 !code.contains("ok_or_else"),
650 "optional param should NOT use ok_or_else, got: {}",
651 code
652 );
653 }
654
655 #[test]
656 fn param_extraction_required_uses_ok_or_else() {
657 let method = parse_method(quote! {
658 fn greet(&self, name: String) {}
659 });
660
661 let tokens = generate_param_extraction(&method.params[0]);
662 let code = tokens.to_string();
663
664 assert!(
665 code.contains("ok_or_else"),
666 "required param should use ok_or_else pattern, got: {}",
667 code
668 );
669 assert!(
670 !code.contains("and_then"),
671 "required param should NOT use and_then, got: {}",
672 code
673 );
674 }
675
676 #[test]
677 fn param_extraction_references_correct_name() {
678 let method = parse_method(quote! {
679 fn greet(&self, user_name: String) {}
680 });
681
682 let tokens = generate_param_extraction(&method.params[0]);
683 let code = tokens.to_string();
684
685 assert!(
686 code.contains("\"user_name\""),
687 "extraction should reference param name string, got: {}",
688 code
689 );
690 }
691
692 #[test]
697 fn method_call_sync() {
698 let method = parse_method(quote! {
699 fn ping(&self) {}
700 });
701
702 let tokens = generate_method_call(&method, AsyncHandling::Error);
703 let code = tokens.to_string();
704
705 assert!(
706 code.contains("self . ping"),
707 "sync call should invoke self.ping, got: {}",
708 code
709 );
710 assert!(
711 !code.contains("await"),
712 "sync call should not contain await, got: {}",
713 code
714 );
715 }
716
717 #[test]
718 fn method_call_sync_with_args() {
719 let method = parse_method(quote! {
720 fn greet(&self, name: String, count: u32) {}
721 });
722
723 let tokens = generate_method_call(&method, AsyncHandling::Error);
724 let code = tokens.to_string();
725
726 assert!(
727 code.contains("self . greet"),
728 "should call self.greet, got: {}",
729 code
730 );
731 assert!(code.contains("name"), "should pass name arg, got: {}", code);
732 assert!(
733 code.contains("count"),
734 "should pass count arg, got: {}",
735 code
736 );
737 }
738
739 #[test]
740 fn method_call_async_error() {
741 let method = parse_method(quote! {
742 async fn fetch(&self) -> String { todo!() }
743 });
744
745 let tokens = generate_method_call(&method, AsyncHandling::Error);
746 let code = tokens.to_string();
747
748 assert!(
749 code.contains("Err") || code.contains("return"),
750 "async + Error should return an error, got: {}",
751 code
752 );
753 assert!(
754 code.contains("not supported"),
755 "error message should mention not supported, got: {}",
756 code
757 );
758 }
759
760 #[test]
761 fn method_call_async_await() {
762 let method = parse_method(quote! {
763 async fn fetch(&self) -> String { todo!() }
764 });
765
766 let tokens = generate_method_call(&method, AsyncHandling::Await);
767 let code = tokens.to_string();
768
769 assert!(
770 code.contains(". await"),
771 "async + Await should contain .await, got: {}",
772 code
773 );
774 }
775
776 #[test]
777 fn method_call_async_block_on() {
778 let method = parse_method(quote! {
779 async fn fetch(&self) -> String { todo!() }
780 });
781
782 let tokens = generate_method_call(&method, AsyncHandling::BlockOn);
783 let code = tokens.to_string();
784
785 assert!(
786 code.contains("block_on"),
787 "async + BlockOn should contain block_on, got: {}",
788 code
789 );
790 assert!(
791 code.contains("Runtime"),
792 "should reference tokio Runtime, got: {}",
793 code
794 );
795 }
796
797 #[test]
802 fn json_response_unit() {
803 let method = parse_method(quote! {
804 fn ping(&self) {}
805 });
806
807 let tokens = generate_json_response(&method);
808 let code = tokens.to_string();
809
810 assert!(
811 code.contains("success"),
812 "unit return should produce success: true, got: {}",
813 code
814 );
815 }
816
817 #[test]
818 fn json_response_result() {
819 let method = parse_method(quote! {
820 fn get(&self) -> Result<String, String> { todo!() }
821 });
822
823 let tokens = generate_json_response(&method);
824 let code = tokens.to_string();
825
826 assert!(
827 code.contains("Ok"),
828 "Result return should match Ok, got: {}",
829 code
830 );
831 assert!(
832 code.contains("Err"),
833 "Result return should match Err, got: {}",
834 code
835 );
836 }
837
838 #[test]
839 fn json_response_option() {
840 let method = parse_method(quote! {
841 fn find(&self) -> Option<String> { todo!() }
842 });
843
844 let tokens = generate_json_response(&method);
845 let code = tokens.to_string();
846
847 assert!(
848 code.contains("Some"),
849 "Option return should match Some, got: {}",
850 code
851 );
852 assert!(
853 code.contains("None"),
854 "Option return should match None, got: {}",
855 code
856 );
857 assert!(
858 code.contains("Null"),
859 "Option None should produce Null, got: {}",
860 code
861 );
862 }
863
864 #[test]
865 fn json_response_plain_type() {
866 let method = parse_method(quote! {
867 fn count(&self) -> u64 { todo!() }
868 });
869
870 let tokens = generate_json_response(&method);
871 let code = tokens.to_string();
872
873 assert!(
874 code.contains("to_value"),
875 "plain return should serialize with to_value, got: {}",
876 code
877 );
878 assert!(
880 !code.contains("match"),
881 "plain return should not have match, got: {}",
882 code
883 );
884 }
885
886 #[test]
891 fn dispatch_arm_contains_method_name_string() {
892 let method = parse_method(quote! {
893 fn greet(&self, name: String) -> String { todo!() }
894 });
895
896 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
897 let code = tokens.to_string();
898
899 assert!(
900 code.contains("\"greet\""),
901 "dispatch arm should match on method name string, got: {}",
902 code
903 );
904 }
905
906 #[test]
907 fn dispatch_arm_with_name_override() {
908 let method = parse_method(quote! {
909 fn greet(&self, name: String) -> String { todo!() }
910 });
911
912 let tokens = generate_dispatch_arm(&method, Some("say_hello"), AsyncHandling::Error);
913 let code = tokens.to_string();
914
915 assert!(
916 code.contains("\"say_hello\""),
917 "dispatch arm should use overridden name, got: {}",
918 code
919 );
920 assert!(
921 !code.contains("\"greet\""),
922 "dispatch arm should not use original name when overridden, got: {}",
923 code
924 );
925 }
926
927 #[test]
928 fn dispatch_arm_includes_param_extraction() {
929 let method = parse_method(quote! {
930 fn greet(&self, name: String) -> String { todo!() }
931 });
932
933 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
934 let code = tokens.to_string();
935
936 assert!(
938 code.contains("\"name\""),
939 "dispatch arm should extract 'name' param, got: {}",
940 code
941 );
942 }
943
944 #[test]
945 fn dispatch_arm_includes_method_call_and_response() {
946 let method = parse_method(quote! {
947 fn ping(&self) {}
948 });
949
950 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
951 let code = tokens.to_string();
952
953 assert!(
954 code.contains("self . ping"),
955 "dispatch arm should call self.ping, got: {}",
956 code
957 );
958 assert!(
959 code.contains("success"),
960 "dispatch arm for unit return should include success response, got: {}",
961 code
962 );
963 }
964
965 #[test]
966 fn dispatch_arm_async_error_returns_early() {
967 let method = parse_method(quote! {
968 async fn fetch(&self) -> String { todo!() }
969 });
970
971 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
972 let code = tokens.to_string();
973
974 assert!(
975 code.contains("not supported"),
976 "async dispatch with Error handling should return error, got: {}",
977 code
978 );
979 }
980
981 #[test]
982 fn dispatch_arm_async_await() {
983 let method = parse_method(quote! {
984 async fn fetch(&self, url: String) -> Result<String, String> { todo!() }
985 });
986
987 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Await);
988 let code = tokens.to_string();
989
990 assert!(
991 code.contains(". await"),
992 "async dispatch with Await should contain .await, got: {}",
993 code
994 );
995 assert!(
996 code.contains("\"url\""),
997 "should extract url param, got: {}",
998 code
999 );
1000 }
1001
1002 #[test]
1007 fn dispatch_arm_with_injections_replaces_injected_param() {
1008 let method = parse_method(quote! {
1009 fn handle(&self, ctx: Context, name: String) -> String { todo!() }
1010 });
1011
1012 let injection = quote! { __ctx.clone() };
1013 let tokens = generate_dispatch_arm_with_injections(
1014 &method,
1015 None,
1016 AsyncHandling::Error,
1017 &[(0, injection)],
1018 );
1019 let code = tokens.to_string();
1020
1021 assert!(
1023 code.contains("__ctx"),
1024 "injected param should use provided expression, got: {}",
1025 code
1026 );
1027 assert!(
1029 code.contains("\"name\""),
1030 "non-injected param should be extracted from JSON, got: {}",
1031 code
1032 );
1033 }
1034
1035 #[test]
1040 fn all_param_extractions_generates_one_per_param() {
1041 let method = parse_method(quote! {
1042 fn create(&self, name: String, value: i32, label: Option<String>) {}
1043 });
1044
1045 let extractions = generate_all_param_extractions(&method);
1046 assert_eq!(
1047 extractions.len(),
1048 3,
1049 "should generate one extraction per param"
1050 );
1051 }
1052
1053 #[test]
1058 fn param_extractions_for_subset() {
1059 let method = parse_method(quote! {
1060 fn handle(&self, ctx: Context, name: String, age: u32) {}
1061 });
1062
1063 let subset: Vec<&ParamInfo> = method.params.iter().skip(1).collect();
1065 let extractions = generate_param_extractions_for(&subset);
1066 assert_eq!(extractions.len(), 2);
1067
1068 let code = extractions
1069 .iter()
1070 .map(|t| t.to_string())
1071 .collect::<String>();
1072 assert!(
1073 !code.contains("\"ctx\""),
1074 "should not extract ctx, got: {}",
1075 code
1076 );
1077 assert!(
1078 code.contains("\"name\""),
1079 "should extract name, got: {}",
1080 code
1081 );
1082 }
1083
1084 #[test]
1089 fn method_call_with_custom_args() {
1090 let method = parse_method(quote! {
1091 fn handle(&self, ctx: Context, name: String) -> String { todo!() }
1092 });
1093
1094 let args = vec![quote! { __ctx }, quote! { name }];
1095 let tokens = generate_method_call_with_args(&method, args, AsyncHandling::Error);
1096 let code = tokens.to_string();
1097
1098 assert!(
1099 code.contains("__ctx"),
1100 "should pass custom arg expression, got: {}",
1101 code
1102 );
1103 assert!(
1104 code.contains("self . handle"),
1105 "should call self.handle, got: {}",
1106 code
1107 );
1108 }
1109
1110 #[test]
1115 fn param_schema_for_subset() {
1116 let method = parse_method(quote! {
1117 fn handle(&self, ctx: Context, name: String, limit: Option<u32>) {}
1118 });
1119
1120 let subset: Vec<&ParamInfo> = method.params.iter().skip(1).collect();
1121 let (properties, required) = generate_param_schema_for(&subset);
1122
1123 assert_eq!(properties.len(), 2);
1124 assert_eq!(required, vec!["name"]);
1125 assert!(!required.contains(&"ctx".to_string()));
1126 }
1127
1128 #[test]
1133 fn dispatch_arm_no_params_unit_return() {
1134 let method = parse_method(quote! {
1135 fn health_check(&self) {}
1136 });
1137
1138 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
1139 let code = tokens.to_string();
1140
1141 assert!(
1142 code.contains("\"health_check\""),
1143 "should match on method name, got: {}",
1144 code
1145 );
1146 assert!(
1147 code.contains("success"),
1148 "unit return should produce success, got: {}",
1149 code
1150 );
1151 }
1152
1153 #[test]
1154 fn infer_json_type_option_string_is_string() {
1155 let ty: syn::Type = syn::parse_quote!(Option<String>);
1157 assert_eq!(infer_json_type(&ty), "string");
1158 }
1159
1160 #[test]
1161 fn infer_json_type_vec_u8_is_array() {
1162 let ty: syn::Type = syn::parse_quote!(Vec<u8>);
1164 assert_eq!(infer_json_type(&ty), "array");
1165 }
1166
1167 #[test]
1168 fn infer_json_type_hashmap_is_object() {
1169 let ty: syn::Type = syn::parse_quote!(HashMap<String, i32>);
1170 assert_eq!(infer_json_type(&ty), "object");
1171 }
1172
1173 #[test]
1174 fn method_call_sync_ignores_async_handling_variant() {
1175 let method = parse_method(quote! {
1177 fn ping(&self) {}
1178 });
1179
1180 let code_error = generate_method_call(&method, AsyncHandling::Error).to_string();
1181 let code_await = generate_method_call(&method, AsyncHandling::Await).to_string();
1182 let code_block = generate_method_call(&method, AsyncHandling::BlockOn).to_string();
1183
1184 assert_eq!(code_error, code_await);
1185 assert_eq!(code_await, code_block);
1186 }
1187}