1use proc_macro2::TokenStream;
10use quote::quote;
11use server_less_parse::{MethodInfo, ParamInfo};
12
13pub fn generate_param_extraction(param: &ParamInfo) -> TokenStream {
15 let name = ¶m.name;
16 let name_str = param.name.to_string();
17 let ty = ¶m.ty;
18
19 if param.is_optional {
20 quote! {
22 let #name: #ty = args.get(#name_str)
23 .and_then(|v| if v.is_null() { None } else {
24 ::server_less::serde_json::from_value(v.clone()).ok()
25 });
26 }
27 } else {
28 quote! {
30 let __val = args.get(#name_str)
31 .ok_or_else(|| format!("Missing required parameter: {}", #name_str))?
32 .clone();
33 let #name: #ty = ::server_less::serde_json::from_value::<#ty>(__val)
34 .map_err(|e| format!("Invalid parameter {}: {}", #name_str, e))?;
35 }
36 }
37}
38
39pub fn generate_all_param_extractions(method: &MethodInfo) -> Vec<TokenStream> {
41 method
42 .params
43 .iter()
44 .map(generate_param_extraction)
45 .collect()
46}
47
48pub fn generate_param_extractions_for(params: &[&ParamInfo]) -> Vec<TokenStream> {
53 params
54 .iter()
55 .map(|p| generate_param_extraction(p))
56 .collect()
57}
58
59pub fn generate_method_call(method: &MethodInfo, handle_async: AsyncHandling) -> TokenStream {
64 let method_name = &method.name;
65 let arg_names: Vec<_> = method.params.iter().map(|p| &p.name).collect();
66
67 match (method.is_async, handle_async) {
68 (true, AsyncHandling::Error) => {
69 quote! {
70 return Err("Async methods not supported in sync context".to_string());
71 }
72 }
73 (true, AsyncHandling::Await) => {
74 quote! {
75 let result = self.#method_name(#(#arg_names),*).await;
76 }
77 }
78 (true, AsyncHandling::BlockOn) => {
79 quote! {
80 let result = ::tokio::runtime::Runtime::new()
81 .expect("Failed to create Tokio runtime")
82 .block_on(self.#method_name(#(#arg_names),*));
83 }
84 }
85 (false, _) => {
86 quote! {
87 let result = self.#method_name(#(#arg_names),*);
88 }
89 }
90 }
91}
92
93pub fn generate_method_call_with_args(
98 method: &MethodInfo,
99 arg_exprs: Vec<TokenStream>,
100 handle_async: AsyncHandling,
101) -> TokenStream {
102 let method_name = &method.name;
103
104 match (method.is_async, handle_async) {
105 (true, AsyncHandling::Error) => {
106 quote! {
107 return Err("Async methods not supported in sync context".to_string());
108 }
109 }
110 (true, AsyncHandling::Await) => {
111 quote! {
112 let result = self.#method_name(#(#arg_exprs),*).await;
113 }
114 }
115 (true, AsyncHandling::BlockOn) => {
116 quote! {
117 let result = ::tokio::runtime::Runtime::new()
118 .expect("Failed to create Tokio runtime")
119 .block_on(self.#method_name(#(#arg_exprs),*));
120 }
121 }
122 (false, _) => {
123 quote! {
124 let result = self.#method_name(#(#arg_exprs),*);
125 }
126 }
127 }
128}
129
130#[derive(Debug, Clone, Copy)]
132pub enum AsyncHandling {
133 Error,
135 Await,
137 BlockOn,
139}
140
141pub fn generate_json_response(method: &MethodInfo) -> TokenStream {
149 let ret = &method.return_info;
150
151 if ret.is_unit {
152 quote! {
153 Ok(::server_less::serde_json::json!({"success": true}))
154 }
155 } else if ret.is_stream {
156 quote! {
158 {
159 use ::server_less::futures::StreamExt;
160 let collected: Vec<_> = result.collect().await;
161 Ok(::server_less::serde_json::to_value(collected)
162 .map_err(|e| format!("Serialization error: {}", e))?)
163 }
164 }
165 } else if ret.is_iterator {
166 quote! {
168 {
169 let __collected: Vec<_> = result.collect();
170 Ok(::server_less::serde_json::to_value(&__collected)
171 .map_err(|e| format!("Serialization error: {}", e))?)
172 }
173 }
174 } else if ret.is_result {
175 quote! {
176 match result {
177 Ok(value) => Ok(::server_less::serde_json::to_value(value)
178 .map_err(|e| format!("Serialization error: {}", e))?),
179 Err(err) => Err(format!("{:?}", err)),
180 }
181 }
182 } else if ret.is_option {
183 quote! {
184 match result {
185 Some(value) => Ok(::server_less::serde_json::to_value(value)
186 .map_err(|e| format!("Serialization error: {}", e))?),
187 None => Ok(::server_less::serde_json::Value::Null),
188 }
189 }
190 } else {
191 quote! {
193 Ok(::server_less::serde_json::to_value(result)
194 .map_err(|e| format!("Serialization error: {}", e))?)
195 }
196 }
197}
198
199pub fn generate_dispatch_arm(
203 method: &MethodInfo,
204 method_name_override: Option<&str>,
205 async_handling: AsyncHandling,
206) -> TokenStream {
207 let method_name_str = method_name_override
208 .map(String::from)
209 .unwrap_or_else(|| method.name.to_string());
210
211 let requires_async = method.is_async || method.return_info.is_stream;
213
214 if requires_async && matches!(async_handling, AsyncHandling::Error) {
216 return quote! {
217 #method_name_str => {
218 return Err("Async methods and streaming methods not supported in sync context".to_string());
219 }
220 };
221 }
222
223 let param_extractions = generate_all_param_extractions(method);
224 let call = generate_method_call(method, async_handling);
225 let response = generate_json_response(method);
226
227 quote! {
228 #method_name_str => {
229 #(#param_extractions)*
230 #call
231 #response
232 }
233 }
234}
235
236pub fn generate_dispatch_arm_with_injections(
242 method: &MethodInfo,
243 method_name_override: Option<&str>,
244 async_handling: AsyncHandling,
245 injected_params: &[(usize, TokenStream)],
246) -> TokenStream {
247 let method_name_str = method_name_override
248 .map(String::from)
249 .unwrap_or_else(|| method.name.to_string());
250
251 let requires_async = method.is_async || method.return_info.is_stream;
253
254 if requires_async && matches!(async_handling, AsyncHandling::Error) {
256 return quote! {
257 #method_name_str => {
258 return Err("Async methods and streaming methods not supported in sync context".to_string());
259 }
260 };
261 }
262
263 let param_extractions: Vec<TokenStream> = method
265 .params
266 .iter()
267 .enumerate()
268 .map(|(i, p)| {
269 if let Some((_, injection)) = injected_params.iter().find(|(idx, _)| *idx == i) {
270 let name = &p.name;
271 quote! { let #name = #injection; }
272 } else {
273 generate_param_extraction(p)
274 }
275 })
276 .collect();
277
278 let call = generate_method_call(method, async_handling);
279 let response = generate_json_response(method);
280
281 quote! {
282 #method_name_str => {
283 #(#param_extractions)*
284 #call
285 #response
286 }
287 }
288}
289
290pub fn infer_json_type(ty: &syn::Type) -> &'static str {
295 use syn::{GenericArgument, PathArguments, Type};
296 match ty {
297 Type::Path(type_path) => {
298 if let Some(segment) = type_path.path.segments.last() {
299 match segment.ident.to_string().as_str() {
300 "String" => "string",
301 "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize"
302 | "usize" => "integer",
303 "f32" | "f64" => "number",
304 "bool" => "boolean",
305 "Vec" => "array",
306 "HashMap" | "BTreeMap" | "IndexMap" => "object",
307 "Option" => {
308 if let PathArguments::AngleBracketed(args) = &segment.arguments
310 && let Some(GenericArgument::Type(inner)) = args.args.first()
311 {
312 return infer_json_type(inner);
313 }
314 "object"
315 }
316 _ => "object",
317 }
318 } else {
319 "object"
320 }
321 }
322 Type::Reference(r) => {
324 if let Type::Path(tp) = r.elem.as_ref()
325 && tp.path.is_ident("str")
326 {
327 "string"
328 } else {
329 infer_json_type(&r.elem)
330 }
331 }
332 Type::Slice(_) => "array",
333 _ => "object",
334 }
335}
336
337pub fn generate_param_schema(params: &[ParamInfo]) -> (Vec<TokenStream>, Vec<String>) {
339 let properties: Vec<_> = params
340 .iter()
341 .map(|p| {
342 let param_name = p.name.to_string();
343 let param_type = infer_json_type(&p.ty);
344 let description = p
345 .help_text
346 .clone()
347 .unwrap_or_else(|| format!("Parameter: {}", param_name));
348
349 quote! {
350 (#param_name, #param_type, #description)
351 }
352 })
353 .collect();
354
355 let required: Vec<_> = params
356 .iter()
357 .filter(|p| !p.is_optional)
358 .map(|p| p.name.to_string())
359 .collect();
360
361 (properties, required)
362}
363
364pub fn generate_param_schema_for(params: &[&ParamInfo]) -> (Vec<TokenStream>, Vec<String>) {
366 let properties: Vec<_> = params
367 .iter()
368 .map(|p| {
369 let param_name = p.name.to_string();
370 let param_type = infer_json_type(&p.ty);
371 let description = p
372 .help_text
373 .clone()
374 .unwrap_or_else(|| format!("Parameter: {}", param_name));
375
376 quote! {
377 (#param_name, #param_type, #description)
378 }
379 })
380 .collect();
381
382 let required: Vec<_> = params
383 .iter()
384 .filter(|p| !p.is_optional)
385 .map(|p| p.name.to_string())
386 .collect();
387
388 (properties, required)
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use quote::quote;
395 use syn::ImplItemFn;
396
397 fn parse_method(tokens: proc_macro2::TokenStream) -> MethodInfo {
400 let method: ImplItemFn = syn::parse2(tokens).expect("failed to parse method");
401 MethodInfo::parse(&method)
402 .expect("MethodInfo::parse failed")
403 .expect("method was skipped (no self receiver?)")
404 }
405
406 #[test]
411 fn infer_json_type_string() {
412 let ty: syn::Type = syn::parse_quote!(String);
413 assert_eq!(infer_json_type(&ty), "string");
414 }
415
416 #[test]
417 fn infer_json_type_str_ref() {
418 let ty: syn::Type = syn::parse_quote!(&str);
419 assert_eq!(infer_json_type(&ty), "string");
420 }
421
422 #[test]
423 fn infer_json_type_integers() {
424 for type_str in &[
425 "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "isize", "usize",
426 ] {
427 let ty: syn::Type =
428 syn::parse_str(type_str).unwrap_or_else(|_| panic!("parse {}", type_str));
429 assert_eq!(
430 infer_json_type(&ty),
431 "integer",
432 "expected 'integer' for {}",
433 type_str
434 );
435 }
436 }
437
438 #[test]
439 fn infer_json_type_floats() {
440 let ty_f32: syn::Type = syn::parse_quote!(f32);
441 assert_eq!(infer_json_type(&ty_f32), "number");
442
443 let ty_f64: syn::Type = syn::parse_quote!(f64);
444 assert_eq!(infer_json_type(&ty_f64), "number");
445 }
446
447 #[test]
448 fn infer_json_type_bool() {
449 let ty: syn::Type = syn::parse_quote!(bool);
450 assert_eq!(infer_json_type(&ty), "boolean");
451 }
452
453 #[test]
454 fn infer_json_type_vec() {
455 let ty: syn::Type = syn::parse_quote!(Vec<MyItem>);
457 assert_eq!(infer_json_type(&ty), "array");
458 }
459
460 #[test]
461 fn infer_json_type_vec_string_is_array() {
462 let ty: syn::Type = syn::parse_quote!(Vec<String>);
464 assert_eq!(infer_json_type(&ty), "array");
465 }
466
467 #[test]
468 fn infer_json_type_custom_struct() {
469 let ty: syn::Type = syn::parse_quote!(MyCustomStruct);
470 assert_eq!(infer_json_type(&ty), "object");
471 }
472
473 #[test]
478 fn param_schema_required_params() {
479 let method = parse_method(quote! {
480 fn greet(&self, name: String, age: u32) {}
481 });
482
483 let (properties, required) = generate_param_schema(&method.params);
484
485 assert_eq!(properties.len(), 2);
486 assert_eq!(required, vec!["name", "age"]);
487 }
488
489 #[test]
490 fn param_schema_optional_params_excluded_from_required() {
491 let method = parse_method(quote! {
492 fn search(&self, query: String, limit: Option<u32>) {}
493 });
494
495 let (properties, required) = generate_param_schema(&method.params);
496
497 assert_eq!(properties.len(), 2);
498 assert_eq!(required, vec!["query"]);
499 assert!(!required.contains(&"limit".to_string()));
500 }
501
502 #[test]
503 fn param_schema_all_optional() {
504 let method = parse_method(quote! {
505 fn list(&self, offset: Option<u32>, limit: Option<u32>) {}
506 });
507
508 let (_properties, required) = generate_param_schema(&method.params);
509 assert!(required.is_empty());
510 }
511
512 #[test]
513 fn param_schema_no_params() {
514 let method = parse_method(quote! {
515 fn ping(&self) {}
516 });
517
518 let (properties, required) = generate_param_schema(&method.params);
519 assert!(properties.is_empty());
520 assert!(required.is_empty());
521 }
522
523 #[test]
528 fn param_extraction_optional_uses_and_then() {
529 let method = parse_method(quote! {
530 fn search(&self, limit: Option<u32>) {}
531 });
532
533 let tokens = generate_param_extraction(&method.params[0]);
534 let code = tokens.to_string();
535
536 assert!(
537 code.contains("and_then"),
538 "optional param should use and_then pattern, got: {}",
539 code
540 );
541 assert!(
542 !code.contains("ok_or_else"),
543 "optional param should NOT use ok_or_else, got: {}",
544 code
545 );
546 }
547
548 #[test]
549 fn param_extraction_required_uses_ok_or_else() {
550 let method = parse_method(quote! {
551 fn greet(&self, name: String) {}
552 });
553
554 let tokens = generate_param_extraction(&method.params[0]);
555 let code = tokens.to_string();
556
557 assert!(
558 code.contains("ok_or_else"),
559 "required param should use ok_or_else pattern, got: {}",
560 code
561 );
562 assert!(
563 !code.contains("and_then"),
564 "required param should NOT use and_then, got: {}",
565 code
566 );
567 }
568
569 #[test]
570 fn param_extraction_references_correct_name() {
571 let method = parse_method(quote! {
572 fn greet(&self, user_name: String) {}
573 });
574
575 let tokens = generate_param_extraction(&method.params[0]);
576 let code = tokens.to_string();
577
578 assert!(
579 code.contains("\"user_name\""),
580 "extraction should reference param name string, got: {}",
581 code
582 );
583 }
584
585 #[test]
590 fn method_call_sync() {
591 let method = parse_method(quote! {
592 fn ping(&self) {}
593 });
594
595 let tokens = generate_method_call(&method, AsyncHandling::Error);
596 let code = tokens.to_string();
597
598 assert!(
599 code.contains("self . ping"),
600 "sync call should invoke self.ping, got: {}",
601 code
602 );
603 assert!(
604 !code.contains("await"),
605 "sync call should not contain await, got: {}",
606 code
607 );
608 }
609
610 #[test]
611 fn method_call_sync_with_args() {
612 let method = parse_method(quote! {
613 fn greet(&self, name: String, count: u32) {}
614 });
615
616 let tokens = generate_method_call(&method, AsyncHandling::Error);
617 let code = tokens.to_string();
618
619 assert!(
620 code.contains("self . greet"),
621 "should call self.greet, got: {}",
622 code
623 );
624 assert!(code.contains("name"), "should pass name arg, got: {}", code);
625 assert!(
626 code.contains("count"),
627 "should pass count arg, got: {}",
628 code
629 );
630 }
631
632 #[test]
633 fn method_call_async_error() {
634 let method = parse_method(quote! {
635 async fn fetch(&self) -> String { todo!() }
636 });
637
638 let tokens = generate_method_call(&method, AsyncHandling::Error);
639 let code = tokens.to_string();
640
641 assert!(
642 code.contains("Err") || code.contains("return"),
643 "async + Error should return an error, got: {}",
644 code
645 );
646 assert!(
647 code.contains("not supported"),
648 "error message should mention not supported, got: {}",
649 code
650 );
651 }
652
653 #[test]
654 fn method_call_async_await() {
655 let method = parse_method(quote! {
656 async fn fetch(&self) -> String { todo!() }
657 });
658
659 let tokens = generate_method_call(&method, AsyncHandling::Await);
660 let code = tokens.to_string();
661
662 assert!(
663 code.contains(". await"),
664 "async + Await should contain .await, got: {}",
665 code
666 );
667 }
668
669 #[test]
670 fn method_call_async_block_on() {
671 let method = parse_method(quote! {
672 async fn fetch(&self) -> String { todo!() }
673 });
674
675 let tokens = generate_method_call(&method, AsyncHandling::BlockOn);
676 let code = tokens.to_string();
677
678 assert!(
679 code.contains("block_on"),
680 "async + BlockOn should contain block_on, got: {}",
681 code
682 );
683 assert!(
684 code.contains("Runtime"),
685 "should reference tokio Runtime, got: {}",
686 code
687 );
688 }
689
690 #[test]
695 fn json_response_unit() {
696 let method = parse_method(quote! {
697 fn ping(&self) {}
698 });
699
700 let tokens = generate_json_response(&method);
701 let code = tokens.to_string();
702
703 assert!(
704 code.contains("success"),
705 "unit return should produce success: true, got: {}",
706 code
707 );
708 }
709
710 #[test]
711 fn json_response_result() {
712 let method = parse_method(quote! {
713 fn get(&self) -> Result<String, String> { todo!() }
714 });
715
716 let tokens = generate_json_response(&method);
717 let code = tokens.to_string();
718
719 assert!(
720 code.contains("Ok"),
721 "Result return should match Ok, got: {}",
722 code
723 );
724 assert!(
725 code.contains("Err"),
726 "Result return should match Err, got: {}",
727 code
728 );
729 }
730
731 #[test]
732 fn json_response_option() {
733 let method = parse_method(quote! {
734 fn find(&self) -> Option<String> { todo!() }
735 });
736
737 let tokens = generate_json_response(&method);
738 let code = tokens.to_string();
739
740 assert!(
741 code.contains("Some"),
742 "Option return should match Some, got: {}",
743 code
744 );
745 assert!(
746 code.contains("None"),
747 "Option return should match None, got: {}",
748 code
749 );
750 assert!(
751 code.contains("Null"),
752 "Option None should produce Null, got: {}",
753 code
754 );
755 }
756
757 #[test]
758 fn json_response_plain_type() {
759 let method = parse_method(quote! {
760 fn count(&self) -> u64 { todo!() }
761 });
762
763 let tokens = generate_json_response(&method);
764 let code = tokens.to_string();
765
766 assert!(
767 code.contains("to_value"),
768 "plain return should serialize with to_value, got: {}",
769 code
770 );
771 assert!(
773 !code.contains("match"),
774 "plain return should not have match, got: {}",
775 code
776 );
777 }
778
779 #[test]
784 fn dispatch_arm_contains_method_name_string() {
785 let method = parse_method(quote! {
786 fn greet(&self, name: String) -> String { todo!() }
787 });
788
789 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
790 let code = tokens.to_string();
791
792 assert!(
793 code.contains("\"greet\""),
794 "dispatch arm should match on method name string, got: {}",
795 code
796 );
797 }
798
799 #[test]
800 fn dispatch_arm_with_name_override() {
801 let method = parse_method(quote! {
802 fn greet(&self, name: String) -> String { todo!() }
803 });
804
805 let tokens = generate_dispatch_arm(&method, Some("say_hello"), AsyncHandling::Error);
806 let code = tokens.to_string();
807
808 assert!(
809 code.contains("\"say_hello\""),
810 "dispatch arm should use overridden name, got: {}",
811 code
812 );
813 assert!(
814 !code.contains("\"greet\""),
815 "dispatch arm should not use original name when overridden, got: {}",
816 code
817 );
818 }
819
820 #[test]
821 fn dispatch_arm_includes_param_extraction() {
822 let method = parse_method(quote! {
823 fn greet(&self, name: String) -> String { todo!() }
824 });
825
826 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
827 let code = tokens.to_string();
828
829 assert!(
831 code.contains("\"name\""),
832 "dispatch arm should extract 'name' param, got: {}",
833 code
834 );
835 }
836
837 #[test]
838 fn dispatch_arm_includes_method_call_and_response() {
839 let method = parse_method(quote! {
840 fn ping(&self) {}
841 });
842
843 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
844 let code = tokens.to_string();
845
846 assert!(
847 code.contains("self . ping"),
848 "dispatch arm should call self.ping, got: {}",
849 code
850 );
851 assert!(
852 code.contains("success"),
853 "dispatch arm for unit return should include success response, got: {}",
854 code
855 );
856 }
857
858 #[test]
859 fn dispatch_arm_async_error_returns_early() {
860 let method = parse_method(quote! {
861 async fn fetch(&self) -> String { todo!() }
862 });
863
864 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
865 let code = tokens.to_string();
866
867 assert!(
868 code.contains("not supported"),
869 "async dispatch with Error handling should return error, got: {}",
870 code
871 );
872 }
873
874 #[test]
875 fn dispatch_arm_async_await() {
876 let method = parse_method(quote! {
877 async fn fetch(&self, url: String) -> Result<String, String> { todo!() }
878 });
879
880 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Await);
881 let code = tokens.to_string();
882
883 assert!(
884 code.contains(". await"),
885 "async dispatch with Await should contain .await, got: {}",
886 code
887 );
888 assert!(
889 code.contains("\"url\""),
890 "should extract url param, got: {}",
891 code
892 );
893 }
894
895 #[test]
900 fn dispatch_arm_with_injections_replaces_injected_param() {
901 let method = parse_method(quote! {
902 fn handle(&self, ctx: Context, name: String) -> String { todo!() }
903 });
904
905 let injection = quote! { __ctx.clone() };
906 let tokens = generate_dispatch_arm_with_injections(
907 &method,
908 None,
909 AsyncHandling::Error,
910 &[(0, injection)],
911 );
912 let code = tokens.to_string();
913
914 assert!(
916 code.contains("__ctx"),
917 "injected param should use provided expression, got: {}",
918 code
919 );
920 assert!(
922 code.contains("\"name\""),
923 "non-injected param should be extracted from JSON, got: {}",
924 code
925 );
926 }
927
928 #[test]
933 fn all_param_extractions_generates_one_per_param() {
934 let method = parse_method(quote! {
935 fn create(&self, name: String, value: i32, label: Option<String>) {}
936 });
937
938 let extractions = generate_all_param_extractions(&method);
939 assert_eq!(
940 extractions.len(),
941 3,
942 "should generate one extraction per param"
943 );
944 }
945
946 #[test]
951 fn param_extractions_for_subset() {
952 let method = parse_method(quote! {
953 fn handle(&self, ctx: Context, name: String, age: u32) {}
954 });
955
956 let subset: Vec<&ParamInfo> = method.params.iter().skip(1).collect();
958 let extractions = generate_param_extractions_for(&subset);
959 assert_eq!(extractions.len(), 2);
960
961 let code = extractions
962 .iter()
963 .map(|t| t.to_string())
964 .collect::<String>();
965 assert!(
966 !code.contains("\"ctx\""),
967 "should not extract ctx, got: {}",
968 code
969 );
970 assert!(
971 code.contains("\"name\""),
972 "should extract name, got: {}",
973 code
974 );
975 }
976
977 #[test]
982 fn method_call_with_custom_args() {
983 let method = parse_method(quote! {
984 fn handle(&self, ctx: Context, name: String) -> String { todo!() }
985 });
986
987 let args = vec![quote! { __ctx }, quote! { name }];
988 let tokens = generate_method_call_with_args(&method, args, AsyncHandling::Error);
989 let code = tokens.to_string();
990
991 assert!(
992 code.contains("__ctx"),
993 "should pass custom arg expression, got: {}",
994 code
995 );
996 assert!(
997 code.contains("self . handle"),
998 "should call self.handle, got: {}",
999 code
1000 );
1001 }
1002
1003 #[test]
1008 fn param_schema_for_subset() {
1009 let method = parse_method(quote! {
1010 fn handle(&self, ctx: Context, name: String, limit: Option<u32>) {}
1011 });
1012
1013 let subset: Vec<&ParamInfo> = method.params.iter().skip(1).collect();
1014 let (properties, required) = generate_param_schema_for(&subset);
1015
1016 assert_eq!(properties.len(), 2);
1017 assert_eq!(required, vec!["name"]);
1018 assert!(!required.contains(&"ctx".to_string()));
1019 }
1020
1021 #[test]
1026 fn dispatch_arm_no_params_unit_return() {
1027 let method = parse_method(quote! {
1028 fn health_check(&self) {}
1029 });
1030
1031 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
1032 let code = tokens.to_string();
1033
1034 assert!(
1035 code.contains("\"health_check\""),
1036 "should match on method name, got: {}",
1037 code
1038 );
1039 assert!(
1040 code.contains("success"),
1041 "unit return should produce success, got: {}",
1042 code
1043 );
1044 }
1045
1046 #[test]
1047 fn infer_json_type_option_string_is_string() {
1048 let ty: syn::Type = syn::parse_quote!(Option<String>);
1050 assert_eq!(infer_json_type(&ty), "string");
1051 }
1052
1053 #[test]
1054 fn infer_json_type_vec_u8_is_array() {
1055 let ty: syn::Type = syn::parse_quote!(Vec<u8>);
1057 assert_eq!(infer_json_type(&ty), "array");
1058 }
1059
1060 #[test]
1061 fn infer_json_type_hashmap_is_object() {
1062 let ty: syn::Type = syn::parse_quote!(HashMap<String, i32>);
1063 assert_eq!(infer_json_type(&ty), "object");
1064 }
1065
1066 #[test]
1067 fn method_call_sync_ignores_async_handling_variant() {
1068 let method = parse_method(quote! {
1070 fn ping(&self) {}
1071 });
1072
1073 let code_error = generate_method_call(&method, AsyncHandling::Error).to_string();
1074 let code_await = generate_method_call(&method, AsyncHandling::Await).to_string();
1075 let code_block = generate_method_call(&method, AsyncHandling::BlockOn).to_string();
1076
1077 assert_eq!(code_error, code_await);
1078 assert_eq!(code_await, code_block);
1079 }
1080}