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_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        // Plain T
183        quote! {
184            Ok(::server_less::serde_json::to_value(result)
185                .map_err(|e| format!("Serialization error: {}", e))?)
186        }
187    }
188}
189
190/// Generate a complete dispatch match arm for an RPC method.
191///
192/// Combines param extraction, method call, and response handling.
193pub 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    // Methods that are async OR return streams require async context
203    let requires_async = method.is_async || method.return_info.is_stream;
204
205    // For methods requiring async with Error handling, return early
206    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
227/// Generate a dispatch arm with support for injected parameters.
228///
229/// Parameters whose index appears in `injected_params` will use the provided
230/// TokenStream expression instead of being deserialized from JSON. This is
231/// used for mount trait dispatch where Context/WsSender need injection.
232pub 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    // Methods that are async OR return streams require async context
243    let requires_async = method.is_async || method.return_info.is_stream;
244
245    // For methods requiring async with Error handling, return early
246    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    // Generate param extractions, substituting injected params
255    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
281/// Infer JSON schema type from Rust type.
282pub 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
310/// Generate JSON schema properties for method parameters.
311pub 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
334/// Generate JSON schema properties for specific parameters (e.g., excluding Context).
335pub 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    /// Helper: parse a method signature string into a MethodInfo.
365    /// The method must have a `&self` receiver.
366    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    // ---------------------------------------------------------------
374    // infer_json_type
375    // ---------------------------------------------------------------
376
377    #[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        // Vec<T> where T doesn't match an earlier rule maps to "array"
423        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        // Note: infer_json_type uses string matching, so Vec<String>
430        // matches "String" before "Vec", returning "string".
431        // This documents the current behavior.
432        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    // ---------------------------------------------------------------
443    // generate_param_schema
444    // ---------------------------------------------------------------
445
446    #[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    // ---------------------------------------------------------------
493    // generate_param_extraction
494    // ---------------------------------------------------------------
495
496    #[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    // ---------------------------------------------------------------
555    // generate_method_call
556    // ---------------------------------------------------------------
557
558    #[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    // ---------------------------------------------------------------
660    // generate_json_response
661    // ---------------------------------------------------------------
662
663    #[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        // Should NOT have Ok/Err match arms for Result or Some/None for Option
741        assert!(
742            !code.contains("match"),
743            "plain return should not have match, got: {}",
744            code
745        );
746    }
747
748    // ---------------------------------------------------------------
749    // generate_dispatch_arm
750    // ---------------------------------------------------------------
751
752    #[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        // Should include param extraction for "name"
799        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    // ---------------------------------------------------------------
865    // generate_dispatch_arm_with_injections
866    // ---------------------------------------------------------------
867
868    #[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        // The injected param should use the injection expression
884        assert!(
885            code.contains("__ctx"),
886            "injected param should use provided expression, got: {}",
887            code
888        );
889        // The non-injected param should still be extracted from JSON
890        assert!(
891            code.contains("\"name\""),
892            "non-injected param should be extracted from JSON, got: {}",
893            code
894        );
895    }
896
897    // ---------------------------------------------------------------
898    // generate_all_param_extractions
899    // ---------------------------------------------------------------
900
901    #[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    // ---------------------------------------------------------------
916    // generate_param_extractions_for (subset)
917    // ---------------------------------------------------------------
918
919    #[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        // Only generate extractions for name and age, not ctx
926        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    // ---------------------------------------------------------------
947    // generate_method_call_with_args
948    // ---------------------------------------------------------------
949
950    #[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    // ---------------------------------------------------------------
973    // generate_param_schema_for (subset)
974    // ---------------------------------------------------------------
975
976    #[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    // ---------------------------------------------------------------
991    // Edge cases
992    // ---------------------------------------------------------------
993
994    #[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        // Option<String> contains "String" so it maps to "string"
1018        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        // Vec<u8> matches "u8" (integer) before "Vec" (array) due to
1025        // string-based matching order. This documents the current behavior.
1026        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        // A sync method should generate the same code regardless of AsyncHandling variant
1033        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}