Skip to main content

conduit_derive/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3//! Proc macros for conduit: `#[derive(Encode, Decode)]` for binary codecs
4//! and `#[command]` for Tauri-style named-parameter handlers.
5
6use proc_macro::TokenStream;
7use quote::{format_ident, quote};
8use syn::{Data, DeriveInput, Fields, FnArg, ItemFn, Pat, parse_macro_input};
9
10/// Derive the `Encode` trait for a struct with named fields.
11///
12/// Generates a `conduit_core::Encode` implementation that encodes each
13/// field in declaration order by delegating to the field type's own
14/// `Encode` impl.
15///
16/// # Example
17///
18/// ```rust,ignore
19/// use conduit_derive::Encode;
20///
21/// #[derive(Encode)]
22/// struct MarketTick {
23///     timestamp: i64,
24///     price: f64,
25///     volume: f64,
26///     side: u8,
27/// }
28/// ```
29#[proc_macro_derive(Encode)]
30pub fn derive_encode(input: TokenStream) -> TokenStream {
31    let input = parse_macro_input!(input as DeriveInput);
32    match impl_encode(&input) {
33        Ok(tokens) => tokens.into(),
34        Err(err) => err.to_compile_error().into(),
35    }
36}
37
38/// Derive the `Decode` trait for a struct with named fields.
39///
40/// Generates a `conduit_core::Decode` implementation that decodes each
41/// field in declaration order by delegating to the field type's own
42/// `Decode` impl, tracking the cumulative byte offset.
43///
44/// # Example
45///
46/// ```rust,ignore
47/// use conduit_derive::Decode;
48///
49/// #[derive(Decode)]
50/// struct MarketTick {
51///     timestamp: i64,
52///     price: f64,
53///     volume: f64,
54///     side: u8,
55/// }
56/// ```
57#[proc_macro_derive(Decode)]
58pub fn derive_decode(input: TokenStream) -> TokenStream {
59    let input = parse_macro_input!(input as DeriveInput);
60    match impl_decode(&input) {
61        Ok(tokens) => tokens.into(),
62        Err(err) => err.to_compile_error().into(),
63    }
64}
65
66/// Extract named fields from a `DeriveInput`, rejecting enums, unions, and
67/// tuple/unit structs with a compile error.
68fn named_fields(input: &DeriveInput) -> syn::Result<&syn::FieldsNamed> {
69    let name = &input.ident;
70    match &input.data {
71        Data::Struct(data) => match &data.fields {
72            Fields::Named(named) => Ok(named),
73            _ => Err(syn::Error::new_spanned(
74                name,
75                "Encode / Decode can only be derived for structs with named fields",
76            )),
77        },
78        Data::Enum(_) => Err(syn::Error::new_spanned(
79            name,
80            "Encode / Decode cannot be derived for enums",
81        )),
82        Data::Union(_) => Err(syn::Error::new_spanned(
83            name,
84            "Encode / Decode cannot be derived for unions",
85        )),
86    }
87}
88
89/// Reject generic structs with a compile error — wire encoding requires
90/// a fixed, concrete layout.
91fn reject_generics(input: &DeriveInput) -> syn::Result<()> {
92    if !input.generics.params.is_empty() {
93        return Err(syn::Error::new_spanned(
94            &input.generics,
95            "Encode / Decode cannot be derived for generic structs",
96        ));
97    }
98    Ok(())
99}
100
101/// Generate the `Encode` impl: encodes each named field in declaration
102/// order and sums their `encode_size()` for the total.
103fn impl_encode(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
104    reject_generics(input)?;
105    let name = &input.ident;
106    let fields = named_fields(input)?;
107
108    let encode_stmts: Vec<_> = fields
109        .named
110        .iter()
111        .map(|f| {
112            let ident = f.ident.as_ref().unwrap();
113            quote! {
114                ::conduit_core::Encode::encode(&self.#ident, buf);
115            }
116        })
117        .collect();
118
119    let size_terms: Vec<_> = fields
120        .named
121        .iter()
122        .map(|f| {
123            let ident = f.ident.as_ref().unwrap();
124            quote! {
125                ::conduit_core::Encode::encode_size(&self.#ident)
126            }
127        })
128        .collect();
129
130    // Handle the zero-field edge case: encode_size returns 0.
131    let size_expr = if size_terms.is_empty() {
132        quote! { 0 }
133    } else {
134        let first = &size_terms[0];
135        let rest = &size_terms[1..];
136        quote! { #first #(+ #rest)* }
137    };
138
139    Ok(quote! {
140        impl ::conduit_core::Encode for #name {
141            fn encode(&self, buf: &mut Vec<u8>) {
142                #(#encode_stmts)*
143            }
144
145            fn encode_size(&self) -> usize {
146                #size_expr
147            }
148        }
149    })
150}
151
152/// Generate the `Decode` impl: decodes each named field in declaration
153/// order, tracking the cumulative byte offset through the input slice.
154fn impl_decode(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
155    reject_generics(input)?;
156    let name = &input.ident;
157    let fields = named_fields(input)?;
158
159    let decode_stmts: Vec<_> = fields
160        .named
161        .iter()
162        .map(|f| {
163            let ident = f.ident.as_ref().unwrap();
164            quote! {
165                let #ident = {
166                    let (__cdec_v__, __cdec_n__) = ::conduit_core::Decode::decode(&__cdec_src__[__cdec_off__..])?;
167                    __cdec_off__ += __cdec_n__;
168                    __cdec_v__
169                };
170            }
171        })
172        .collect();
173
174    let field_names: Vec<_> = fields
175        .named
176        .iter()
177        .map(|f| f.ident.as_ref().unwrap())
178        .collect();
179
180    Ok(quote! {
181        impl ::conduit_core::Decode for #name {
182            fn decode(__cdec_src__: &[u8]) -> Option<(Self, usize)> {
183                let mut __cdec_off__ = 0usize;
184                #(#decode_stmts)*
185                Some((Self { #(#field_names),* }, __cdec_off__))
186            }
187        }
188    })
189}
190
191// ---------------------------------------------------------------------------
192// #[command] attribute macro
193// ---------------------------------------------------------------------------
194
195/// Attribute macro that transforms a function into a conduit command handler.
196///
197/// Preserves the original function and generates a hidden handler struct
198/// (`__conduit_handler_{fn_name}`) implementing [`conduit_core::ConduitHandler`].
199/// Use [`handler!`] to obtain the handler struct for registration.
200///
201/// This is conduit's 1:1 equivalent of `#[tauri::command]`. The macro supports:
202///
203/// - **Named parameters** — generates a hidden args struct with
204///   `#[derive(Deserialize)]` and `#[serde(rename_all = "camelCase")]`.
205///   Rust snake_case parameters are automatically converted to camelCase
206///   in JSON, matching `#[tauri::command]` behavior.
207/// - **`State<T>` injection** — parameters whose type path ends in `State`
208///   are extracted from the context (which must be an `AppHandle<Wry>`).
209/// - **`AppHandle` injection** — parameters whose type path ends in `AppHandle`.
210/// - **`Window`/`WebviewWindow` injection** — parameters whose type path ends
211///   in `Window` or `WebviewWindow`, resolved via `app_handle.get_webview_window(label)`.
212/// - **`Webview` injection** — parameters whose type path ends in `Webview`,
213///   resolved via `app_handle.get_webview(label)`.
214/// - **`Result<T, E>` returns** — errors are converted via `Display` into
215///   `conduit_core::Error::Handler`.
216/// - **`async` functions** — truly async, spawned on the tokio runtime
217///   (not `block_on`).
218///
219/// # Examples
220///
221/// ```rust,ignore
222/// use conduit::command;
223///
224/// // Named parameters — frontend sends { "name": "Alice", "greeting": "Hi" }
225/// #[command]
226/// fn greet(name: String, greeting: String) -> String {
227///     format!("{greeting}, {name}!")
228/// }
229///
230/// // Result return — errors become conduit_core::Error::Handler
231/// #[command]
232/// fn divide(a: f64, b: f64) -> Result<f64, String> {
233///     if b == 0.0 { Err("division by zero".into()) }
234///     else { Ok(a / b) }
235/// }
236///
237/// // State injection + async + Result
238/// #[command]
239/// async fn fetch_user(state: State<'_, Db>, id: u64) -> Result<User, String> {
240///     state.get_user(id).await.map_err(|e| e.to_string())
241/// }
242/// ```
243///
244/// # Error handling
245///
246/// When a `Result`-returning handler returns `Err(e)`, the error's
247/// `Display` text is sent to the frontend as a JSON error response.
248/// This matches `#[tauri::command]` behavior. Be careful about what
249/// information your error types expose via `Display`.
250///
251/// # Limitations
252///
253/// - **`tauri::Wry` only**: Generated handlers assume `tauri::Wry` as the
254///   runtime backend. This is the default (and typically only) runtime in
255///   Tauri v2.
256/// - **Multiple `State<T>` params**: Each `State<T>` must use a distinct
257///   concrete type `T`. Tauri's state system is keyed by `TypeId`, so two
258///   params with the same `T` will receive the same instance.
259/// - **Name-based injection detection**: `State`, `AppHandle`, `Window`,
260///   `WebviewWindow`, and `Webview` are identified by the last path segment
261///   of the type. Any user type with these names will be misinterpreted as
262///   a Tauri injectable type. Rename your types to avoid false matches.
263/// - **Name-based Result detection**: The return type is detected as
264///   `Result` by checking the last path segment. Type aliases like
265///   `type MyResult<T> = Result<T, E>` are NOT detected as Result returns
266///   and will be serialized directly instead of unwrapping `Ok`/`Err`.
267/// - **Window/Webview require label**: `Window` and `Webview` injection
268///   requires the frontend to send the `X-Conduit-Webview` header (handled
269///   automatically by the TS client). If no label is available, the handler
270///   returns an error.
271/// - **No `impl` block support**: The macro generates struct definitions
272///   at the call site, which is illegal inside `impl` blocks. Only use
273///   `#[command]` on free-standing functions.
274#[proc_macro_attribute]
275pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
276    if !attr.is_empty() {
277        return syn::Error::new(
278            proc_macro2::Span::call_site(),
279            "#[command] does not accept arguments",
280        )
281        .to_compile_error()
282        .into();
283    }
284    let func = parse_macro_input!(item as ItemFn);
285    match impl_conduit_command(func) {
286        Ok(tokens) => tokens.into(),
287        Err(err) => err.to_compile_error().into(),
288    }
289}
290
291/// Check if a type is `State<...>` by looking at the last path segment.
292///
293/// **Limitation**: This matches any type whose last path segment is `State`,
294/// not just `tauri::State`. If you have a custom type named `State`, rename
295/// it to avoid being treated as an injectable Tauri state parameter.
296fn is_state_type(ty: &syn::Type) -> bool {
297    if let syn::Type::Reference(type_ref) = ty {
298        // Handle &State<...> (reference to State)
299        return is_state_type(&type_ref.elem);
300    }
301    if let syn::Type::Path(type_path) = ty {
302        if let Some(seg) = type_path.path.segments.last() {
303            return seg.ident == "State";
304        }
305    }
306    false
307}
308
309/// Check if a type is `AppHandle<...>` by looking at the last path segment.
310fn is_app_handle_type(ty: &syn::Type) -> bool {
311    if let syn::Type::Reference(type_ref) = ty {
312        return is_app_handle_type(&type_ref.elem);
313    }
314    if let syn::Type::Path(type_path) = ty {
315        if let Some(seg) = type_path.path.segments.last() {
316            return seg.ident == "AppHandle";
317        }
318    }
319    false
320}
321
322/// Extract the inner type `T` from `State<'_, T>`.
323///
324/// Returns the second generic argument (skipping the lifetime).
325fn extract_state_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
326    // Unwrap references first
327    let ty = if let syn::Type::Reference(type_ref) = ty {
328        &*type_ref.elem
329    } else {
330        ty
331    };
332
333    if let syn::Type::Path(type_path) = ty {
334        if let Some(seg) = type_path.path.segments.last() {
335            if seg.ident == "State" {
336                if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
337                    // Find the first type argument (skip lifetimes)
338                    for arg in &args.args {
339                        if let syn::GenericArgument::Type(inner_ty) = arg {
340                            return Some(inner_ty);
341                        }
342                    }
343                }
344            }
345        }
346    }
347    None
348}
349
350/// Check if a type is `Window` or `WebviewWindow` by looking at the last path segment.
351///
352/// Both `Window` and `WebviewWindow` are treated identically — the generated
353/// code calls `app_handle.get_webview_window(label)` which returns a
354/// `WebviewWindow` (the unified type in Tauri v2).
355fn is_window_type(ty: &syn::Type) -> bool {
356    if let syn::Type::Reference(type_ref) = ty {
357        return is_window_type(&type_ref.elem);
358    }
359    if let syn::Type::Path(type_path) = ty {
360        if let Some(seg) = type_path.path.segments.last() {
361            return seg.ident == "Window" || seg.ident == "WebviewWindow";
362        }
363    }
364    false
365}
366
367/// Check if a type is `Webview` by looking at the last path segment.
368fn is_webview_type(ty: &syn::Type) -> bool {
369    if let syn::Type::Reference(type_ref) = ty {
370        return is_webview_type(&type_ref.elem);
371    }
372    if let syn::Type::Path(type_path) = ty {
373        if let Some(seg) = type_path.path.segments.last() {
374            return seg.ident == "Webview";
375        }
376    }
377    false
378}
379
380/// Check if a type is `Option<...>` by looking at the last path segment.
381fn is_option_type(ty: &syn::Type) -> bool {
382    if let syn::Type::Path(type_path) = ty {
383        if let Some(seg) = type_path.path.segments.last() {
384            return seg.ident == "Option";
385        }
386    }
387    false
388}
389
390/// Check if the return type is `Result<...>`.
391fn is_result_return(output: &syn::ReturnType) -> bool {
392    match output {
393        syn::ReturnType::Default => false,
394        syn::ReturnType::Type(_, ty) => {
395            if let syn::Type::Path(type_path) = ty.as_ref() {
396                if let Some(seg) = type_path.path.segments.last() {
397                    return seg.ident == "Result";
398                }
399            }
400            false
401        }
402    }
403}
404
405/// Implementation of the `#[command]` attribute macro.
406///
407/// Preserves the original function and generates a hidden handler struct
408/// (`__conduit_handler_{fn_name}`) implementing [`conduit_core::ConduitHandler`].
409/// This mirrors `#[tauri::command]` behavior: the function remains callable
410/// directly, and the handler struct is used for registration via
411/// `conduit::handler!(fn_name)`.
412fn impl_conduit_command(func: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
413    let fn_name = &func.sig.ident;
414    let fn_vis = &func.vis;
415    let fn_sig = &func.sig;
416    let fn_block = &func.block;
417    let fn_attrs = &func.attrs;
418    let is_async = func.sig.asyncness.is_some();
419
420    if !func.sig.generics.params.is_empty() {
421        return Err(syn::Error::new_spanned(
422            &func.sig.generics,
423            "#[command] cannot be used on generic functions",
424        ));
425    }
426
427    if func.sig.generics.where_clause.is_some() {
428        return Err(syn::Error::new_spanned(
429            &func.sig.generics.where_clause,
430            "#[command] cannot be used on functions with where clauses",
431        ));
432    }
433
434    for arg in &func.sig.inputs {
435        if let FnArg::Typed(pat_type) = arg {
436            if matches!(&*pat_type.ty, syn::Type::ImplTrait(_)) {
437                return Err(syn::Error::new_spanned(
438                    &pat_type.ty,
439                    "#[command] cannot be used with `impl Trait` parameters",
440                ));
441            }
442        }
443    }
444
445    // Reject borrowed types on regular (non-State, non-AppHandle) parameters.
446    for arg in &func.sig.inputs {
447        if let FnArg::Typed(pat_type) = arg {
448            if !is_state_type(&pat_type.ty)
449                && !is_app_handle_type(&pat_type.ty)
450                && matches!(&*pat_type.ty, syn::Type::Reference(_))
451            {
452                return Err(syn::Error::new_spanned(
453                    &pat_type.ty,
454                    "#[command] parameters must be owned types (use String instead of &str)",
455                ));
456            }
457        }
458    }
459
460    let handler_struct_name = format_ident!("__conduit_handler_{}", fn_name);
461
462    // Separate State, AppHandle, Window/Webview, and regular params
463    let mut state_params: Vec<(&syn::Ident, &syn::Type)> = Vec::new();
464    let mut app_handle_params: Vec<(&syn::Ident, &syn::Type)> = Vec::new();
465    let mut window_params: Vec<(&syn::Ident, &syn::Type)> = Vec::new();
466    let mut webview_params: Vec<(&syn::Ident, &syn::Type)> = Vec::new();
467    let mut regular_params: Vec<(&syn::Ident, &syn::Type)> = Vec::new();
468    // Track all params in original order for the function call
469    let mut all_param_names: Vec<&syn::Ident> = Vec::new();
470
471    for arg in &func.sig.inputs {
472        if let FnArg::Receiver(_) = arg {
473            return Err(syn::Error::new_spanned(
474                arg,
475                "#[command] cannot be used on methods with `self`",
476            ));
477        }
478        if let FnArg::Typed(pat_type) = arg {
479            if let Pat::Ident(pat_ident) = &*pat_type.pat {
480                if pat_ident.by_ref.is_some() {
481                    return Err(syn::Error::new_spanned(
482                        &pat_type.pat,
483                        "#[command] does not support `ref` parameter bindings",
484                    ));
485                }
486                let param_name = &pat_ident.ident;
487                let param_type = &*pat_type.ty;
488
489                all_param_names.push(param_name);
490
491                if is_state_type(param_type) {
492                    state_params.push((param_name, param_type));
493                } else if is_app_handle_type(param_type) {
494                    app_handle_params.push((param_name, param_type));
495                } else if is_window_type(param_type) {
496                    window_params.push((param_name, param_type));
497                } else if is_webview_type(param_type) {
498                    webview_params.push((param_name, param_type));
499                } else {
500                    regular_params.push((param_name, param_type));
501                }
502            } else {
503                return Err(syn::Error::new_spanned(
504                    &pat_type.pat,
505                    "#[command] requires named parameters",
506                ));
507            }
508        }
509    }
510
511    // Detect Result return type
512    let is_result = is_result_return(&func.sig.output);
513
514    // Generate args struct for regular params
515    let has_args = !regular_params.is_empty();
516    let struct_name = format_ident!("__conduit_args_{}", fn_name);
517
518    let regular_names: Vec<_> = regular_params.iter().map(|(n, _)| *n).collect();
519
520    let has_state = !state_params.is_empty();
521    let has_app_handle = !app_handle_params.is_empty();
522    let has_window = !window_params.is_empty();
523    let has_webview = !webview_params.is_empty();
524    let needs_context = has_state || has_app_handle || has_window || has_webview;
525
526    // Context extraction code (State, AppHandle, Window, Webview injection)
527    let state_extraction = if needs_context {
528        let state_stmts: Vec<proc_macro2::TokenStream> = state_params
529            .iter()
530            .map(|(name, ty)| {
531                let inner_ty = extract_state_inner_type(ty);
532                match inner_ty {
533                    Some(inner) => {
534                        quote! {
535                            let #name: ::tauri::State<'_, #inner> = ::tauri::Manager::state(&*__app);
536                        }
537                    }
538                    None => {
539                        // Fallback: use the full type as-is
540                        quote! {
541                            let #name: #ty = ::tauri::Manager::state(&*__app);
542                        }
543                    }
544                }
545            })
546            .collect();
547
548        let app_handle_stmts: Vec<proc_macro2::TokenStream> = app_handle_params
549            .iter()
550            .map(|(name, _ty)| {
551                quote! {
552                    let #name = __app.clone();
553                }
554            })
555            .collect();
556
557        // Window/WebviewWindow injection: look up by webview label from HandlerContext
558        let window_stmts: Vec<proc_macro2::TokenStream> = window_params
559            .iter()
560            .map(|(name, _ty)| {
561                quote! {
562                    let #name = {
563                        let __label = __handler_ctx.webview_label.as_ref()
564                            .ok_or_else(|| ::conduit_core::Error::Handler(
565                                "Window injection requires X-Conduit-Webview header".into()
566                            ))?;
567                        ::tauri::Manager::get_webview_window(&*__app, __label)
568                            .ok_or_else(|| ::conduit_core::Error::Handler(
569                                ::std::format!("webview window '{}' not found", __label)
570                            ))?
571                    };
572                }
573            })
574            .collect();
575
576        // Webview injection
577        let webview_stmts: Vec<proc_macro2::TokenStream> = webview_params
578            .iter()
579            .map(|(name, _ty)| {
580                quote! {
581                    let #name = {
582                        let __label = __handler_ctx.webview_label.as_ref()
583                            .ok_or_else(|| ::conduit_core::Error::Handler(
584                                "Webview injection requires X-Conduit-Webview header".into()
585                            ))?;
586                        ::tauri::Manager::get_webview(&*__app, __label)
587                            .ok_or_else(|| ::conduit_core::Error::Handler(
588                                ::std::format!("webview '{}' not found", __label)
589                            ))?
590                    };
591                }
592            })
593            .collect();
594
595        let context_downcast = quote! {
596            let __handler_ctx = __ctx
597                .downcast_ref::<::conduit_core::HandlerContext>()
598                .ok_or_else(|| ::conduit_core::Error::Handler(
599                    "internal: handler context must be HandlerContext".into()
600                ))?;
601            let __app = __handler_ctx.app_handle
602                .downcast_ref::<::tauri::AppHandle<::tauri::Wry>>()
603                .ok_or_else(|| ::conduit_core::Error::Handler(
604                    "internal: handler context app_handle must be AppHandle<Wry>".into()
605                ))?;
606        };
607
608        quote! {
609            #context_downcast
610            #(#state_stmts)*
611            #(#app_handle_stmts)*
612            #(#window_stmts)*
613            #(#webview_stmts)*
614        }
615    } else {
616        quote! {}
617    };
618
619    // Args deserialization
620    let args_deser = if has_args {
621        quote! {
622            let #struct_name { #(#regular_names),* } =
623                ::conduit_core::sonic_rs::from_slice(&__payload)
624                    .map_err(::conduit_core::Error::from)?;
625        }
626    } else {
627        quote! {
628            let _ = &__payload;
629        }
630    };
631
632    // Function call — delegates to the preserved original function
633    let fn_call = if is_async {
634        quote! { #fn_name(#(#all_param_names),*).await }
635    } else {
636        quote! { #fn_name(#(#all_param_names),*) }
637    };
638
639    // Result handling
640    let result_handling = if is_result {
641        quote! {
642            let __result = #fn_call;
643            match __result {
644                ::std::result::Result::Ok(__v) => {
645                    ::conduit_core::sonic_rs::to_vec(&__v).map_err(::conduit_core::Error::from)
646                }
647                ::std::result::Result::Err(__e) => {
648                    ::std::result::Result::Err(::conduit_core::Error::Handler(__e.to_string()))
649                }
650            }
651        }
652    } else {
653        quote! {
654            let __result = #fn_call;
655            ::conduit_core::sonic_rs::to_vec(&__result).map_err(::conduit_core::Error::from)
656        }
657    };
658
659    // Generate args struct definition (only if has regular params)
660    let struct_def = if has_args {
661        // Add #[serde(default)] on Option<T> fields so they can be omitted from JSON.
662        let field_defs: Vec<proc_macro2::TokenStream> = regular_params
663            .iter()
664            .map(|(name, ty)| {
665                if is_option_type(ty) {
666                    quote! { #[serde(default)] #name: #ty }
667                } else {
668                    quote! { #name: #ty }
669                }
670            })
671            .collect();
672        quote! {
673            #[doc(hidden)]
674            #[allow(non_camel_case_types)]
675            #[derive(::conduit_core::serde::Deserialize)]
676            #[serde(crate = "::conduit_core::serde", rename_all = "camelCase")]
677            struct #struct_name {
678                #(#field_defs),*
679            }
680        }
681    } else {
682        quote! {}
683    };
684
685    // Generate the handler body — sync wraps in a closure, async in Box::pin
686    let handler_body = if is_async {
687        quote! {
688            ::conduit_core::HandlerResponse::Async(::std::boxed::Box::pin(async move {
689                #state_extraction
690                #args_deser
691                #result_handling
692            }))
693        }
694    } else {
695        quote! {
696            ::conduit_core::HandlerResponse::Sync((|| -> ::std::result::Result<::std::vec::Vec<u8>, ::conduit_core::Error> {
697                #state_extraction
698                #args_deser
699                #result_handling
700            })())
701        }
702    };
703
704    Ok(quote! {
705        #struct_def
706
707        // Preserved original function — callable directly in tests and non-conduit contexts.
708        #(#fn_attrs)*
709        #fn_vis #fn_sig #fn_block
710
711        // Hidden handler struct for conduit registration.
712        #[doc(hidden)]
713        #[allow(non_camel_case_types)]
714        #fn_vis struct #handler_struct_name;
715
716        impl ::conduit_core::ConduitHandler for #handler_struct_name {
717            fn call(
718                &self,
719                __payload: ::std::vec::Vec<u8>,
720                __ctx: ::std::sync::Arc<dyn ::std::any::Any + ::std::marker::Send + ::std::marker::Sync>,
721            ) -> ::conduit_core::HandlerResponse {
722                #handler_body
723            }
724        }
725    })
726}
727
728/// Resolve a `#[command]` function name to its generated handler struct.
729///
730/// Expands `handler!(foo)` to the hidden unit struct `__conduit_handler_foo`
731/// that `#[command]` generates alongside the original function. The struct
732/// implements [`conduit_core::ConduitHandler`] and is intended for
733/// registration with `PluginBuilder::handler`.
734///
735/// # Requirements
736///
737/// The target function **must** have `#[command]` applied. If `#[command]`
738/// is missing, the compiler will report "cannot find value
739/// `__conduit_handler_foo` in this scope".
740///
741/// # Example
742///
743/// ```rust,ignore
744/// use conduit::{command, handler};
745///
746/// #[command]
747/// fn greet(name: String) -> String {
748///     format!("Hello, {name}!")
749/// }
750///
751/// // Register with the plugin builder:
752/// tauri_plugin_conduit::init()
753///     .handler("greet", handler!(greet))
754///     .build()
755/// ```
756#[proc_macro]
757pub fn handler(input: TokenStream) -> TokenStream {
758    let mut path = parse_macro_input!(input as syn::Path);
759    if let Some(last) = path.segments.last_mut() {
760        last.ident = format_ident!("__conduit_handler_{}", last.ident);
761    }
762    quote! { #path }.into()
763}