Skip to main content

server_less_rpc/
lib.rs

1//! Shared utilities for RPC-style macros (MCP, WebSocket, JSON-RPC).
2//!
3//! These macros use JSON-RPC-like dispatch:
4//! - Receive `{"method": "name", "params": {...}}`
5//! - Extract params from JSON
6//! - Call the method
7//! - Serialize result back to JSON
8
9use proc_macro2::TokenStream;
10use quote::quote;
11use server_less_parse::{MethodInfo, ParamInfo};
12
13/// Generate code to extract a parameter from a `serde_json::Value` args object.
14pub fn generate_param_extraction(param: &ParamInfo) -> TokenStream {
15    let name = &param.name;
16    let name_str = param.name.to_string();
17    let ty = &param.ty;
18
19    if param.is_optional {
20        // For Option<T>, extract inner value, return None if missing/null
21        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        // Required parameter - error if missing
29        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
39/// Generate all param extractions for a method.
40pub fn generate_all_param_extractions(method: &MethodInfo) -> Vec<TokenStream> {
41    method
42        .params
43        .iter()
44        .map(generate_param_extraction)
45        .collect()
46}
47
48/// Generate param extractions for specific parameters only.
49///
50/// This allows filtering out framework-injected params (like Context)
51/// that shouldn't be extracted from JSON.
52pub fn generate_param_extractions_for(params: &[&ParamInfo]) -> Vec<TokenStream> {
53    params
54        .iter()
55        .map(|p| generate_param_extraction(p))
56        .collect()
57}
58
59/// Generate the method call expression.
60///
61/// Returns tokens for calling `self.method_name(arg1, arg2, ...)`.
62/// For async methods, returns an error (caller should handle async context).
63pub 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
93/// Generate method call with custom argument expressions.
94///
95/// This allows mixing framework-injected args (like `__ctx`) with
96/// params extracted from JSON.
97pub 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/// How to handle async methods.
131#[derive(Debug, Clone, Copy)]
132pub enum AsyncHandling {
133    /// Return an error if method is async
134    Error,
135    /// Await the method (caller must be async)
136    Await,
137    /// Use tokio::runtime::Runtime::block_on
138    BlockOn,
139}
140
141/// Generate response handling that converts the method result to JSON.
142///
143/// Handles:
144/// - `()` → `{"success": true}`
145/// - `Result<T, E>` → `Ok(T)` or `Err(message)`
146/// - `Option<T>` → `T` or `null`
147/// - `T` → serialized T
148pub 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        // Automatically collect streams into Vec for JSON serialization
157        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        // Collect iterator into Vec before serializing (Iterator doesn't implement Serialize)
167        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        // Plain T
192        quote! {
193            Ok(::server_less::serde_json::to_value(result)
194                .map_err(|e| format!("Serialization error: {}", e))?)
195        }
196    }
197}
198
199/// Generate a complete dispatch match arm for an RPC method.
200///
201/// Combines param extraction, method call, and response handling.
202pub 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    // Methods that are async OR return streams require async context
212    let requires_async = method.is_async || method.return_info.is_stream;
213
214    // For methods requiring async with Error handling, return early
215    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
236/// Generate a dispatch arm with support for injected parameters.
237///
238/// Parameters whose index appears in `injected_params` will use the provided
239/// TokenStream expression instead of being deserialized from JSON. This is
240/// used for mount trait dispatch where Context/WsSender need injection.
241pub 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    // Methods that are async OR return streams require async context
252    let requires_async = method.is_async || method.return_info.is_stream;
253
254    // For methods requiring async with Error handling, return early
255    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    // Generate param extractions, substituting injected params
264    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
290/// Infer JSON schema type from Rust type using AST inspection.
291///
292/// Checks the outermost type name, not substrings, so `Vec<String>` → `"array"`,
293/// not `"string"`. Recurses into `Option<T>` to get the inner type.
294pub 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                        // Recurse into Option<T> to get the inner type's schema type
309                        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        // &str and &T
323        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
337/// Generate JSON schema properties for method parameters.
338pub 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
364/// Generate JSON schema properties for specific parameters (e.g., excluding Context).
365pub 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    /// Helper: parse a method signature string into a MethodInfo.
398    /// The method must have a `&self` receiver.
399    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    // ---------------------------------------------------------------
407    // infer_json_type
408    // ---------------------------------------------------------------
409
410    #[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        // Vec<T> where T doesn't match an earlier rule maps to "array"
456        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        // Vec<String> → outer type is Vec → "array"
463        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    // ---------------------------------------------------------------
474    // generate_param_schema
475    // ---------------------------------------------------------------
476
477    #[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    // ---------------------------------------------------------------
524    // generate_param_extraction
525    // ---------------------------------------------------------------
526
527    #[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    // ---------------------------------------------------------------
586    // generate_method_call
587    // ---------------------------------------------------------------
588
589    #[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    // ---------------------------------------------------------------
691    // generate_json_response
692    // ---------------------------------------------------------------
693
694    #[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        // Should NOT have Ok/Err match arms for Result or Some/None for Option
772        assert!(
773            !code.contains("match"),
774            "plain return should not have match, got: {}",
775            code
776        );
777    }
778
779    // ---------------------------------------------------------------
780    // generate_dispatch_arm
781    // ---------------------------------------------------------------
782
783    #[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        // Should include param extraction for "name"
830        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    // ---------------------------------------------------------------
896    // generate_dispatch_arm_with_injections
897    // ---------------------------------------------------------------
898
899    #[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        // The injected param should use the injection expression
915        assert!(
916            code.contains("__ctx"),
917            "injected param should use provided expression, got: {}",
918            code
919        );
920        // The non-injected param should still be extracted from JSON
921        assert!(
922            code.contains("\"name\""),
923            "non-injected param should be extracted from JSON, got: {}",
924            code
925        );
926    }
927
928    // ---------------------------------------------------------------
929    // generate_all_param_extractions
930    // ---------------------------------------------------------------
931
932    #[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    // ---------------------------------------------------------------
947    // generate_param_extractions_for (subset)
948    // ---------------------------------------------------------------
949
950    #[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        // Only generate extractions for name and age, not ctx
957        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    // ---------------------------------------------------------------
978    // generate_method_call_with_args
979    // ---------------------------------------------------------------
980
981    #[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    // ---------------------------------------------------------------
1004    // generate_param_schema_for (subset)
1005    // ---------------------------------------------------------------
1006
1007    #[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    // ---------------------------------------------------------------
1022    // Edge cases
1023    // ---------------------------------------------------------------
1024
1025    #[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        // Option<T> recurses into T, so Option<String> → "string"
1049        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        // Vec<u8> → outer type is Vec → "array"
1056        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        // A sync method should generate the same code regardless of AsyncHandling variant
1069        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}