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}