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}