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