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_result {
166 quote! {
167 match result {
168 Ok(value) => Ok(::server_less::serde_json::to_value(value)
169 .map_err(|e| format!("Serialization error: {}", e))?),
170 Err(err) => Err(format!("{:?}", err)),
171 }
172 }
173 } else if ret.is_option {
174 quote! {
175 match result {
176 Some(value) => Ok(::server_less::serde_json::to_value(value)
177 .map_err(|e| format!("Serialization error: {}", e))?),
178 None => Ok(::server_less::serde_json::Value::Null),
179 }
180 }
181 } else {
182 quote! {
184 Ok(::server_less::serde_json::to_value(result)
185 .map_err(|e| format!("Serialization error: {}", e))?)
186 }
187 }
188}
189
190pub fn generate_dispatch_arm(
194 method: &MethodInfo,
195 method_name_override: Option<&str>,
196 async_handling: AsyncHandling,
197) -> TokenStream {
198 let method_name_str = method_name_override
199 .map(String::from)
200 .unwrap_or_else(|| method.name.to_string());
201
202 let requires_async = method.is_async || method.return_info.is_stream;
204
205 if requires_async && matches!(async_handling, AsyncHandling::Error) {
207 return quote! {
208 #method_name_str => {
209 return Err("Async methods and streaming methods not supported in sync context".to_string());
210 }
211 };
212 }
213
214 let param_extractions = generate_all_param_extractions(method);
215 let call = generate_method_call(method, async_handling);
216 let response = generate_json_response(method);
217
218 quote! {
219 #method_name_str => {
220 #(#param_extractions)*
221 #call
222 #response
223 }
224 }
225}
226
227pub fn generate_dispatch_arm_with_injections(
233 method: &MethodInfo,
234 method_name_override: Option<&str>,
235 async_handling: AsyncHandling,
236 injected_params: &[(usize, TokenStream)],
237) -> TokenStream {
238 let method_name_str = method_name_override
239 .map(String::from)
240 .unwrap_or_else(|| method.name.to_string());
241
242 let requires_async = method.is_async || method.return_info.is_stream;
244
245 if requires_async && matches!(async_handling, AsyncHandling::Error) {
247 return quote! {
248 #method_name_str => {
249 return Err("Async methods and streaming methods not supported in sync context".to_string());
250 }
251 };
252 }
253
254 let param_extractions: Vec<TokenStream> = method
256 .params
257 .iter()
258 .enumerate()
259 .map(|(i, p)| {
260 if let Some((_, injection)) = injected_params.iter().find(|(idx, _)| *idx == i) {
261 let name = &p.name;
262 quote! { let #name = #injection; }
263 } else {
264 generate_param_extraction(p)
265 }
266 })
267 .collect();
268
269 let call = generate_method_call(method, async_handling);
270 let response = generate_json_response(method);
271
272 quote! {
273 #method_name_str => {
274 #(#param_extractions)*
275 #call
276 #response
277 }
278 }
279}
280
281pub fn infer_json_type(ty: &syn::Type) -> &'static str {
283 let ty_str = quote!(#ty).to_string();
284
285 if ty_str.contains("String") || ty_str.contains("str") {
286 "string"
287 } else if ty_str.contains("i8")
288 || ty_str.contains("i16")
289 || ty_str.contains("i32")
290 || ty_str.contains("i64")
291 || ty_str.contains("u8")
292 || ty_str.contains("u16")
293 || ty_str.contains("u32")
294 || ty_str.contains("u64")
295 || ty_str.contains("isize")
296 || ty_str.contains("usize")
297 {
298 "integer"
299 } else if ty_str.contains("f32") || ty_str.contains("f64") {
300 "number"
301 } else if ty_str.contains("bool") {
302 "boolean"
303 } else if ty_str.contains("Vec") || ty_str.contains("[]") {
304 "array"
305 } else {
306 "object"
307 }
308}
309
310pub fn generate_param_schema(params: &[ParamInfo]) -> (Vec<TokenStream>, Vec<String>) {
312 let properties: Vec<_> = params
313 .iter()
314 .map(|p| {
315 let param_name = p.name.to_string();
316 let param_type = infer_json_type(&p.ty);
317 let description = format!("Parameter: {}", param_name);
318
319 quote! {
320 (#param_name, #param_type, #description)
321 }
322 })
323 .collect();
324
325 let required: Vec<_> = params
326 .iter()
327 .filter(|p| !p.is_optional)
328 .map(|p| p.name.to_string())
329 .collect();
330
331 (properties, required)
332}
333
334pub fn generate_param_schema_for(params: &[&ParamInfo]) -> (Vec<TokenStream>, Vec<String>) {
336 let properties: Vec<_> = params
337 .iter()
338 .map(|p| {
339 let param_name = p.name.to_string();
340 let param_type = infer_json_type(&p.ty);
341 let description = format!("Parameter: {}", param_name);
342
343 quote! {
344 (#param_name, #param_type, #description)
345 }
346 })
347 .collect();
348
349 let required: Vec<_> = params
350 .iter()
351 .filter(|p| !p.is_optional)
352 .map(|p| p.name.to_string())
353 .collect();
354
355 (properties, required)
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use quote::quote;
362 use syn::ImplItemFn;
363
364 fn parse_method(tokens: proc_macro2::TokenStream) -> MethodInfo {
367 let method: ImplItemFn = syn::parse2(tokens).expect("failed to parse method");
368 MethodInfo::parse(&method)
369 .expect("MethodInfo::parse failed")
370 .expect("method was skipped (no self receiver?)")
371 }
372
373 #[test]
378 fn infer_json_type_string() {
379 let ty: syn::Type = syn::parse_quote!(String);
380 assert_eq!(infer_json_type(&ty), "string");
381 }
382
383 #[test]
384 fn infer_json_type_str_ref() {
385 let ty: syn::Type = syn::parse_quote!(&str);
386 assert_eq!(infer_json_type(&ty), "string");
387 }
388
389 #[test]
390 fn infer_json_type_integers() {
391 for type_str in &[
392 "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "isize", "usize",
393 ] {
394 let ty: syn::Type =
395 syn::parse_str(type_str).unwrap_or_else(|_| panic!("parse {}", type_str));
396 assert_eq!(
397 infer_json_type(&ty),
398 "integer",
399 "expected 'integer' for {}",
400 type_str
401 );
402 }
403 }
404
405 #[test]
406 fn infer_json_type_floats() {
407 let ty_f32: syn::Type = syn::parse_quote!(f32);
408 assert_eq!(infer_json_type(&ty_f32), "number");
409
410 let ty_f64: syn::Type = syn::parse_quote!(f64);
411 assert_eq!(infer_json_type(&ty_f64), "number");
412 }
413
414 #[test]
415 fn infer_json_type_bool() {
416 let ty: syn::Type = syn::parse_quote!(bool);
417 assert_eq!(infer_json_type(&ty), "boolean");
418 }
419
420 #[test]
421 fn infer_json_type_vec() {
422 let ty: syn::Type = syn::parse_quote!(Vec<MyItem>);
424 assert_eq!(infer_json_type(&ty), "array");
425 }
426
427 #[test]
428 fn infer_json_type_vec_string_matches_string_first() {
429 let ty: syn::Type = syn::parse_quote!(Vec<String>);
433 assert_eq!(infer_json_type(&ty), "string");
434 }
435
436 #[test]
437 fn infer_json_type_custom_struct() {
438 let ty: syn::Type = syn::parse_quote!(MyCustomStruct);
439 assert_eq!(infer_json_type(&ty), "object");
440 }
441
442 #[test]
447 fn param_schema_required_params() {
448 let method = parse_method(quote! {
449 fn greet(&self, name: String, age: u32) {}
450 });
451
452 let (properties, required) = generate_param_schema(&method.params);
453
454 assert_eq!(properties.len(), 2);
455 assert_eq!(required, vec!["name", "age"]);
456 }
457
458 #[test]
459 fn param_schema_optional_params_excluded_from_required() {
460 let method = parse_method(quote! {
461 fn search(&self, query: String, limit: Option<u32>) {}
462 });
463
464 let (properties, required) = generate_param_schema(&method.params);
465
466 assert_eq!(properties.len(), 2);
467 assert_eq!(required, vec!["query"]);
468 assert!(!required.contains(&"limit".to_string()));
469 }
470
471 #[test]
472 fn param_schema_all_optional() {
473 let method = parse_method(quote! {
474 fn list(&self, offset: Option<u32>, limit: Option<u32>) {}
475 });
476
477 let (_properties, required) = generate_param_schema(&method.params);
478 assert!(required.is_empty());
479 }
480
481 #[test]
482 fn param_schema_no_params() {
483 let method = parse_method(quote! {
484 fn ping(&self) {}
485 });
486
487 let (properties, required) = generate_param_schema(&method.params);
488 assert!(properties.is_empty());
489 assert!(required.is_empty());
490 }
491
492 #[test]
497 fn param_extraction_optional_uses_and_then() {
498 let method = parse_method(quote! {
499 fn search(&self, limit: Option<u32>) {}
500 });
501
502 let tokens = generate_param_extraction(&method.params[0]);
503 let code = tokens.to_string();
504
505 assert!(
506 code.contains("and_then"),
507 "optional param should use and_then pattern, got: {}",
508 code
509 );
510 assert!(
511 !code.contains("ok_or_else"),
512 "optional param should NOT use ok_or_else, got: {}",
513 code
514 );
515 }
516
517 #[test]
518 fn param_extraction_required_uses_ok_or_else() {
519 let method = parse_method(quote! {
520 fn greet(&self, name: String) {}
521 });
522
523 let tokens = generate_param_extraction(&method.params[0]);
524 let code = tokens.to_string();
525
526 assert!(
527 code.contains("ok_or_else"),
528 "required param should use ok_or_else pattern, got: {}",
529 code
530 );
531 assert!(
532 !code.contains("and_then"),
533 "required param should NOT use and_then, got: {}",
534 code
535 );
536 }
537
538 #[test]
539 fn param_extraction_references_correct_name() {
540 let method = parse_method(quote! {
541 fn greet(&self, user_name: String) {}
542 });
543
544 let tokens = generate_param_extraction(&method.params[0]);
545 let code = tokens.to_string();
546
547 assert!(
548 code.contains("\"user_name\""),
549 "extraction should reference param name string, got: {}",
550 code
551 );
552 }
553
554 #[test]
559 fn method_call_sync() {
560 let method = parse_method(quote! {
561 fn ping(&self) {}
562 });
563
564 let tokens = generate_method_call(&method, AsyncHandling::Error);
565 let code = tokens.to_string();
566
567 assert!(
568 code.contains("self . ping"),
569 "sync call should invoke self.ping, got: {}",
570 code
571 );
572 assert!(
573 !code.contains("await"),
574 "sync call should not contain await, got: {}",
575 code
576 );
577 }
578
579 #[test]
580 fn method_call_sync_with_args() {
581 let method = parse_method(quote! {
582 fn greet(&self, name: String, count: u32) {}
583 });
584
585 let tokens = generate_method_call(&method, AsyncHandling::Error);
586 let code = tokens.to_string();
587
588 assert!(
589 code.contains("self . greet"),
590 "should call self.greet, got: {}",
591 code
592 );
593 assert!(code.contains("name"), "should pass name arg, got: {}", code);
594 assert!(
595 code.contains("count"),
596 "should pass count arg, got: {}",
597 code
598 );
599 }
600
601 #[test]
602 fn method_call_async_error() {
603 let method = parse_method(quote! {
604 async fn fetch(&self) -> String { todo!() }
605 });
606
607 let tokens = generate_method_call(&method, AsyncHandling::Error);
608 let code = tokens.to_string();
609
610 assert!(
611 code.contains("Err") || code.contains("return"),
612 "async + Error should return an error, got: {}",
613 code
614 );
615 assert!(
616 code.contains("not supported"),
617 "error message should mention not supported, got: {}",
618 code
619 );
620 }
621
622 #[test]
623 fn method_call_async_await() {
624 let method = parse_method(quote! {
625 async fn fetch(&self) -> String { todo!() }
626 });
627
628 let tokens = generate_method_call(&method, AsyncHandling::Await);
629 let code = tokens.to_string();
630
631 assert!(
632 code.contains(". await"),
633 "async + Await should contain .await, got: {}",
634 code
635 );
636 }
637
638 #[test]
639 fn method_call_async_block_on() {
640 let method = parse_method(quote! {
641 async fn fetch(&self) -> String { todo!() }
642 });
643
644 let tokens = generate_method_call(&method, AsyncHandling::BlockOn);
645 let code = tokens.to_string();
646
647 assert!(
648 code.contains("block_on"),
649 "async + BlockOn should contain block_on, got: {}",
650 code
651 );
652 assert!(
653 code.contains("Runtime"),
654 "should reference tokio Runtime, got: {}",
655 code
656 );
657 }
658
659 #[test]
664 fn json_response_unit() {
665 let method = parse_method(quote! {
666 fn ping(&self) {}
667 });
668
669 let tokens = generate_json_response(&method);
670 let code = tokens.to_string();
671
672 assert!(
673 code.contains("success"),
674 "unit return should produce success: true, got: {}",
675 code
676 );
677 }
678
679 #[test]
680 fn json_response_result() {
681 let method = parse_method(quote! {
682 fn get(&self) -> Result<String, String> { todo!() }
683 });
684
685 let tokens = generate_json_response(&method);
686 let code = tokens.to_string();
687
688 assert!(
689 code.contains("Ok"),
690 "Result return should match Ok, got: {}",
691 code
692 );
693 assert!(
694 code.contains("Err"),
695 "Result return should match Err, got: {}",
696 code
697 );
698 }
699
700 #[test]
701 fn json_response_option() {
702 let method = parse_method(quote! {
703 fn find(&self) -> Option<String> { todo!() }
704 });
705
706 let tokens = generate_json_response(&method);
707 let code = tokens.to_string();
708
709 assert!(
710 code.contains("Some"),
711 "Option return should match Some, got: {}",
712 code
713 );
714 assert!(
715 code.contains("None"),
716 "Option return should match None, got: {}",
717 code
718 );
719 assert!(
720 code.contains("Null"),
721 "Option None should produce Null, got: {}",
722 code
723 );
724 }
725
726 #[test]
727 fn json_response_plain_type() {
728 let method = parse_method(quote! {
729 fn count(&self) -> u64 { todo!() }
730 });
731
732 let tokens = generate_json_response(&method);
733 let code = tokens.to_string();
734
735 assert!(
736 code.contains("to_value"),
737 "plain return should serialize with to_value, got: {}",
738 code
739 );
740 assert!(
742 !code.contains("match"),
743 "plain return should not have match, got: {}",
744 code
745 );
746 }
747
748 #[test]
753 fn dispatch_arm_contains_method_name_string() {
754 let method = parse_method(quote! {
755 fn greet(&self, name: String) -> String { todo!() }
756 });
757
758 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
759 let code = tokens.to_string();
760
761 assert!(
762 code.contains("\"greet\""),
763 "dispatch arm should match on method name string, got: {}",
764 code
765 );
766 }
767
768 #[test]
769 fn dispatch_arm_with_name_override() {
770 let method = parse_method(quote! {
771 fn greet(&self, name: String) -> String { todo!() }
772 });
773
774 let tokens = generate_dispatch_arm(&method, Some("say_hello"), AsyncHandling::Error);
775 let code = tokens.to_string();
776
777 assert!(
778 code.contains("\"say_hello\""),
779 "dispatch arm should use overridden name, got: {}",
780 code
781 );
782 assert!(
783 !code.contains("\"greet\""),
784 "dispatch arm should not use original name when overridden, got: {}",
785 code
786 );
787 }
788
789 #[test]
790 fn dispatch_arm_includes_param_extraction() {
791 let method = parse_method(quote! {
792 fn greet(&self, name: String) -> String { todo!() }
793 });
794
795 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
796 let code = tokens.to_string();
797
798 assert!(
800 code.contains("\"name\""),
801 "dispatch arm should extract 'name' param, got: {}",
802 code
803 );
804 }
805
806 #[test]
807 fn dispatch_arm_includes_method_call_and_response() {
808 let method = parse_method(quote! {
809 fn ping(&self) {}
810 });
811
812 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
813 let code = tokens.to_string();
814
815 assert!(
816 code.contains("self . ping"),
817 "dispatch arm should call self.ping, got: {}",
818 code
819 );
820 assert!(
821 code.contains("success"),
822 "dispatch arm for unit return should include success response, got: {}",
823 code
824 );
825 }
826
827 #[test]
828 fn dispatch_arm_async_error_returns_early() {
829 let method = parse_method(quote! {
830 async fn fetch(&self) -> String { todo!() }
831 });
832
833 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
834 let code = tokens.to_string();
835
836 assert!(
837 code.contains("not supported"),
838 "async dispatch with Error handling should return error, got: {}",
839 code
840 );
841 }
842
843 #[test]
844 fn dispatch_arm_async_await() {
845 let method = parse_method(quote! {
846 async fn fetch(&self, url: String) -> Result<String, String> { todo!() }
847 });
848
849 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Await);
850 let code = tokens.to_string();
851
852 assert!(
853 code.contains(". await"),
854 "async dispatch with Await should contain .await, got: {}",
855 code
856 );
857 assert!(
858 code.contains("\"url\""),
859 "should extract url param, got: {}",
860 code
861 );
862 }
863
864 #[test]
869 fn dispatch_arm_with_injections_replaces_injected_param() {
870 let method = parse_method(quote! {
871 fn handle(&self, ctx: Context, name: String) -> String { todo!() }
872 });
873
874 let injection = quote! { __ctx.clone() };
875 let tokens = generate_dispatch_arm_with_injections(
876 &method,
877 None,
878 AsyncHandling::Error,
879 &[(0, injection)],
880 );
881 let code = tokens.to_string();
882
883 assert!(
885 code.contains("__ctx"),
886 "injected param should use provided expression, got: {}",
887 code
888 );
889 assert!(
891 code.contains("\"name\""),
892 "non-injected param should be extracted from JSON, got: {}",
893 code
894 );
895 }
896
897 #[test]
902 fn all_param_extractions_generates_one_per_param() {
903 let method = parse_method(quote! {
904 fn create(&self, name: String, value: i32, label: Option<String>) {}
905 });
906
907 let extractions = generate_all_param_extractions(&method);
908 assert_eq!(
909 extractions.len(),
910 3,
911 "should generate one extraction per param"
912 );
913 }
914
915 #[test]
920 fn param_extractions_for_subset() {
921 let method = parse_method(quote! {
922 fn handle(&self, ctx: Context, name: String, age: u32) {}
923 });
924
925 let subset: Vec<&ParamInfo> = method.params.iter().skip(1).collect();
927 let extractions = generate_param_extractions_for(&subset);
928 assert_eq!(extractions.len(), 2);
929
930 let code = extractions
931 .iter()
932 .map(|t| t.to_string())
933 .collect::<String>();
934 assert!(
935 !code.contains("\"ctx\""),
936 "should not extract ctx, got: {}",
937 code
938 );
939 assert!(
940 code.contains("\"name\""),
941 "should extract name, got: {}",
942 code
943 );
944 }
945
946 #[test]
951 fn method_call_with_custom_args() {
952 let method = parse_method(quote! {
953 fn handle(&self, ctx: Context, name: String) -> String { todo!() }
954 });
955
956 let args = vec![quote! { __ctx }, quote! { name }];
957 let tokens = generate_method_call_with_args(&method, args, AsyncHandling::Error);
958 let code = tokens.to_string();
959
960 assert!(
961 code.contains("__ctx"),
962 "should pass custom arg expression, got: {}",
963 code
964 );
965 assert!(
966 code.contains("self . handle"),
967 "should call self.handle, got: {}",
968 code
969 );
970 }
971
972 #[test]
977 fn param_schema_for_subset() {
978 let method = parse_method(quote! {
979 fn handle(&self, ctx: Context, name: String, limit: Option<u32>) {}
980 });
981
982 let subset: Vec<&ParamInfo> = method.params.iter().skip(1).collect();
983 let (properties, required) = generate_param_schema_for(&subset);
984
985 assert_eq!(properties.len(), 2);
986 assert_eq!(required, vec!["name"]);
987 assert!(!required.contains(&"ctx".to_string()));
988 }
989
990 #[test]
995 fn dispatch_arm_no_params_unit_return() {
996 let method = parse_method(quote! {
997 fn health_check(&self) {}
998 });
999
1000 let tokens = generate_dispatch_arm(&method, None, AsyncHandling::Error);
1001 let code = tokens.to_string();
1002
1003 assert!(
1004 code.contains("\"health_check\""),
1005 "should match on method name, got: {}",
1006 code
1007 );
1008 assert!(
1009 code.contains("success"),
1010 "unit return should produce success, got: {}",
1011 code
1012 );
1013 }
1014
1015 #[test]
1016 fn infer_json_type_option_string_is_string() {
1017 let ty: syn::Type = syn::parse_quote!(Option<String>);
1019 assert_eq!(infer_json_type(&ty), "string");
1020 }
1021
1022 #[test]
1023 fn infer_json_type_vec_u8_matches_integer_first() {
1024 let ty: syn::Type = syn::parse_quote!(Vec<u8>);
1027 assert_eq!(infer_json_type(&ty), "integer");
1028 }
1029
1030 #[test]
1031 fn method_call_sync_ignores_async_handling_variant() {
1032 let method = parse_method(quote! {
1034 fn ping(&self) {}
1035 });
1036
1037 let code_error = generate_method_call(&method, AsyncHandling::Error).to_string();
1038 let code_await = generate_method_call(&method, AsyncHandling::Await).to_string();
1039 let code_block = generate_method_call(&method, AsyncHandling::BlockOn).to_string();
1040
1041 assert_eq!(code_error, code_await);
1042 assert_eq!(code_await, code_block);
1043 }
1044}