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