Skip to main content

toolkit_zero_macros/
lib.rs

1//! Procedural macros for `toolkit-zero`.
2//!
3//! This crate is an internal implementation detail of `toolkit-zero`.
4//! Do not depend on it directly. Use the re-exported attribute macros:
5//!
6//! - [`mechanism`] — server-side route declaration, via `toolkit_zero::socket::server`
7//! - [`request`]   — client-side request shorthand, via `toolkit_zero::socket::client`
8
9#[cfg(any(feature = "socket-server", feature = "socket-client"))]
10use proc_macro::TokenStream;
11#[cfg(any(feature = "socket-server", feature = "socket-client"))]
12use quote::quote;
13#[cfg(any(feature = "socket-server", feature = "socket-client"))]
14use syn::{
15    parse::{Parse, ParseStream},
16    parse_macro_input, Expr, Ident, ItemFn, LitStr, Token,
17};
18
19// ─── Argument types ───────────────────────────────────────────────────────────
20
21#[cfg(feature = "socket-server")]
22/// The body/query mode keyword extracted from the attribute arguments.
23enum BodyMode {
24    None,
25    Json,
26    Query,
27    Encrypted(Expr),
28    EncryptedQuery(Expr),
29}
30
31#[cfg(feature = "socket-server")]
32/// Fully parsed attribute arguments.
33struct MechanismArgs {
34    /// The `Server` variable identifier in the enclosing scope.
35    server: Ident,
36    /// HTTP method identifier (GET, POST, …).
37    method: Ident,
38    /// Route path string literal.
39    path: LitStr,
40    /// Optional `state(expr)` argument.
41    state: Option<Expr>,
42    /// How the request body / query is processed.
43    body_mode: BodyMode,
44}
45
46#[cfg(feature = "socket-server")]
47impl Parse for MechanismArgs {
48    fn parse(input: ParseStream) -> syn::Result<Self> {
49        // ── Positional: server_ident, METHOD, "/path" ─────────────────────
50        let server: Ident = input.parse()?;
51        input.parse::<Token![,]>()?;
52
53        let method: Ident = input.parse()?;
54        input.parse::<Token![,]>()?;
55
56        let path: LitStr = input.parse()?;
57
58        // ── Named (order-independent) keywords ────────────────────────────
59        let mut state: Option<Expr> = None;
60        let mut body_mode = BodyMode::None;
61
62        while input.peek(Token![,]) {
63            input.parse::<Token![,]>()?;
64            if input.is_empty() {
65                break;
66            }
67
68            let kw: Ident = input.parse()?;
69            match kw.to_string().as_str() {
70                "json" => {
71                    if !matches!(body_mode, BodyMode::None) {
72                        return Err(syn::Error::new(
73                            kw.span(),
74                            "#[mechanism]: only one of `json`, `query`, \
75                             `encrypted(…)`, or `encrypted_query(…)` may be \
76                             specified per route",
77                        ));
78                    }
79                    body_mode = BodyMode::Json;
80                }
81                "query" => {
82                    if !matches!(body_mode, BodyMode::None) {
83                        return Err(syn::Error::new(
84                            kw.span(),
85                            "#[mechanism]: only one of `json`, `query`, \
86                             `encrypted(…)`, or `encrypted_query(…)` may be \
87                             specified per route",
88                        ));
89                    }
90                    body_mode = BodyMode::Query;
91                }
92                "state" => {
93                    if state.is_some() {
94                        return Err(syn::Error::new(
95                            kw.span(),
96                            "#[mechanism]: `state(…)` may only be specified once",
97                        ));
98                    }
99                    let content;
100                    syn::parenthesized!(content in input);
101                    state = Some(content.parse::<Expr>()?);
102                }
103                "encrypted" => {
104                    if !matches!(body_mode, BodyMode::None) {
105                        return Err(syn::Error::new(
106                            kw.span(),
107                            "#[mechanism]: only one of `json`, `query`, \
108                             `encrypted(…)`, or `encrypted_query(…)` may be \
109                             specified per route",
110                        ));
111                    }
112                    let content;
113                    syn::parenthesized!(content in input);
114                    body_mode = BodyMode::Encrypted(content.parse::<Expr>()?);
115                }
116                "encrypted_query" => {
117                    if !matches!(body_mode, BodyMode::None) {
118                        return Err(syn::Error::new(
119                            kw.span(),
120                            "#[mechanism]: only one of `json`, `query`, \
121                             `encrypted(…)`, or `encrypted_query(…)` may be \
122                             specified per route",
123                        ));
124                    }
125                    let content;
126                    syn::parenthesized!(content in input);
127                    body_mode = BodyMode::EncryptedQuery(content.parse::<Expr>()?);
128                }
129                other => {
130                    return Err(syn::Error::new(
131                        kw.span(),
132                        format!(
133                            "#[mechanism]: unknown keyword `{other}`. \
134                             Valid keywords: json, query, state(<expr>), \
135                             encrypted(<key>), encrypted_query(<key>)"
136                        ),
137                    ));
138                }
139            }
140        }
141
142        Ok(MechanismArgs { server, method, path, state, body_mode })
143    }
144}
145
146// ─── Helper ───────────────────────────────────────────────────────────────────
147
148#[cfg(feature = "socket-server")]
149/// Extract `(&Pat, &Type)` from a `FnArg::Typed`. Emits a proper error for
150/// `FnArg::Receiver` (i.e. `self`).
151fn extract_pat_ty<'a>(
152    arg: &'a syn::FnArg,
153    position: &str,
154) -> syn::Result<(&'a syn::Pat, &'a syn::Type)> {
155    match arg {
156        syn::FnArg::Typed(pt) => Ok((&pt.pat, &pt.ty)),
157        syn::FnArg::Receiver(r) => Err(syn::Error::new_spanned(
158            &r.self_token,
159            format!(
160                "#[mechanism]: unexpected `self` in the {position} parameter position"
161            ),
162        )),
163    }
164}
165
166// ─── Attribute macro ──────────────────────────────────────────────────────────
167
168#[cfg(feature = "socket-server")]
169/// Concise route declaration for `toolkit-zero` socket-server routes.
170///
171/// Replaces an `async fn` item with a `server.mechanism(…)` statement at the
172/// point of declaration. The function body is transplanted verbatim into the
173/// `.onconnect(…)` closure; all variables from the enclosing scope are
174/// accessible via `move` capture.
175///
176/// # Syntax
177///
178/// ```text
179/// #[mechanism(server, METHOD, "/path")]
180/// #[mechanism(server, METHOD, "/path", json)]
181/// #[mechanism(server, METHOD, "/path", query)]
182/// #[mechanism(server, METHOD, "/path", encrypted(key_expr))]
183/// #[mechanism(server, METHOD, "/path", encrypted_query(key_expr))]
184/// #[mechanism(server, METHOD, "/path", state(state_expr))]
185/// #[mechanism(server, METHOD, "/path", state(state_expr), json)]
186/// #[mechanism(server, METHOD, "/path", state(state_expr), query)]
187/// #[mechanism(server, METHOD, "/path", state(state_expr), encrypted(key_expr))]
188/// #[mechanism(server, METHOD, "/path", state(state_expr), encrypted_query(key_expr))]
189/// ```
190///
191/// The first three arguments (`server`, `METHOD`, `"/path"`) are positional.
192/// `json`, `query`, `state(…)`, `encrypted(…)`, and `encrypted_query(…)` may
193/// appear in any order after the path.
194///
195/// # Parameters
196///
197/// | Argument | Meaning |
198/// |---|---|
199/// | `server` | Identifier of the `Server` variable in the enclosing scope |
200/// | `METHOD` | HTTP method: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` |
201/// | `"/path"` | Route path string literal |
202/// | `json` | JSON-deserialised body; fn has one param `(body: T)` |
203/// | `query` | URL query params; fn has one param `(params: T)` |
204/// | `encrypted(key)` | VEIL-encrypted body; fn has one param `(body: T)` |
205/// | `encrypted_query(key)` | VEIL-encrypted query; fn has one param `(params: T)` |
206/// | `state(expr)` | State injection; fn first param is `(state: S)` |
207///
208/// When `state` is combined with a body mode the function receives two
209/// parameters: the state clone first, the body or query second.
210///
211/// # Function signature
212///
213/// The decorated function:
214/// - May be `async` or non-async — it is always wrapped in `async move { … }`.
215/// - May carry a return type annotation or none — it is ignored; Rust infers
216///   the return type from the `reply!` macro inside the body.
217/// - Must have exactly the number of parameters described in the table above.
218///
219/// # Example
220///
221/// ```rust,ignore
222/// use toolkit_zero::socket::server::{Server, mechanism, reply, Status, SerializationKey};
223/// use serde::{Deserialize, Serialize};
224/// use std::sync::{Arc, Mutex};
225///
226/// #[derive(Deserialize, Serialize, Clone)] struct Item { id: u32, name: String }
227/// #[derive(Deserialize)]                  struct NewItem { name: String }
228/// #[derive(Deserialize)]                  struct Filter { page: u32 }
229///
230/// #[tokio::main]
231/// async fn main() {
232///     let mut server = Server::default();
233///     let db: Arc<Mutex<Vec<Item>>> = Arc::new(Mutex::new(vec![]));
234///
235///     // Plain GET — no body, no state
236///     #[mechanism(server, GET, "/health")]
237///     async fn health() { reply!() }
238///
239///     // POST — JSON body
240///     #[mechanism(server, POST, "/items", json)]
241///     async fn create_item(body: NewItem) {
242///         reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
243///     }
244///
245///     // GET — query params
246///     #[mechanism(server, GET, "/items", query)]
247///     async fn list_items(filter: Filter) {
248///         let _ = filter.page;
249///         reply!()
250///     }
251///
252///     // GET — state + query
253///     #[mechanism(server, GET, "/items/all", state(db.clone()), query)]
254///     async fn list_all(db: Arc<Mutex<Vec<Item>>>, filter: Filter) {
255///         let items = db.lock().unwrap().clone();
256///         reply!(json => items)
257///     }
258///
259///     // POST — state + JSON body
260///     #[mechanism(server, POST, "/items/add", state(db.clone()), json)]
261///     async fn add_item(db: Arc<Mutex<Vec<Item>>>, body: NewItem) {
262///         let id = db.lock().unwrap().len() as u32 + 1;
263///         let item = Item { id, name: body.name };
264///         db.lock().unwrap().push(item.clone());
265///         reply!(json => item, status => Status::Created)
266///     }
267///
268///     // POST — VEIL-encrypted body
269///     #[mechanism(server, POST, "/secure", encrypted(SerializationKey::Default))]
270///     async fn secure_post(body: NewItem) {
271///         reply!(json => Item { id: 99, name: body.name })
272///     }
273///
274///     server.serve(([127, 0, 0, 1], 8080)).await;
275/// }
276/// ```
277#[cfg(feature = "socket-server")]
278#[proc_macro_attribute]
279pub fn mechanism(attr: TokenStream, item: TokenStream) -> TokenStream {
280    let args = parse_macro_input!(attr as MechanismArgs);
281    let func = parse_macro_input!(item as ItemFn);
282
283    // ── Validate and normalise the HTTP method ────────────────────────────
284    let method_str = args.method.to_string().to_lowercase();
285    let method_ident = Ident::new(&method_str, args.method.span());
286
287    match method_str.as_str() {
288        "get" | "post" | "put" | "delete" | "patch" | "head" | "options" => {}
289        other => {
290            return syn::Error::new(
291                args.method.span(),
292                format!(
293                    "#[mechanism]: `{other}` is not a valid HTTP method. \
294                     Expected GET, POST, PUT, DELETE, PATCH, HEAD, or OPTIONS."
295                ),
296            )
297            .to_compile_error()
298            .into();
299        }
300    }
301
302    let server = &args.server;
303    let path   = &args.path;
304    let body   = &func.block;
305    let params: Vec<&syn::FnArg> = func.sig.inputs.iter().collect();
306
307    // ── Convenience macro: emit a compile error and return ────────────────
308    macro_rules! bail {
309        ($span:expr, $msg:literal) => {{
310            return syn::Error::new($span, $msg)
311                .to_compile_error()
312                .into();
313        }};
314    }
315
316    // ── Build the builder-chain expression ────────────────────────────────
317    let route_expr = match (&args.state, &args.body_mode) {
318
319        // ── Plain ─────────────────────────────────────────────────────────
320        (None, BodyMode::None) => {
321            quote! {
322                toolkit_zero::socket::server::ServerMechanism::#method_ident(#path)
323                    .onconnect(|| async move #body)
324            }
325        }
326
327        // ── JSON body, no state ───────────────────────────────────────────
328        (None, BodyMode::Json) => {
329            if params.is_empty() {
330                bail!(
331                    func.sig.ident.span(),
332                    "#[mechanism]: `json` mode requires one function parameter — `(body: YourType)`"
333                );
334            }
335            let (name, ty) = match extract_pat_ty(params[0], "body") {
336                Ok(v) => v,
337                Err(e) => return e.to_compile_error().into(),
338            };
339            quote! {
340                toolkit_zero::socket::server::ServerMechanism::#method_ident(#path)
341                    .json::<#ty>()
342                    .onconnect(|#name: #ty| async move #body)
343            }
344        }
345
346        // ── Query params, no state ────────────────────────────────────────
347        (None, BodyMode::Query) => {
348            if params.is_empty() {
349                bail!(
350                    func.sig.ident.span(),
351                    "#[mechanism]: `query` mode requires one function parameter — `(params: YourType)`"
352                );
353            }
354            let (name, ty) = match extract_pat_ty(params[0], "query") {
355                Ok(v) => v,
356                Err(e) => return e.to_compile_error().into(),
357            };
358            quote! {
359                toolkit_zero::socket::server::ServerMechanism::#method_ident(#path)
360                    .query::<#ty>()
361                    .onconnect(|#name: #ty| async move #body)
362            }
363        }
364
365        // ── VEIL-encrypted body, no state ─────────────────────────────────
366        (None, BodyMode::Encrypted(key_expr)) => {
367            if params.is_empty() {
368                bail!(
369                    func.sig.ident.span(),
370                    "#[mechanism]: `encrypted(key)` mode requires one function parameter — `(body: YourType)`"
371                );
372            }
373            let (name, ty) = match extract_pat_ty(params[0], "body") {
374                Ok(v) => v,
375                Err(e) => return e.to_compile_error().into(),
376            };
377            quote! {
378                toolkit_zero::socket::server::ServerMechanism::#method_ident(#path)
379                    .encryption::<#ty>(#key_expr)
380                    .onconnect(|#name: #ty| async move #body)
381            }
382        }
383
384        // ── VEIL-encrypted query, no state ────────────────────────────────
385        (None, BodyMode::EncryptedQuery(key_expr)) => {
386            if params.is_empty() {
387                bail!(
388                    func.sig.ident.span(),
389                    "#[mechanism]: `encrypted_query(key)` mode requires one function parameter — `(params: YourType)`"
390                );
391            }
392            let (name, ty) = match extract_pat_ty(params[0], "params") {
393                Ok(v) => v,
394                Err(e) => return e.to_compile_error().into(),
395            };
396            quote! {
397                toolkit_zero::socket::server::ServerMechanism::#method_ident(#path)
398                    .encrypted_query::<#ty>(#key_expr)
399                    .onconnect(|#name: #ty| async move #body)
400            }
401        }
402
403        // ── State only ────────────────────────────────────────────────────
404        (Some(state_expr), BodyMode::None) => {
405            if params.is_empty() {
406                bail!(
407                    func.sig.ident.span(),
408                    "#[mechanism]: `state(expr)` mode requires one function parameter — `(state: YourStateType)`"
409                );
410            }
411            let (name, ty) = match extract_pat_ty(params[0], "state") {
412                Ok(v) => v,
413                Err(e) => return e.to_compile_error().into(),
414            };
415            quote! {
416                toolkit_zero::socket::server::ServerMechanism::#method_ident(#path)
417                    .state(#state_expr)
418                    .onconnect(|#name: #ty| async move #body)
419            }
420        }
421
422        // ── State + JSON body ─────────────────────────────────────────────
423        (Some(state_expr), BodyMode::Json) => {
424            if params.len() < 2 {
425                bail!(
426                    func.sig.ident.span(),
427                    "#[mechanism]: `state(expr), json` mode requires two function parameters — `(state: S, body: T)`"
428                );
429            }
430            let (s_name, s_ty) = match extract_pat_ty(params[0], "state") {
431                Ok(v) => v,
432                Err(e) => return e.to_compile_error().into(),
433            };
434            let (b_name, b_ty) = match extract_pat_ty(params[1], "body") {
435                Ok(v) => v,
436                Err(e) => return e.to_compile_error().into(),
437            };
438            quote! {
439                toolkit_zero::socket::server::ServerMechanism::#method_ident(#path)
440                    .state(#state_expr)
441                    .json::<#b_ty>()
442                    .onconnect(|#s_name: #s_ty, #b_name: #b_ty| async move #body)
443            }
444        }
445
446        // ── State + Query params ──────────────────────────────────────────
447        (Some(state_expr), BodyMode::Query) => {
448            if params.len() < 2 {
449                bail!(
450                    func.sig.ident.span(),
451                    "#[mechanism]: `state(expr), query` mode requires two function parameters — `(state: S, params: T)`"
452                );
453            }
454            let (s_name, s_ty) = match extract_pat_ty(params[0], "state") {
455                Ok(v) => v,
456                Err(e) => return e.to_compile_error().into(),
457            };
458            let (q_name, q_ty) = match extract_pat_ty(params[1], "query") {
459                Ok(v) => v,
460                Err(e) => return e.to_compile_error().into(),
461            };
462            quote! {
463                toolkit_zero::socket::server::ServerMechanism::#method_ident(#path)
464                    .state(#state_expr)
465                    .query::<#q_ty>()
466                    .onconnect(|#s_name: #s_ty, #q_name: #q_ty| async move #body)
467            }
468        }
469
470        // ── State + VEIL-encrypted body ───────────────────────────────────
471        (Some(state_expr), BodyMode::Encrypted(key_expr)) => {
472            if params.len() < 2 {
473                bail!(
474                    func.sig.ident.span(),
475                    "#[mechanism]: `state(expr), encrypted(key)` mode requires two function parameters — `(state: S, body: T)`"
476                );
477            }
478            let (s_name, s_ty) = match extract_pat_ty(params[0], "state") {
479                Ok(v) => v,
480                Err(e) => return e.to_compile_error().into(),
481            };
482            let (b_name, b_ty) = match extract_pat_ty(params[1], "body") {
483                Ok(v) => v,
484                Err(e) => return e.to_compile_error().into(),
485            };
486            quote! {
487                toolkit_zero::socket::server::ServerMechanism::#method_ident(#path)
488                    .state(#state_expr)
489                    .encryption::<#b_ty>(#key_expr)
490                    .onconnect(|#s_name: #s_ty, #b_name: #b_ty| async move #body)
491            }
492        }
493
494        // ── State + VEIL-encrypted query ──────────────────────────────────
495        (Some(state_expr), BodyMode::EncryptedQuery(key_expr)) => {
496            if params.len() < 2 {
497                bail!(
498                    func.sig.ident.span(),
499                    "#[mechanism]: `state(expr), encrypted_query(key)` mode requires two function parameters — `(state: S, params: T)`"
500                );
501            }
502            let (s_name, s_ty) = match extract_pat_ty(params[0], "state") {
503                Ok(v) => v,
504                Err(e) => return e.to_compile_error().into(),
505            };
506            let (q_name, q_ty) = match extract_pat_ty(params[1], "query") {
507                Ok(v) => v,
508                Err(e) => return e.to_compile_error().into(),
509            };
510            quote! {
511                toolkit_zero::socket::server::ServerMechanism::#method_ident(#path)
512                    .state(#state_expr)
513                    .encrypted_query::<#q_ty>(#key_expr)
514                    .onconnect(|#s_name: #s_ty, #q_name: #q_ty| async move #body)
515            }
516        }
517    };
518
519    // ── Final expansion: server.mechanism(<route_expr>); ──────────────────
520    quote! {
521        #server.mechanism(#route_expr);
522    }
523    .into()
524}
525
526// ─── Client-side: #[request] ─────────────────────────────────────────────────
527
528#[cfg(feature = "socket-client")]
529/// Body/query attachment mode for a client request.
530enum RequestBodyMode {
531    None,
532    Json(Expr),
533    Query(Expr),
534    Encrypted(Expr, Expr),
535    EncryptedQuery(Expr, Expr),
536}
537
538#[cfg(feature = "socket-client")]
539/// Whether to call `.send().await?` or `.send_sync()?`.
540enum SendMode {
541    Async,
542    Sync,
543}
544
545#[cfg(feature = "socket-client")]
546/// Fully parsed `#[request]` attribute arguments.
547struct RequestArgs {
548    client: Ident,
549    method: Ident,
550    path:   LitStr,
551    mode:   RequestBodyMode,
552    send:   SendMode,
553}
554
555#[cfg(feature = "socket-client")]
556/// Parse the mandatory final `, async` or `, sync` keyword.
557fn parse_send_mode(input: ParseStream) -> syn::Result<SendMode> {
558    input.parse::<Token![,]>()?;
559    if input.peek(Token![async]) {
560        input.parse::<Token![async]>()?;
561        Ok(SendMode::Async)
562    } else {
563        let kw: Ident = input.parse()?;
564        match kw.to_string().as_str() {
565            "sync" => Ok(SendMode::Sync),
566            _ => Err(syn::Error::new(
567                kw.span(),
568                "#[request]: expected `async` or `sync` as the final argument",
569            )),
570        }
571    }
572}
573
574#[cfg(feature = "socket-client")]
575impl Parse for RequestArgs {
576    fn parse(input: ParseStream) -> syn::Result<Self> {
577        // ── Positional: client_ident, METHOD, "/path" ─────────────────────
578        let client: Ident = input.parse()?;
579        input.parse::<Token![,]>()?;
580
581        let method: Ident = input.parse()?;
582        input.parse::<Token![,]>()?;
583
584        let path: LitStr = input.parse()?;
585        input.parse::<Token![,]>()?;
586
587        // ── Optional mode keyword + mandatory send mode ───────────────────
588        let (mode, send) = if input.peek(Token![async]) {
589            input.parse::<Token![async]>()?;
590            (RequestBodyMode::None, SendMode::Async)
591        } else {
592            let kw: Ident = input.parse()?;
593            match kw.to_string().as_str() {
594                "sync" => (RequestBodyMode::None, SendMode::Sync),
595                "json" => {
596                    let content;
597                    syn::parenthesized!(content in input);
598                    let expr: Expr = content.parse()?;
599                    let send = parse_send_mode(input)?;
600                    (RequestBodyMode::Json(expr), send)
601                }
602                "query" => {
603                    let content;
604                    syn::parenthesized!(content in input);
605                    let expr: Expr = content.parse()?;
606                    let send = parse_send_mode(input)?;
607                    (RequestBodyMode::Query(expr), send)
608                }
609                "encrypted" => {
610                    let content;
611                    syn::parenthesized!(content in input);
612                    let body: Expr = content.parse()?;
613                    content.parse::<Token![,]>()?;
614                    let key: Expr = content.parse()?;
615                    let send = parse_send_mode(input)?;
616                    (RequestBodyMode::Encrypted(body, key), send)
617                }
618                "encrypted_query" => {
619                    let content;
620                    syn::parenthesized!(content in input);
621                    let params: Expr = content.parse()?;
622                    content.parse::<Token![,]>()?;
623                    let key: Expr = content.parse()?;
624                    let send = parse_send_mode(input)?;
625                    (RequestBodyMode::EncryptedQuery(params, key), send)
626                }
627                other => {
628                    return Err(syn::Error::new(
629                        kw.span(),
630                        format!(
631                            "#[request]: unknown keyword `{other}`. \
632                             Valid modes: json(<expr>), query(<expr>), \
633                             encrypted(<body>, <key>), encrypted_query(<params>, <key>). \
634                             Final argument must be `async` or `sync`."
635                        ),
636                    ));
637                }
638            }
639        };
640
641        Ok(RequestArgs { client, method, path, mode, send })
642    }
643}
644
645/// Concise HTTP client request for `toolkit-zero` socket-client routes.
646///
647/// Replaces a decorated `fn` item with a `let` binding statement that performs
648/// the HTTP request inline.  The function name becomes the binding name; the
649/// return type annotation is used as the response type `R` in `.send::<R>()`.
650/// The function body is discarded entirely.
651///
652/// # Syntax
653///
654/// ```text
655/// #[request(client, METHOD, "/path", async|sync)]
656/// #[request(client, METHOD, "/path", json(<body_expr>), async|sync)]
657/// #[request(client, METHOD, "/path", query(<params_expr>), async|sync)]
658/// #[request(client, METHOD, "/path", encrypted(<body_expr>, <key_expr>), async|sync)]
659/// #[request(client, METHOD, "/path", encrypted_query(<params_expr>, <key_expr>), async|sync)]
660/// ```
661///
662/// # Parameters
663///
664/// | Argument | Meaning |
665/// |---|---|
666/// | `client` | The [`Client`](toolkit_zero::socket::client::Client) variable in the enclosing scope |
667/// | `METHOD` | HTTP method: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` |
668/// | `"/path"` | Endpoint path string literal |
669/// | `json(expr)` | Serialise `expr` as a JSON body (`Content-Type: application/json`) |
670/// | `query(expr)` | Serialise `expr` as URL query parameters |
671/// | `encrypted(body, key)` | VEIL-seal `body` with a [`SerializationKey`](toolkit_zero::socket::SerializationKey) |
672/// | `encrypted_query(params, key)` | VEIL-seal `params`, send as `?data=<base64url>` |
673/// | `async` | Finalise with `.send::<R>().await?` |
674/// | `sync`  | Finalise with `.send_sync::<R>()?` |
675///
676/// The function **must** carry an explicit return type — it becomes `R` in the turbofish.
677/// The enclosing function must return a `Result<_, E>` where `E` implements the relevant
678/// `From` for `?` to propagate: `reqwest::Error` for plain/json/query, or
679/// `ClientError` for encrypted variants.
680///
681/// # Example
682///
683/// ```rust,ignore
684/// use toolkit_zero::socket::client::{Client, Target, request};
685/// use serde::{Deserialize, Serialize};
686///
687/// #[derive(Deserialize, Serialize, Clone)] struct Item   { id: u32, name: String }
688/// #[derive(Serialize)]                     struct NewItem { name: String }
689/// #[derive(Serialize)]                     struct Filter  { page: u32 }
690///
691/// async fn example() -> Result<(), reqwest::Error> {
692///     let client = Client::new_async(Target::Localhost(8080));
693///
694///     // Plain async GET → let items: Vec<Item> = client.get("/items").send::<Vec<Item>>().await?
695///     #[request(client, GET, "/items", async)]
696///     async fn items() -> Vec<Item> {}
697///
698///     // POST with JSON body
699///     #[request(client, POST, "/items", json(NewItem { name: "widget".into() }), async)]
700///     async fn created() -> Item {}
701///
702///     // GET with query params
703///     #[request(client, GET, "/items", query(Filter { page: 2 }), async)]
704///     async fn page() -> Vec<Item> {}
705///
706///     // Sync DELETE
707///     #[request(client, DELETE, "/items/1", sync)]
708///     fn deleted() -> Item {}
709///
710///     Ok(())
711/// }
712/// ```
713#[cfg(feature = "socket-client")]
714#[proc_macro_attribute]
715pub fn request(attr: TokenStream, item: TokenStream) -> TokenStream {
716    let args = parse_macro_input!(attr as RequestArgs);
717    let func = parse_macro_input!(item as ItemFn);
718
719    // ── Validate HTTP method ──────────────────────────────────────────────
720    let method_str = args.method.to_string().to_lowercase();
721    let method_ident = Ident::new(&method_str, args.method.span());
722
723    match method_str.as_str() {
724        "get" | "post" | "put" | "delete" | "patch" | "head" | "options" => {}
725        other => {
726            return syn::Error::new(
727                args.method.span(),
728                format!(
729                    "#[request]: `{other}` is not a valid HTTP method. \
730                     Expected GET, POST, PUT, DELETE, PATCH, HEAD, or OPTIONS."
731                ),
732            )
733            .to_compile_error()
734            .into();
735        }
736    }
737
738    let client   = &args.client;
739    let path     = &args.path;
740    let var_name = &func.sig.ident;
741
742    // ── Return type — required; used as turbofish argument ────────────────
743    let ret_ty = match &func.sig.output {
744        syn::ReturnType::Type(_, ty) => ty.as_ref(),
745        syn::ReturnType::Default => {
746            return syn::Error::new(
747                func.sig.ident.span(),
748                "#[request]: a return type is required — it specifies the response type `R` \
749                 in `.send::<R>()`. Example: `async fn my_var() -> Vec<MyType> {}`",
750            )
751            .to_compile_error()
752            .into();
753        }
754    };
755
756    // ── Build the partial builder chain (without the send call) ──────────
757    let chain = match &args.mode {
758        RequestBodyMode::None => quote! {
759            #client.#method_ident(#path)
760        },
761        RequestBodyMode::Json(expr) => quote! {
762            #client.#method_ident(#path).json(#expr)
763        },
764        RequestBodyMode::Query(expr) => quote! {
765            #client.#method_ident(#path).query(#expr)
766        },
767        RequestBodyMode::Encrypted(body_expr, key_expr) => quote! {
768            #client.#method_ident(#path).encryption(#body_expr, #key_expr)
769        },
770        RequestBodyMode::EncryptedQuery(params_expr, key_expr) => quote! {
771            #client.#method_ident(#path).encrypted_query(#params_expr, #key_expr)
772        },
773    };
774
775    // ── Final let-binding statement ───────────────────────────────────────
776    match &args.send {
777        SendMode::Async => quote! {
778            let #var_name: #ret_ty = #chain.send::<#ret_ty>().await?;
779        },
780        SendMode::Sync => quote! {
781            let #var_name: #ret_ty = #chain.send_sync::<#ret_ty>()?;
782        },
783    }
784    .into()
785}