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 — all macros are re-exported through the
5//! relevant `toolkit-zero` module:
6//!
7//! | Macro | Enable with | Import from |
8//! |---|---|---|
9//! | [`mechanism`] | `features = ["socket-server"]` | `toolkit_zero::socket::server` |
10//! | [`request`] | `features = ["socket-client"]` | `toolkit_zero::socket::client` |
11//! | [`serializable`] | `features = ["serialization"]` | `toolkit_zero::serialization` |
12//! | [`serialize`] | `features = ["serialization"]` | `toolkit_zero::serialization` |
13//! | [`deserialize`] | `features = ["serialization"]` | `toolkit_zero::serialization` |
14//! | [`browser`] | `features = ["location"]` | `toolkit_zero::location` |
15//! | [`timelock`] | any `enc-timelock-*` feature | `toolkit_zero::encryption::timelock` |
16//! | [`dependencies`] | `features = ["dependency-graph-capture"]` | `toolkit_zero::dependency_graph::capture` |
17//!
18//! ---
19//!
20//! # `#[mechanism]` — server-side route declaration
21//!
22//! Replaces a decorated `fn` item with a `server.mechanism(…)` builder
23//! statement. The function body is transplanted verbatim into the
24//! `.onconnect(…)` closure; all variables from the enclosing scope are
25//! accessible via `move` capture.
26//!
27//! ## Syntax
28//!
29//! ```text
30//! #[mechanism(server, METHOD, "/path")]
31//! #[mechanism(server, METHOD, "/path", json)]
32//! #[mechanism(server, METHOD, "/path", query)]
33//! #[mechanism(server, METHOD, "/path", encrypted(<key_expr>))]
34//! #[mechanism(server, METHOD, "/path", encrypted_query(<key_expr>))]
35//! #[mechanism(server, METHOD, "/path", state(<state_expr>))]
36//! #[mechanism(server, METHOD, "/path", state(<state_expr>), json)]
37//! #[mechanism(server, METHOD, "/path", state(<state_expr>), query)]
38//! #[mechanism(server, METHOD, "/path", state(<state_expr>), encrypted(<key_expr>))]
39//! #[mechanism(server, METHOD, "/path", state(<state_expr>), encrypted_query(<key_expr>))]
40//! ```
41//!
42//! The first three arguments (`server`, `METHOD`, `"/path"`) are positional
43//! and required. Keywords after the path (`json`, `query`, `state(…)`,
44//! `encrypted(…)`, `encrypted_query(…)`) may appear in **any order**.
45//!
46//! ## Parameters
47//!
48//! | Argument | Type | Description |
49//! |---|---|---|
50//! | `server` | ident | The [`Server`](toolkit_zero::socket::server::Server) variable in scope |
51//! | `METHOD` | ident | HTTP verb: `GET` `POST` `PUT` `DELETE` `PATCH` `HEAD` `OPTIONS` |
52//! | `"/path"` | string literal | Route path |
53//! | `json` | keyword | Deserialise JSON body; fn receives `(body: T)` |
54//! | `query` | keyword | Deserialise URL query params; fn receives `(params: T)` |
55//! | `encrypted(key)` | keyword + expr | Decrypt body (ChaCha20-Poly1305); fn receives `(body: T)` |
56//! | `encrypted_query(key)` | keyword + expr | Decrypt query (ChaCha20-Poly1305); fn receives `(params: T)` |
57//! | `state(expr)` | keyword + expr | Clone state into handler; fn first param is `(state: S)` |
58//!
59//! When `state` is combined with a body mode, the function receives two
60//! parameters: state first, then body/params.
61//!
62//! ## Function signature rules
63//!
64//! - The function **may** be `async` or non-async — it is always wrapped in `async move { … }`.
65//! - The return type annotation is ignored; Rust infers it from the `reply!` macro inside the body.
66//! - The number of parameters must match the chosen mode exactly (compile error otherwise).
67//!
68//! ## What the macro expands to
69//!
70//! ```rust,ignore
71//! // Input:
72//! #[mechanism(server, POST, "/items", json)]
73//! async fn create(body: NewItem) {
74//!     reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
75//! }
76//!
77//! // Expanded to:
78//! server.mechanism(
79//!     toolkit_zero::socket::server::ServerMechanism::post("/items")
80//!         .json::<NewItem>()
81//!         .onconnect(|body: NewItem| async move {
82//!             reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
83//!         })
84//! );
85//! ```
86//!
87//! ## Example
88//!
89//! ```rust,ignore
90//! use toolkit_zero::socket::server::{Server, mechanism, reply, Status, SerializationKey};
91//! use serde::{Deserialize, Serialize};
92//! use std::sync::{Arc, Mutex};
93//!
94//! #[derive(Deserialize, Serialize, Clone)] struct Item    { id: u32, name: String }
95//! #[derive(Deserialize)]                   struct NewItem  { name: String }
96//! #[derive(Deserialize)]                   struct Filter   { page: u32 }
97//!
98//! #[tokio::main]
99//! async fn main() {
100//!     let mut server = Server::default();
101//!     let db: Arc<Mutex<Vec<Item>>> = Arc::new(Mutex::new(vec![]));
102//!
103//!     // Plain GET — no body
104//!     #[mechanism(server, GET, "/health")]
105//!     async fn health() { reply!() }
106//!
107//!     // JSON body
108//!     #[mechanism(server, POST, "/items", json)]
109//!     async fn create(body: NewItem) {
110//!         reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
111//!     }
112//!
113//!     // URL query params
114//!     #[mechanism(server, GET, "/items", query)]
115//!     async fn list(filter: Filter) {
116//!         let _ = filter.page;
117//!         reply!()
118//!     }
119//!
120//!     // Shared state + JSON body
121//!     #[mechanism(server, POST, "/items/add", state(db.clone()), json)]
122//!     async fn add(db: Arc<Mutex<Vec<Item>>>, body: NewItem) {
123//!         let id = db.lock().unwrap().len() as u32 + 1;
124//!         db.lock().unwrap().push(Item { id, name: body.name.clone() });
125//!         reply!(json => Item { id, name: body.name }, status => Status::Created)
126//!     }
127//!
128//!     // ChaCha20-Poly1305-encrypted body
129//!     #[mechanism(server, POST, "/secure", encrypted(SerializationKey::Default))]
130//!     async fn secure(body: NewItem) {
131//!         reply!(json => Item { id: 99, name: body.name })
132//!     }
133//!
134//!     server.serve(([127, 0, 0, 1], 8080)).await;
135//! }
136//! ```
137//!
138//! ---
139//!
140//! # `#[request]` — client-side request shorthand
141//!
142//! Replaces a decorated `fn` item with an inline `let` binding that performs
143//! an HTTP request. The **function name** becomes the binding name; the
144//! **return type** becomes `R` in the `.send::<R>()` turbofish. The function
145//! body is discarded entirely.
146//!
147//! ## Syntax
148//!
149//! ```text
150//! #[request(client, METHOD, "/path", async|sync)]
151//! #[request(client, METHOD, "/path", json(<body_expr>), async|sync)]
152//! #[request(client, METHOD, "/path", query(<params_expr>), async|sync)]
153//! #[request(client, METHOD, "/path", encrypted(<body_expr>, <key_expr>), async|sync)]
154//! #[request(client, METHOD, "/path", encrypted_query(<params_expr>, <key_expr>), async|sync)]
155//! ```
156//!
157//! The first three arguments are positional. The mode keyword (if any) comes
158//! before the mandatory `async` or `sync` terminator.
159//!
160//! ## Parameters
161//!
162//! | Argument | Description |
163//! |---|---|
164//! | `client` | The [`Client`](toolkit_zero::socket::client::Client) variable in scope |
165//! | `METHOD` | HTTP verb: `GET` `POST` `PUT` `DELETE` `PATCH` `HEAD` `OPTIONS` |
166//! | `"/path"` | Endpoint path string literal |
167//! | `json(expr)` | Serialise `expr` as a JSON body (`Content-Type: application/json`) |
168//! | `query(expr)` | Serialise `expr` as URL query parameters |
169//! | `encrypted(body, key)` | Seal `body` (ChaCha20-Poly1305) with `key` before sending |
170//! | `encrypted_query(params, key)` | Seal `params` (ChaCha20-Poly1305), send as `?data=<base64url>` |
171//! | `async` | Finalise with `.send::<R>().await?` |
172//! | `sync` | Finalise with `.send_sync::<R>()?` |
173//!
174//! ## Requirements
175//!
176//! - A **return type annotation is required** — it is used as `R` in the turbofish.
177//!   Omitting it is a compile error.
178//! - The enclosing function must return `Result<_, E>` where `E: From<reqwest::Error>`
179//!   (plain/json/query modes) or `E: From<ClientError>` (encrypted modes), so that
180//!   `?` can propagate.
181//!
182//! ## What the macro expands to
183//!
184//! ```rust,ignore
185//! // Input:
186//! #[request(client, POST, "/items", json(NewItem { name: "widget".into() }), async)]
187//! async fn created() -> Item {}
188//!
189//! // Expanded to:
190//! let created: Item = client.post("/items")
191//!     .json(NewItem { name: "widget".into() })
192//!     .send::<Item>()
193//!     .await?;
194//! ```
195//!
196//! ## Example
197//!
198//! ```rust,ignore
199//! use toolkit_zero::socket::client::{Client, Target, request};
200//! use serde::{Deserialize, Serialize};
201//!
202//! #[derive(Deserialize, Serialize)] struct Item    { id: u32, name: String }
203//! #[derive(Serialize)]              struct NewItem  { name: String }
204//! #[derive(Serialize)]              struct Filter   { page: u32 }
205//!
206//! async fn example() -> Result<(), reqwest::Error> {
207//!     let client = Client::new_async(Target::Localhost(8080));
208//!
209//!     // GET → let items: Vec<Item> = client.get("/items").send::<Vec<Item>>().await?
210//!     #[request(client, GET, "/items", async)]
211//!     async fn items() -> Vec<Item> {}
212//!
213//!     // POST with JSON body
214//!     #[request(client, POST, "/items", json(NewItem { name: "widget".into() }), async)]
215//!     async fn created() -> Item {}
216//!
217//!     // GET with query params
218//!     #[request(client, GET, "/items", query(Filter { page: 2 }), async)]
219//!     async fn page() -> Vec<Item> {}
220//!
221//!     // Synchronous DELETE
222//!     #[request(client, DELETE, "/items/1", sync)]
223//!     fn deleted() -> Item {}
224//!
225//!     Ok(())
226//! }
227//! ```
228//!
229//! ---
230//!
231//! # `#[serializable]` — derive + inject seal/open
232//!
233//! Automatically derives `bincode::Encode + bincode::Decode` on a struct or
234//! enum and injects three methods:
235//!
236//! ```text
237//! fn seal(&self, key: Option<String>) -> Result<Vec<u8>, SerializationError>
238//! fn open(bytes: &[u8], key: Option<String>) -> Result<Self, SerializationError>
239//! ```
240//!
241//! The `key` is **moved in** and internally wrapped in `Zeroizing<String>`,
242//! wiping it from memory on drop.  Pass `None` to use the built-in default key.
243//!
244//! Field-level `#[serializable(key = "literal")]` additionally generates
245//! per-field helpers with the key baked in:
246//!
247//! ```text
248//! fn seal_<field>(&self)        -> Result<Vec<u8>, SerializationError>
249//! fn open_<field>(bytes: &[u8]) -> Result<FieldType, SerializationError>
250//! ```
251//!
252//! ## Syntax
253//!
254//! ```text
255//! #[serializable]                      // on a struct or enum
256//! struct Foo { … }
257//!
258//! #[serializable]                      // field annotation inside a struct
259//! struct Bar {
260//!     pub normal: String,
261//!     #[serializable(key = "my-key")]  // generates seal_secret / open_secret
262//!     pub secret: String,
263//! }
264//! ```
265//!
266//! > **Note:** `#[serializable]` on an enum does not scan fields for the
267//! > field-level annotation — only named struct fields are supported for
268//! > per-field helpers.
269//!
270//! ## What the macro expands to
271//!
272//! ```rust,ignore
273//! // Input:
274//! #[serializable]
275//! struct Config { host: String, port: u16 }
276//!
277//! // Expanded to:
278//! #[derive(::toolkit_zero::serialization::Encode, ::toolkit_zero::serialization::Decode)]
279//! struct Config { host: String, port: u16 }
280//!
281//! impl Config {
282//!     pub fn seal(&self, key: Option<String>) -> Result<Vec<u8>, SerializationError> {
283//!         ::toolkit_zero::serialization::seal(self, key)
284//!     }
285//!     pub fn open(bytes: &[u8], key: Option<String>) -> Result<Self, SerializationError> {
286//!         ::toolkit_zero::serialization::open::<Self>(bytes, key)
287//!     }
288//! }
289//! ```
290//!
291//! ## Example
292//!
293//! ```rust,ignore
294//! use toolkit_zero::serialization::serializable;
295//!
296//! #[serializable]
297//! struct Config { host: String, port: u16 }
298//!
299//! let c = Config { host: "localhost".into(), port: 8080 };
300//!
301//! // Seal / open via struct methods
302//! let blob = c.seal(None).unwrap();
303//! let back = Config::open(&blob, None).unwrap();
304//! assert_eq!(c.host, back.host);
305//!
306//! // Custom key — moved in, zeroized on drop
307//! let blob2 = c.seal(Some("secret".to_string())).unwrap();
308//! let back2 = Config::open(&blob2, Some("secret".to_string())).unwrap();
309//!
310//! // Per-field annotation
311//! #[serializable]
312//! struct Creds {
313//!     pub user: String,
314//!     #[serializable(key = "field-secret")]
315//!     pub password: String,
316//! }
317//!
318//! let creds = Creds { user: "alice".into(), password: "hunter2".into() };
319//! let pw_blob = creds.seal_password().unwrap();    // key baked in
320//! let pw_back = Creds::open_password(&pw_blob).unwrap();
321//! assert_eq!("hunter2", pw_back);
322//! ```
323//!
324//! ---
325//!
326//! # `#[serialize]` — inline seal statement
327//!
328//! Replaces a `fn` item with an inline seal statement. Two modes are
329//! selected by the presence or absence of `path`:
330//!
331//! - **Variable mode** (`path` absent) — emits a `let` binding. The function
332//!   name becomes the variable name; the **return type is required** and
333//!   becomes the type annotation. The function body is discarded.
334//! - **File write mode** (`path = "..."` present) — emits
335//!   `std::fs::write(path, seal(…)?)?`. The function name and return type are
336//!   ignored.
337//!
338//! ## Syntax
339//!
340//! ```text
341//! // Variable mode
342//! #[serialize(source_expr)]
343//! #[serialize(source_expr, key = key_expr)]
344//!
345//! // File write mode  
346//! #[serialize(source_expr, path = "file.bin")]
347//! #[serialize(source_expr, path = "file.bin", key = key_expr)]
348//! ```
349//!
350//! | Argument | Required | Description |
351//! |---|---|---|
352//! | `source_expr` | yes | Expression to seal (must be `bincode::Encode`) |
353//! | `key = expr` | no | Key expression — `Option<String>` moved into `seal()` |
354//! | `path = "..."` | no | File path — switches to file write mode |
355//!
356//! `key` and `path` may appear in any order after `source_expr`.
357//!
358//! ## What the macro expands to
359//!
360//! ```rust,ignore
361//! // Variable mode:
362//! #[serialize(cfg, key = my_key)]
363//! fn blob() -> Vec<u8> {}
364//! // →
365//! let blob: Vec<u8> = ::toolkit_zero::serialization::seal(&cfg, Some(my_key))?;
366//!
367//! // Variable mode, default key:
368//! #[serialize(cfg)]
369//! fn blob() -> Vec<u8> {}
370//! // →
371//! let blob: Vec<u8> = ::toolkit_zero::serialization::seal(&cfg, None)?;
372//!
373//! // File write mode:
374//! #[serialize(cfg, path = "out.bin", key = my_key)]
375//! fn _() {}
376//! // →
377//! ::std::fs::write("out.bin", ::toolkit_zero::serialization::seal(&cfg, Some(my_key))?)?;
378//! ```
379//!
380//! ## Example
381//!
382//! ```rust,ignore
383//! use toolkit_zero::serialization::{serializable, serialize};
384//!
385//! #[serializable]
386//! struct Config { threshold: f64 }
387//!
388//! fn save(cfg: &Config, key: String) -> Result<(), Box<dyn std::error::Error>> {
389//!     // Seal to a variable
390//!     #[serialize(cfg, key = key.clone())]
391//!     fn blob() -> Vec<u8> {}
392//!     // blob: Vec<u8> is now in scope
393//!
394//!     // Write directly to a file
395//!     #[serialize(cfg, path = "config.bin", key = key)]
396//!     fn _() {}
397//!
398//!     Ok(())
399//! }
400//! ```
401//!
402//! ---
403//!
404//! # `#[deserialize]` — inline open statement
405//!
406//! Replaces a `fn` item with an inline open statement. Two modes:
407//!
408//! - **Variable mode** (`path` absent) — opens from a blob expression already
409//!   in scope. The function name becomes the binding name; the **return type
410//!   is required** and used as the turbofish type `T` in `open::<T>`.
411//! - **File read mode** (`path = "..."` present) — reads the file first, then
412//!   opens. Same name/return-type rules apply.
413//!
414//! ## Syntax
415//!
416//! ```text
417//! // Variable mode
418//! #[deserialize(blob_expr)]
419//! #[deserialize(blob_expr, key = key_expr)]
420//!
421//! // File read mode
422//! #[deserialize(path = "file.bin")]
423//! #[deserialize(path = "file.bin", key = key_expr)]
424//! ```
425//!
426//! | Argument | Required | Description |
427//! |---|---|---|
428//! | `blob_expr` | yes (variable mode) | Expression whose value is `&[u8]` (reference taken automatically) |
429//! | `path = "..."` | yes (file mode) | File to read; switches to file read mode |
430//! | `key = expr` | no | Key expression — `Option<String>` moved into `open()` |
431//!
432//! ## What the macro expands to
433//!
434//! ```rust,ignore
435//! // Variable mode:
436//! #[deserialize(blob, key = my_key)]
437//! fn config() -> Config {}
438//! // →
439//! let config: Config = ::toolkit_zero::serialization::open::<Config>(&blob, Some(my_key))?;
440//!
441//! // Variable mode, default key:
442//! #[deserialize(blob)]
443//! fn config() -> Config {}
444//! // →
445//! let config: Config = ::toolkit_zero::serialization::open::<Config>(&blob, None)?;
446//!
447//! // File read mode:
448//! #[deserialize(path = "config.bin", key = my_key)]
449//! fn config() -> Config {}
450//! // →
451//! let config: Config = ::toolkit_zero::serialization::open::<Config>(
452//!     &::std::fs::read("config.bin")?,
453//!     Some(my_key),
454//! )?;
455//! ```
456//!
457//! ## Example
458//!
459//! ```rust,ignore
460//! use toolkit_zero::serialization::{serializable, serialize, deserialize};
461//!
462//! #[serializable]
463//! struct Config { threshold: f64 }
464//!
465//! fn round_trip(cfg: &Config, key: String) -> Result<Config, Box<dyn std::error::Error>> {
466//!     // Write to disk
467//!     #[serialize(cfg, path = "config.bin", key = key.clone())]
468//!     fn _() {}
469//!
470//!     // Read back
471//!     #[deserialize(path = "config.bin", key = key)]
472//!     fn loaded() -> Config {}
473//!
474//!     Ok(loaded)
475//! }
476//!
477//! fn from_bytes(blob: Vec<u8>) -> Result<Config, Box<dyn std::error::Error>> {
478//!     #[deserialize(blob)]
479//!     fn cfg() -> Config {}
480//!
481//!     Ok(cfg)
482//! }
483//! ```
484//!
485//! ---
486//!
487//! # `#[browser]` — inline location capture
488//!
489//! Replaces a `fn` item with an inline call to either
490//! [`__location__`](toolkit_zero::location::browser::__location__) (sync) or
491//! [`__location_async__`](toolkit_zero::location::browser::__location_async__)
492//! (async, default). The [`PageTemplate`](toolkit_zero::location::browser::PageTemplate)
493//! is built from the macro arguments — no need to construct it manually.
494//!
495//! The function **name** becomes the binding that holds the resulting
496//! [`LocationData`](toolkit_zero::location::browser::LocationData).
497//! The function **body** is discarded. A `?` is appended so any
498//! [`LocationError`](toolkit_zero::location::browser::LocationError) propagates
499//! to the enclosing function.
500//!
501//! ## Syntax
502//!
503//! ```text
504//! #[browser]                                        // async, Default template
505//! #[browser(sync)]                                  // blocking, Default template
506//! #[browser(title = "My App")]                      // async, Default, custom title
507//! #[browser(title = "T", body = "B")]               // async, Default, title + body
508//! #[browser(tickbox)]                               // async, Tickbox template (all defaults)
509//! #[browser(tickbox, title = "T", consent = "C")]   // async, Tickbox with arguments
510//! #[browser(html = "<html>…</html>")]               // async, Custom template
511//! #[browser(sync, html = "…")]                      // sync + Custom template
512//! ```
513//!
514//! All arguments are optional and may appear in **any order**.
515//!
516//! ## Arguments
517//!
518//! | Argument | Type | Notes |
519//! |---|---|---|
520//! | `sync` | flag | Use the blocking `__location__` function; default uses `__location_async__().await` |
521//! | `tickbox` | flag | Use `PageTemplate::Tickbox`; incompatible with `html` |
522//! | `title = "…"` | string literal | Tab/heading title for Default or Tickbox |
523//! | `body = "…"` | string literal | Body paragraph text for Default or Tickbox |
524//! | `consent = "…"` | string literal | Checkbox label for Tickbox only |
525//! | `html = "…"` | string literal | Use `PageTemplate::Custom`; **mutually exclusive** with all other template args |
526//!
527//! ## Expansion examples
528//!
529//! ```rust,ignore
530//! use toolkit_zero::location::{browser, LocationData};
531//!
532//! // Async, Default template with a custom title:
533//! async fn get_loc() -> Result<LocationData, Box<dyn std::error::Error>> {
534//!     #[browser(title = "My App")]
535//!     fn loc() {}
536//!     // expands to:
537//!     // let loc = ::toolkit_zero::location::browser::__location_async__(
538//!     //     ::toolkit_zero::location::browser::PageTemplate::Default {
539//!     //         title:     Some("My App".to_string()),
540//!     //         body_text: None,
541//!     //     }
542//!     // ).await?;
543//!     Ok(loc)
544//! }
545//!
546//! // Sync, Tickbox with consent text:
547//! fn get_loc_sync() -> Result<LocationData, Box<dyn std::error::Error>> {
548//!     #[browser(sync, tickbox, consent = "I agree")]
549//!     fn loc() {}
550//!     // expands to:
551//!     // let loc = ::toolkit_zero::location::browser::__location__(
552//!     //     ::toolkit_zero::location::browser::PageTemplate::Tickbox {
553//!     //         title:        None,
554//!     //         body_text:    None,
555//!     //         consent_text: Some("I agree".to_string()),
556//!     //     }
557//!     // )?;
558//!     Ok(loc)
559//! }
560//! ```
561//!
562//! ---
563//!
564//! # `#[timelock]` — inline key derivation
565//!
566//! Replaces a `fn` item with an inline call to either
567//! [`timelock`](toolkit_zero::encryption::timelock::timelock) (sync) or
568//! [`timelock_async`](toolkit_zero::encryption::timelock::timelock_async)
569//! (async — add the `async` flag). The 7 positional `Option<…>` arguments
570//! are built from named keyword arguments, and the correct path
571//! (*encryption* or *decryption*) is selected automatically.
572//!
573//! The function **name** becomes the binding; the function **body** is
574//! discarded. A `?` propagates any
575//! [`TimeLockError`](toolkit_zero::encryption::timelock::TimeLockError).
576//!
577//! ## Two paths
578//!
579//! | Path | Supply | Required args |
580//! |---|---|---|
581//! | **Encryption** | Explicit time → derive key | `precision`, `format`, `time`, `salts`, `kdf` |
582//! | **Decryption** | Stored header → same key | `params` (a [`TimeLockParams`](toolkit_zero::encryption::timelock::TimeLockParams)) |
583//!
584//! ## Syntax
585//!
586//! ```text
587//! // Encryption path
588//! #[timelock(precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k)]
589//! #[timelock(precision = Hour, format = Hour24, time(9, 0), salts = s, kdf = k,
590//!            cadence = DayOfWeek(Tuesday))]
591//! #[timelock(async, precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k)]
592//!
593//! // Decryption path
594//! #[timelock(params = header)]
595//! #[timelock(async, params = header)]
596//! ```
597//!
598//! All arguments are keyword-based and may appear in **any order**.
599//!
600//! ## Arguments
601//!
602//! | Argument | Type | Notes |
603//! |---|---|---|
604//! | `async` | flag | Use `timelock_async().await?`; default uses `timelock()?` |
605//! | `params = expr` | `TimeLockParams` | **Decryption path.** Mutually exclusive with all other args |
606//! | `precision = …` | `Hour` \| `Quarter` \| `Minute` | Encryption. Time quantisation level |
607//! | `format = …` | `Hour12` \| `Hour24` | Encryption. Clock representation |
608//! | `time(h, m)` | two int literals | Encryption. Target time (24-hour `h` 0–23, `m` 0–59) |
609//! | `cadence = …` | see below | Optional. Calendar constraint; defaults to `None` |
610//! | `salts = expr` | `TimeLockSalts` | Encryption. Three 32-byte KDF salts |
611//! | `kdf = expr` | `KdfParams` | Encryption. KDF work-factor parameters |
612//!
613//! ## Cadence variants
614//!
615//! ```text
616//! cadence = None
617//! cadence = DayOfWeek(Tuesday)
618//! cadence = DayOfMonth(15)
619//! cadence = MonthOfYear(January)
620//! cadence = DayOfWeekInMonth(Tuesday, January)
621//! cadence = DayOfMonthInMonth(15, January)
622//! cadence = DayOfWeekAndDayOfMonth(Tuesday, 15)
623//! ```
624//!
625//! ## Expansion examples
626//!
627//! ```rust,ignore
628//! use toolkit_zero::encryption::timelock::*;
629//!
630//! // Encryption — derive key for 14:37 with Minute precision.
631//! fn encrypt() -> Result<TimeLockKey, TimeLockError> {
632//!     let salts = TimeLockSalts::generate();
633//!     let kdf   = KdfPreset::Balanced.params();
634//!
635//!     #[timelock(precision = Minute, format = Hour24, time(14, 37), salts = salts, kdf = kdf)]
636//!     fn enc_key() {}
637//!     // let enc_key = timelock(
638//!     //     Some(TimeLockCadence::None),
639//!     //     Some(TimeLockTime::new(14, 37).unwrap()),
640//!     //     Some(TimePrecision::Minute),
641//!     //     Some(TimeFormat::Hour24),
642//!     //     Some(salts), Some(kdf), None,
643//!     // )?;
644//!     Ok(enc_key)
645//! }
646//!
647//! // Decryption — re-derive from a stored header.
648//! fn decrypt(header: TimeLockParams) -> Result<TimeLockKey, TimeLockError> {
649//!     #[timelock(params = header)]
650//!     fn dec_key() {}
651//!     // let dec_key = timelock(None, None, None, None, None, None, Some(header))?;
652//!     Ok(dec_key)
653//! }
654//!
655//! // Async encryption with a calendar cadence (Tuesdays only).
656//! async fn async_encrypt() -> Result<TimeLockKey, TimeLockError> {
657//!     let salts = TimeLockSalts::generate();
658//!     let kdf   = KdfPreset::BalancedMac.params();
659//!
660//!     #[timelock(async,
661//!                precision = Minute, format = Hour24, time(14, 37),
662//!                cadence = DayOfWeek(Tuesday),
663//!                salts = salts, kdf = kdf)]
664//!     fn enc_key() {}
665//!     Ok(enc_key)
666//! }
667//! ```
668//!
669//! ---
670//!
671//! # `#[dependencies]` — build-time fingerprint capture
672//!
673//! Embeds the `fingerprint.json` produced by `generate_fingerprint()` (feature
674//! `dependency-graph-build`) into the binary and binds either the parsed
675//! [`BuildTimeFingerprintData`](toolkit_zero::dependency_graph::capture::BuildTimeFingerprintData)
676//! or the raw `&'static [u8]` JSON bytes.
677//!
678//! Apply the attribute to an **empty `fn`** inside a function body; the
679//! function name becomes the `let` binding name. Requires the
680//! `dependency-graph-capture` feature.
681//!
682//! ## Syntax
683//!
684//! ```text
685//! #[dependencies]           // parse mode  → BuildTimeFingerprintData (propagates ?)
686//! #[dependencies(bytes)]    // bytes mode  → &'static [u8]  (infallible)
687//! ```
688//!
689//! ## Expansion
690//!
691//! Both modes expand to a `const` embedding followed by a `let` binding:
692//!
693//! ```rust,ignore
694//! // Parse mode:
695//! // const __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__: &str =
696//! //     include_str!(concat!(env!("OUT_DIR"), "/fingerprint.json"));
697//! // let <binding> = capture::parse(__TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__)?;
698//!
699//! // Bytes mode:
700//! // const __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__: &str =
701//! //     include_str!(concat!(env!("OUT_DIR"), "/fingerprint.json"));
702//! // let <binding>: &'static [u8] = __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__.as_bytes();
703//! ```
704//!
705//! ## Examples
706//!
707//! ```rust,ignore
708//! use toolkit_zero::dependency_graph::capture::dependencies;
709//!
710//! fn show_info() -> Result<(), Box<dyn std::error::Error>> {
711//!     #[dependencies]
712//!     fn data() {}
713//!     println!("{} v{}", data.package.name, data.package.version);
714//!     println!("target  : {}", data.build.target);
715//!     println!("lock    : {}", data.cargo_lock_sha256);
716//!     Ok(())
717//! }
718//!
719//! fn raw_bytes() -> &'static [u8] {
720//!     #[dependencies(bytes)]
721//!     fn raw() {}
722//!     raw
723//! }
724//! ```
725
726#[allow(unused)]
727use proc_macro::TokenStream;
728
729#[cfg(feature = "socket-server")]
730mod mechanism;
731#[cfg(feature = "socket-client")]
732mod request_macro;
733#[cfg(feature = "serialization")]
734mod serialization_macro;
735
736// ─── socket-server ────────────────────────────────────────────────────────────
737
738/// Declare a server-side route.
739///
740/// Available when the `socket-server` feature is enabled.
741/// Re-exported as `toolkit_zero::socket::server::mechanism`.
742///
743/// Full documentation, parameter table, and worked examples are in the
744/// [crate-level `#[mechanism]` section](self#mechanism--server-side-route-declaration).
745#[cfg(feature = "socket-server")]
746#[proc_macro_attribute]
747pub fn mechanism(attr: TokenStream, item: TokenStream) -> TokenStream {
748    mechanism::expand(attr, item)
749}
750
751// ─── socket-client ────────────────────────────────────────────────────────────
752
753/// Emit an inline HTTP request and bind the response.
754///
755/// Available when the `socket-client` feature is enabled.
756/// Re-exported as `toolkit_zero::socket::client::request`.
757///
758/// Full documentation, parameter table, and worked examples are in the
759/// [crate-level `#[request]` section](self#request--client-side-request-shorthand).
760#[cfg(feature = "socket-client")]
761#[proc_macro_attribute]
762pub fn request(attr: TokenStream, item: TokenStream) -> TokenStream {
763    request_macro::expand(attr, item)
764}
765
766// ─── serialization ───────────────────────────────────────────────────────────
767
768/// Derive `bincode::Encode + bincode::Decode` and inject `seal` / `open` methods.
769///
770/// Available when the `serialization` feature is enabled.
771/// Re-exported as `toolkit_zero::serialization::serializable`.
772///
773/// Applies to structs and enums. Named struct fields annotated with
774/// `#[serializable(key = "literal")]` additionally receive `seal_<field>` /
775/// `open_<field>` helpers with the key baked in. Keys are moved in and
776/// wrapped in `Zeroizing<String>`, wiping memory on drop.
777///
778/// Full documentation, expansion details, and examples are in the
779/// [crate-level `#[serializable]` section](self#serializable--derive--inject-sealopen).
780#[cfg(feature = "serialization")]
781#[proc_macro_attribute]
782pub fn serializable(attr: TokenStream, item: TokenStream) -> TokenStream {
783    serialization_macro::expand_serializable(attr, item)
784}
785
786/// Emit an inline `seal()` call, binding the result or writing it to a file.
787///
788/// Available when the `serialization` feature is enabled.
789/// Re-exported as `toolkit_zero::serialization::serialize`.
790///
791/// - **Variable mode** — the function name becomes the binding name; the
792///   return type annotation (required) becomes the type of the `let` binding.
793///   The function body is discarded.
794///   ```text
795///   #[serialize(source, key = my_key)]  fn blob() -> Vec<u8> {}
796///   // expands to:  let blob: Vec<u8> = seal(&source, Some(my_key))?;
797///   ```
798///
799/// - **File write mode** — triggered by `path = "..."`. Emits `fs::write(path, seal(…)?)?"`.
800///   The function name and return type are ignored.
801///   ```text
802///   #[serialize(source, path = "out.bin")]  fn _() {}
803///   ```
804///
805/// Full documentation and examples are in the
806/// [crate-level `#[serialize]` section](self#serialize--inline-seal-statement).
807#[cfg(feature = "serialization")]
808#[proc_macro_attribute]
809pub fn serialize(attr: TokenStream, item: TokenStream) -> TokenStream {
810    serialization_macro::expand_serialize(attr, item)
811}
812
813/// Emit an inline `open()` call, decoding a blob or reading from a file.
814///
815/// Available when the `serialization` feature is enabled.
816/// Re-exported as `toolkit_zero::serialization::deserialize`.
817///
818/// - **Variable mode** — `blob_expr` must be in scope; the function name
819///   becomes the binding name; the return type (required) is used as the
820///   turbofish type `T` in `open::<T>(…)`. The function body is discarded.
821///   ```text
822///   #[deserialize(blob)]  fn config() -> Config {}
823///   // expands to:  let config: Config = open::<Config>(&blob, None)?;
824///   ```
825///
826/// - **File read mode** — triggered by `path = "..."`. Reads the file first,
827///   then passes the bytes to `open::<T>`. Return type still required.
828///   ```text
829///   #[deserialize(path = "config.bin")]  fn config() -> Config {}
830///   ```
831///
832/// Full documentation and examples are in the
833/// [crate-level `#[deserialize]` section](self#deserialize--inline-open-statement).
834#[cfg(feature = "serialization")]
835#[proc_macro_attribute]
836pub fn deserialize(attr: TokenStream, item: TokenStream) -> TokenStream {
837    serialization_macro::expand_deserialize(attr, item)
838}
839
840// ─── location-browser ────────────────────────────────────────────────────────
841
842#[cfg(feature = "location-browser")]
843mod browser_macro;
844
845/// Replace a decorated `fn` with an inline location-capture statement.
846///
847/// Available when the `location` (or `location-browser`) feature is enabled.
848/// Re-exported as `toolkit_zero::location::browser`.
849///
850/// The macro wraps either [`__location__`] (with `sync`) or
851/// [`__location_async__`] (default) and builds the [`PageTemplate`] for you.
852///
853/// [`__location__`]: toolkit_zero::location::browser::__location__
854/// [`__location_async__`]: toolkit_zero::location::browser::__location_async__
855/// [`PageTemplate`]: toolkit_zero::location::browser::PageTemplate
856///
857/// ## Syntax
858///
859/// ```text
860/// #[browser]                                        // async, Default template
861/// #[browser(sync)]                                  // blocking, Default template
862/// #[browser(title = "My App")]                      // async, custom title
863/// #[browser(title = "T", body = "B")]               // async, title + body
864/// #[browser(tickbox)]                               // async, Tickbox template
865/// #[browser(tickbox, title = "T", consent = "C")]   // async, Tickbox with args
866/// #[browser(html = "<html>…</html>")]               // async, Custom template
867/// #[browser(sync, title = "T")]                     // sync + any variant
868/// ```
869///
870/// All arguments are optional and may appear in **any order**.
871///
872/// ## Arguments
873///
874/// | Argument | Type | Notes |
875/// |---|---|---|
876/// | `sync` | flag | Use the blocking `__location__` function; default uses `__location_async__().await` |
877/// | `tickbox` | flag | Use `PageTemplate::Tickbox`; default is `PageTemplate::Default` |
878/// | `title = "…"` | string literal | Sets the `title` field on Default or Tickbox |
879/// | `body = "…"` | string literal | Sets the `body_text` field on Default or Tickbox |
880/// | `consent = "…"` | string literal | Sets the `consent_text` field on Tickbox only |
881/// | `html = "…"` | string literal | Use `PageTemplate::Custom`; **mutually exclusive** with tickbox/title/body/consent |
882///
883/// ## What the macro expands to
884///
885/// The function's **name** becomes the binding; the body is discarded.
886/// A `?` propagates any [`LocationError`](toolkit_zero::location::browser::LocationError).
887///
888/// ```rust,ignore
889/// // Input:
890/// #[browser(title = "My App")]
891/// fn loc() -> LocationData {}
892///
893/// // Expanded to:
894/// let loc = ::toolkit_zero::location::browser::__location_async__(
895///     ::toolkit_zero::location::browser::PageTemplate::Default {
896///         title:     ::std::option::Option::Some("My App".to_string()),
897///         body_text: ::std::option::Option::None,
898///     }
899/// ).await?;
900/// ```
901///
902/// Full documentation and examples are in the
903/// [crate-level `#[browser]` section](self#browser--inline-location-capture).
904#[cfg(feature = "location-browser")]
905#[proc_macro_attribute]
906pub fn browser(attr: TokenStream, item: TokenStream) -> TokenStream {
907    browser_macro::expand_browser(attr, item)
908}
909
910// ─── enc-timelock-* ──────────────────────────────────────────────────────────
911
912#[cfg(any(
913    feature = "enc-timelock-keygen-now",
914    feature = "enc-timelock-keygen-input",
915    feature = "enc-timelock-async-keygen-now",
916    feature = "enc-timelock-async-keygen-input",
917))]
918mod timelock_macro;
919
920#[cfg(feature = "dep-graph-capture")]
921mod dependencies_macro;
922
923/// Replace a decorated `fn` with an inline time-locked key derivation call.
924///
925/// Available when any `enc-timelock-*` feature is enabled.
926/// Re-exported as `toolkit_zero::encryption::timelock::timelock` (the macro).
927///
928/// Routes to either [`timelock`] (sync) or [`timelock_async`] (async — add
929/// the `async` flag) and builds the positional `Option<…>` arguments for you.
930///
931/// [`timelock`]: toolkit_zero::encryption::timelock::timelock
932/// [`timelock_async`]: toolkit_zero::encryption::timelock::timelock_async
933///
934/// ## Two paths
935///
936/// | Path | When to use | Required args |
937/// |---|---|---|
938/// | **Encryption** | Generate a key from an explicit time | `precision`, `format`, `time`, `salts`, `kdf` |
939/// | **Decryption** | Re-derive a key from a stored header | `params` |
940///
941/// ## Syntax
942///
943/// ```text
944/// // Encryption path
945/// #[timelock(precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k)]
946/// #[timelock(precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k,
947///            cadence = DayOfWeek(Tuesday))]
948/// #[timelock(async, precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k)]
949///
950/// // Decryption path
951/// #[timelock(params = header)]
952/// #[timelock(async, params = header)]
953/// ```
954///
955/// ## Arguments
956///
957/// | Argument | Type | Notes |
958/// |---|---|---|
959/// | `async` | flag | Use `timelock_async().await?`; default uses `timelock()?` |
960/// | `params = expr` | [`TimeLockParams`] | **Decryption path**. Mutually exclusive with all other args |
961/// | `precision = …` | `Hour` \| `Quarter` \| `Minute` | Encryption path. Time quantisation level |
962/// | `format = …` | `Hour12` \| `Hour24` | Encryption path. Clock representation |
963/// | `time(h, m)` | two int literals | Encryption path. Target time (24-hour `h`, `m`) |
964/// | `cadence = …` | variant or `None` | Optional; defaults to `None` (`TimeLockCadence::None`) |
965/// | `salts = expr` | [`TimeLockSalts`] | Encryption path. Three 32-byte KDF salts |
966/// | `kdf = expr` | [`KdfParams`] | Encryption path. KDF work-factor parameters |
967///
968/// [`TimeLockParams`]: toolkit_zero::encryption::timelock::TimeLockParams
969/// [`TimeLockSalts`]:  toolkit_zero::encryption::timelock::TimeLockSalts
970/// [`KdfParams`]:      toolkit_zero::encryption::timelock::KdfParams
971///
972/// ## Cadence variants
973///
974/// ```text
975/// cadence = None
976/// cadence = DayOfWeek(Tuesday)
977/// cadence = DayOfMonth(15)
978/// cadence = MonthOfYear(January)
979/// cadence = DayOfWeekInMonth(Tuesday, January)
980/// cadence = DayOfMonthInMonth(15, January)
981/// cadence = DayOfWeekAndDayOfMonth(Tuesday, 15)
982/// ```
983///
984/// ## What the macro expands to
985///
986/// ```rust,ignore
987/// // Encryption path:
988/// #[timelock(precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k)]
989/// fn enc_key() {}
990/// // expands to:
991/// // let enc_key = ::toolkit_zero::encryption::timelock::timelock(
992/// //     Some(TimeLockCadence::None),
993/// //     Some(TimeLockTime::new(14, 37).unwrap()),
994/// //     Some(TimePrecision::Minute),
995/// //     Some(TimeFormat::Hour24),
996/// //     Some(s), Some(k), None,
997/// // )?;
998///
999/// // Decryption path:
1000/// #[timelock(params = header)]
1001/// fn dec_key() {}
1002/// // expands to:
1003/// // let dec_key = ::toolkit_zero::encryption::timelock::timelock(
1004/// //     None, None, None, None, None, None,
1005/// //     Some(header),
1006/// // )?;
1007/// ```
1008///
1009/// Full documentation and examples are in the
1010/// [crate-level `#[timelock]` section](self#timelock--inline-key-derivation).
1011#[cfg(any(
1012    feature = "enc-timelock-keygen-now",
1013    feature = "enc-timelock-keygen-input",
1014    feature = "enc-timelock-async-keygen-now",
1015    feature = "enc-timelock-async-keygen-input",
1016))]
1017#[proc_macro_attribute]
1018pub fn timelock(attr: TokenStream, item: TokenStream) -> TokenStream {
1019    timelock_macro::expand_timelock(attr, item)
1020}
1021
1022// ─── #[dependencies] ──────────────────────────────────────────────────────────
1023
1024/// Embed and parse (or read the raw bytes of) the build-time
1025/// `fingerprint.json` fingerprint in one expression.
1026///
1027/// Apply to an empty `fn`; the function name becomes the `let` binding in
1028/// the expansion. Requires the `dependency-graph-capture` feature.
1029///
1030/// ## Parse mode (default)
1031///
1032/// ```rust,ignore
1033/// use toolkit_zero::dependency_graph::capture::{dependencies, BuildTimeFingerprintData};
1034///
1035/// fn show() -> Result<(), Box<dyn std::error::Error>> {
1036///     #[dependencies]
1037///     fn data() {}
1038///     // expands to:
1039///     // const __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__: &str =
1040///     //     include_str!(concat!(env!("OUT_DIR"), "/fingerprint.json"));
1041///     // let data = capture::parse(__TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__)?;
1042///
1043///     println!("{} v{}", data.package.name, data.package.version);
1044///     Ok(())
1045/// }
1046/// ```
1047///
1048/// ## Bytes mode
1049///
1050/// ```rust,ignore
1051/// fn raw_bytes() -> &'static [u8] {
1052///     #[dependencies(bytes)]
1053///     fn raw() {}
1054///     // expands to:
1055///     // const __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__: &str =
1056///     //     include_str!(concat!(env!("OUT_DIR"), "/fingerprint.json"));
1057///     // let raw: &'static [u8] = __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__.as_bytes();
1058///     raw
1059/// }
1060/// ```
1061///
1062/// Full documentation is in the
1063/// [crate-level `#[dependencies]` section](self#dependencies--build-time-fingerprint-capture).
1064#[cfg(feature = "dep-graph-capture")]
1065#[proc_macro_attribute]
1066pub fn dependencies(attr: TokenStream, item: TokenStream) -> TokenStream {
1067    dependencies_macro::expand_dependencies(attr, item)
1068}