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 optionally injects `seal` and/or `open` methods controlled by
235//! attribute arguments:
236//!
237//! ```text
238//! fn seal(&self, key: Option<String>) -> Result<Vec<u8>, SerializationError>
239//! fn open(bytes: &[u8], key: Option<String>) -> Result<Self, SerializationError>
240//! ```
241//!
242//! The `key` is **moved in** and internally wrapped in `Zeroizing<String>`,
243//! wiping it from memory on drop. Pass `None` to use the built-in default key.
244//!
245//! Field-level `#[serializable(key = "literal")]` additionally generates
246//! per-field seal helpers with the key baked in (only when `SEAL` is active):
247//!
248//! ```text
249//! fn seal_<field>(&self) -> Result<Vec<u8>, SerializationError>
250//! ```
251//!
252//! ## Syntax
253//!
254//! ```text
255//! #[serializable] // derive both seal + open (default)
256//! #[serializable(SEAL, OPEN)] // same as above — explicit
257//! #[serializable(SEAL)] // derive only seal
258//! #[serializable(OPEN)] // derive only open
259//!
260//! #[serializable] // field annotation inside a struct
261//! struct Bar {
262//! pub normal: String,
263//! #[serializable(key = "my-key")] // generates seal_secret
264//! pub secret: String,
265//! }
266//! ```
267//!
268//! > **Note:** `#[serializable]` on an enum does not scan fields for the
269//! > field-level annotation — only named struct fields are supported for
270//! > per-field helpers.
271//!
272//! ## What the macro expands to
273//!
274//! ```rust,ignore
275//! // Input:
276//! #[serializable]
277//! struct Config { host: String, port: u16 }
278//!
279//! // Expanded to:
280//! #[derive(::toolkit_zero::serialization::Encode, ::toolkit_zero::serialization::Decode)]
281//! struct Config { host: String, port: u16 }
282//!
283//! impl Config {
284//! pub fn seal(&self, key: Option<String>) -> Result<Vec<u8>, SerializationError> {
285//! ::toolkit_zero::serialization::seal(self, key)
286//! }
287//! pub fn open(bytes: &[u8], key: Option<String>) -> Result<Self, SerializationError> {
288//! ::toolkit_zero::serialization::open::<Self>(bytes, key)
289//! }
290//! }
291//! ```
292//!
293//! ## Example
294//!
295//! ```rust,ignore
296//! use toolkit_zero::serialization::serializable;
297//!
298//! #[serializable]
299//! struct Config { host: String, port: u16 }
300//!
301//! let c = Config { host: "localhost".into(), port: 8080 };
302//!
303//! // Seal / open via struct methods
304//! let blob = c.seal(None).unwrap();
305//! let back = Config::open(&blob, None).unwrap();
306//! assert_eq!(c.host, back.host);
307//!
308//! // Custom key — moved in, zeroized on drop
309//! let blob2 = c.seal(Some("secret".to_string())).unwrap();
310//! let back2 = Config::open(&blob2, Some("secret".to_string())).unwrap();
311//!
312//! // Per-field annotation
313//! #[serializable]
314//! struct Creds {
315//! pub user: String,
316//! #[serializable(key = "field-secret")]
317//! pub password: String,
318//! }
319//!
320//! let creds = Creds { user: "alice".into(), password: "hunter2".into() };
321//! let pw_blob = creds.seal_password().unwrap(); // key baked in
322//! let pw_back = Creds::open_password(&pw_blob).unwrap();
323//! assert_eq!("hunter2", pw_back);
324//! ```
325//!
326//! ---
327//!
328//! # `#[serialize]` — inline seal statement
329//!
330//! Replaces a `fn` item with an inline seal statement. Two modes are
331//! selected by the presence or absence of `path`:
332//!
333//! - **Variable mode** (`path` absent) — emits a `let` binding. The function
334//! name becomes the variable name; the **return type is required** and
335//! becomes the type annotation. The function body is discarded.
336//! - **File write mode** (`path = "..."` present) — emits
337//! `std::fs::write(path, seal(…)?)?`. The function name and return type are
338//! ignored.
339//!
340//! ## Syntax
341//!
342//! ```text
343//! // Variable mode
344//! #[serialize(source_expr)]
345//! #[serialize(source_expr, key = key_expr)]
346//!
347//! // File write mode
348//! #[serialize(source_expr, path = "file.bin")]
349//! #[serialize(source_expr, path = "file.bin", key = key_expr)]
350//! ```
351//!
352//! | Argument | Required | Description |
353//! |---|---|---|
354//! | `source_expr` | yes | Expression to seal (must be `bincode::Encode`) |
355//! | `key = expr` | no | Key expression — `Option<String>` moved into `seal()` |
356//! | `path = "..."` | no | File path — switches to file write mode |
357//!
358//! `key` and `path` may appear in any order after `source_expr`.
359//!
360//! ## What the macro expands to
361//!
362//! ```rust,ignore
363//! // Variable mode:
364//! #[serialize(cfg, key = my_key)]
365//! fn blob() -> Vec<u8> {}
366//! // →
367//! let blob: Vec<u8> = ::toolkit_zero::serialization::seal(&cfg, Some(my_key))?;
368//!
369//! // Variable mode, default key:
370//! #[serialize(cfg)]
371//! fn blob() -> Vec<u8> {}
372//! // →
373//! let blob: Vec<u8> = ::toolkit_zero::serialization::seal(&cfg, None)?;
374//!
375//! // File write mode:
376//! #[serialize(cfg, path = "out.bin", key = my_key)]
377//! fn _() {}
378//! // →
379//! ::std::fs::write("out.bin", ::toolkit_zero::serialization::seal(&cfg, Some(my_key))?)?;
380//! ```
381//!
382//! ## Example
383//!
384//! ```rust,ignore
385//! use toolkit_zero::serialization::{serializable, serialize};
386//!
387//! #[serializable]
388//! struct Config { threshold: f64 }
389//!
390//! fn save(cfg: &Config, key: String) -> Result<(), Box<dyn std::error::Error>> {
391//! // Seal to a variable
392//! #[serialize(cfg, key = key.clone())]
393//! fn blob() -> Vec<u8> {}
394//! // blob: Vec<u8> is now in scope
395//!
396//! // Write directly to a file
397//! #[serialize(cfg, path = "config.bin", key = key)]
398//! fn _() {}
399//!
400//! Ok(())
401//! }
402//! ```
403//!
404//! ---
405//!
406//! # `#[deserialize]` — inline open statement
407//!
408//! Replaces a `fn` item with an inline open statement. Two modes:
409//!
410//! - **Variable mode** (`path` absent) — opens from a blob expression already
411//! in scope. The function name becomes the binding name; the **return type
412//! is required** and used as the turbofish type `T` in `open::<T>`.
413//! - **File read mode** (`path = "..."` present) — reads the file first, then
414//! opens. Same name/return-type rules apply.
415//!
416//! ## Syntax
417//!
418//! ```text
419//! // Variable mode
420//! #[deserialize(blob_expr)]
421//! #[deserialize(blob_expr, key = key_expr)]
422//!
423//! // File read mode
424//! #[deserialize(path = "file.bin")]
425//! #[deserialize(path = "file.bin", key = key_expr)]
426//! ```
427//!
428//! | Argument | Required | Description |
429//! |---|---|---|
430//! | `blob_expr` | yes (variable mode) | Expression whose value is `&[u8]` (reference taken automatically) |
431//! | `path = "..."` | yes (file mode) | File to read; switches to file read mode |
432//! | `key = expr` | no | Key expression — `Option<String>` moved into `open()` |
433//!
434//! ## What the macro expands to
435//!
436//! ```rust,ignore
437//! // Variable mode:
438//! #[deserialize(blob, key = my_key)]
439//! fn config() -> Config {}
440//! // →
441//! let config: Config = ::toolkit_zero::serialization::open::<Config>(&blob, Some(my_key))?;
442//!
443//! // Variable mode, default key:
444//! #[deserialize(blob)]
445//! fn config() -> Config {}
446//! // →
447//! let config: Config = ::toolkit_zero::serialization::open::<Config>(&blob, None)?;
448//!
449//! // File read mode:
450//! #[deserialize(path = "config.bin", key = my_key)]
451//! fn config() -> Config {}
452//! // →
453//! let config: Config = ::toolkit_zero::serialization::open::<Config>(
454//! &::std::fs::read("config.bin")?,
455//! Some(my_key),
456//! )?;
457//! ```
458//!
459//! ## Example
460//!
461//! ```rust,ignore
462//! use toolkit_zero::serialization::{serializable, serialize, deserialize};
463//!
464//! #[serializable]
465//! struct Config { threshold: f64 }
466//!
467//! fn round_trip(cfg: &Config, key: String) -> Result<Config, Box<dyn std::error::Error>> {
468//! // Write to disk
469//! #[serialize(cfg, path = "config.bin", key = key.clone())]
470//! fn _() {}
471//!
472//! // Read back
473//! #[deserialize(path = "config.bin", key = key)]
474//! fn loaded() -> Config {}
475//!
476//! Ok(loaded)
477//! }
478//!
479//! fn from_bytes(blob: Vec<u8>) -> Result<Config, Box<dyn std::error::Error>> {
480//! #[deserialize(blob)]
481//! fn cfg() -> Config {}
482//!
483//! Ok(cfg)
484//! }
485//! ```
486//!
487//! ---
488//!
489//! # `#[browser]` — inline location capture
490//!
491//! Replaces a `fn` item with an inline call to either
492//! [`__location__`](toolkit_zero::location::browser::__location__) (sync) or
493//! [`__location_async__`](toolkit_zero::location::browser::__location_async__)
494//! (async, default). The [`PageTemplate`](toolkit_zero::location::browser::PageTemplate)
495//! is built from the macro arguments — no need to construct it manually.
496//!
497//! The function **name** becomes the binding that holds the resulting
498//! [`LocationData`](toolkit_zero::location::browser::LocationData).
499//! The function **body** is discarded. A `?` is appended so any
500//! [`LocationError`](toolkit_zero::location::browser::LocationError) propagates
501//! to the enclosing function.
502//!
503//! ## Syntax
504//!
505//! ```text
506//! #[browser] // async, Default template
507//! #[browser(sync)] // blocking, Default template
508//! #[browser(title = "My App")] // async, Default, custom title
509//! #[browser(title = "T", body = "B")] // async, Default, title + body
510//! #[browser(tickbox)] // async, Tickbox template (all defaults)
511//! #[browser(tickbox, title = "T", consent = "C")] // async, Tickbox with arguments
512//! #[browser(html = "<html>…</html>")] // async, Custom template
513//! #[browser(sync, html = "…")] // sync + Custom template
514//! ```
515//!
516//! All arguments are optional and may appear in **any order**.
517//!
518//! ## Arguments
519//!
520//! | Argument | Type | Notes |
521//! |---|---|---|
522//! | `sync` | flag | Use the blocking `__location__` function; default uses `__location_async__().await` |
523//! | `tickbox` | flag | Use `PageTemplate::Tickbox`; incompatible with `html` |
524//! | `title = "…"` | string literal | Tab/heading title for Default or Tickbox |
525//! | `body = "…"` | string literal | Body paragraph text for Default or Tickbox |
526//! | `consent = "…"` | string literal | Checkbox label for Tickbox only |
527//! | `html = "…"` | string literal | Use `PageTemplate::Custom`; **mutually exclusive** with all other template args |
528//!
529//! ## Expansion examples
530//!
531//! ```rust,ignore
532//! use toolkit_zero::location::{browser, LocationData};
533//!
534//! // Async, Default template with a custom title:
535//! async fn get_loc() -> Result<LocationData, Box<dyn std::error::Error>> {
536//! #[browser(title = "My App")]
537//! fn loc() {}
538//! // expands to:
539//! // let loc = ::toolkit_zero::location::browser::__location_async__(
540//! // ::toolkit_zero::location::browser::PageTemplate::Default {
541//! // title: Some("My App".to_string()),
542//! // body_text: None,
543//! // }
544//! // ).await?;
545//! Ok(loc)
546//! }
547//!
548//! // Sync, Tickbox with consent text:
549//! fn get_loc_sync() -> Result<LocationData, Box<dyn std::error::Error>> {
550//! #[browser(sync, tickbox, consent = "I agree")]
551//! fn loc() {}
552//! // expands to:
553//! // let loc = ::toolkit_zero::location::browser::__location__(
554//! // ::toolkit_zero::location::browser::PageTemplate::Tickbox {
555//! // title: None,
556//! // body_text: None,
557//! // consent_text: Some("I agree".to_string()),
558//! // }
559//! // )?;
560//! Ok(loc)
561//! }
562//! ```
563//!
564//! ---
565//!
566//! # `#[timelock]` — inline key derivation
567//!
568//! Replaces a `fn` item with an inline call to either
569//! [`timelock`](toolkit_zero::encryption::timelock::timelock) (sync) or
570//! [`timelock_async`](toolkit_zero::encryption::timelock::timelock_async)
571//! (async — add the `async` flag). The 7 positional `Option<…>` arguments
572//! are built from named keyword arguments, and the correct path
573//! (*encryption* or *decryption*) is selected automatically.
574//!
575//! The function **name** becomes the binding; the function **body** is
576//! discarded. A `?` propagates any
577//! [`TimeLockError`](toolkit_zero::encryption::timelock::TimeLockError).
578//!
579//! ## Two paths
580//!
581//! | Path | Supply | Required args |
582//! |---|---|---|
583//! | **Encryption** | Explicit time → derive key | `precision`, `format`, `time`, `salts`, `kdf` |
584//! | **Decryption** | Stored header → same key | `params` (a [`TimeLockParams`](toolkit_zero::encryption::timelock::TimeLockParams)) |
585//!
586//! ## Syntax
587//!
588//! ```text
589//! // Encryption path
590//! #[timelock(precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k)]
591//! #[timelock(precision = Hour, format = Hour24, time(9, 0), salts = s, kdf = k,
592//! cadence = DayOfWeek(Tuesday))]
593//! #[timelock(async, precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k)]
594//!
595//! // Decryption path
596//! #[timelock(params = header)]
597//! #[timelock(async, params = header)]
598//! ```
599//!
600//! All arguments are keyword-based and may appear in **any order**.
601//!
602//! ## Arguments
603//!
604//! | Argument | Type | Notes |
605//! |---|---|---|
606//! | `async` | flag | Use `timelock_async().await?`; default uses `timelock()?` |
607//! | `params = expr` | `TimeLockParams` | **Decryption path.** Mutually exclusive with all other args |
608//! | `precision = …` | `Hour` \| `Quarter` \| `Minute` | Encryption. Time quantisation level |
609//! | `format = …` | `Hour12` \| `Hour24` | Encryption. Clock representation |
610//! | `time(h, m)` | two int literals | Encryption. Target time (24-hour `h` 0–23, `m` 0–59) |
611//! | `cadence = …` | see below | Optional. Calendar constraint; defaults to `None` |
612//! | `salts = expr` | `TimeLockSalts` | Encryption. Three 32-byte KDF salts |
613//! | `kdf = expr` | `KdfParams` | Encryption. KDF work-factor parameters |
614//!
615//! ## Cadence variants
616//!
617//! ```text
618//! cadence = None
619//! cadence = DayOfWeek(Tuesday)
620//! cadence = DayOfMonth(15)
621//! cadence = MonthOfYear(January)
622//! cadence = DayOfWeekInMonth(Tuesday, January)
623//! cadence = DayOfMonthInMonth(15, January)
624//! cadence = DayOfWeekAndDayOfMonth(Tuesday, 15)
625//! ```
626//!
627//! ## Expansion examples
628//!
629//! ```rust,ignore
630//! use toolkit_zero::encryption::timelock::*;
631//!
632//! // Encryption — derive key for 14:37 with Minute precision.
633//! fn encrypt() -> Result<TimeLockKey, TimeLockError> {
634//! let salts = TimeLockSalts::generate();
635//! let kdf = KdfPreset::Balanced.params();
636//!
637//! #[timelock(precision = Minute, format = Hour24, time(14, 37), salts = salts, kdf = kdf)]
638//! fn enc_key() {}
639//! // let enc_key = timelock(
640//! // Some(TimeLockCadence::None),
641//! // Some(TimeLockTime::new(14, 37).unwrap()),
642//! // Some(TimePrecision::Minute),
643//! // Some(TimeFormat::Hour24),
644//! // Some(salts), Some(kdf), None,
645//! // )?;
646//! Ok(enc_key)
647//! }
648//!
649//! // Decryption — re-derive from a stored header.
650//! fn decrypt(header: TimeLockParams) -> Result<TimeLockKey, TimeLockError> {
651//! #[timelock(params = header)]
652//! fn dec_key() {}
653//! // let dec_key = timelock(None, None, None, None, None, None, Some(header))?;
654//! Ok(dec_key)
655//! }
656//!
657//! // Async encryption with a calendar cadence (Tuesdays only).
658//! async fn async_encrypt() -> Result<TimeLockKey, TimeLockError> {
659//! let salts = TimeLockSalts::generate();
660//! let kdf = KdfPreset::BalancedMac.params();
661//!
662//! #[timelock(async,
663//! precision = Minute, format = Hour24, time(14, 37),
664//! cadence = DayOfWeek(Tuesday),
665//! salts = salts, kdf = kdf)]
666//! fn enc_key() {}
667//! Ok(enc_key)
668//! }
669//! ```
670//!
671//! ---
672//!
673//! # `#[dependencies]` — build-time fingerprint capture
674//!
675//! Embeds the `fingerprint.json` produced by `generate_fingerprint()` (feature
676//! `dependency-graph-build`) into the binary and binds either the parsed
677//! [`BuildTimeFingerprintData`](toolkit_zero::dependency_graph::capture::BuildTimeFingerprintData)
678//! or the raw `&'static [u8]` JSON bytes.
679//!
680//! Apply the attribute to an **empty `fn`** inside a function body; the
681//! function name becomes the `let` binding name. Requires the
682//! `dependency-graph-capture` feature.
683//!
684//! ## Syntax
685//!
686//! ```text
687//! #[dependencies] // parse mode → BuildTimeFingerprintData (propagates ?)
688//! #[dependencies(bytes)] // bytes mode → &'static [u8] (infallible)
689//! ```
690//!
691//! ## Expansion
692//!
693//! Both modes expand to a `const` embedding followed by a `let` binding:
694//!
695//! ```rust,ignore
696//! // Parse mode:
697//! // const __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__: &str =
698//! // include_str!(concat!(env!("OUT_DIR"), "/fingerprint.json"));
699//! // let <binding> = capture::parse(__TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__)?;
700//!
701//! // Bytes mode:
702//! // const __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__: &str =
703//! // include_str!(concat!(env!("OUT_DIR"), "/fingerprint.json"));
704//! // let <binding>: &'static [u8] = __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__.as_bytes();
705//! ```
706//!
707//! ## Examples
708//!
709//! ```rust,ignore
710//! use toolkit_zero::dependency_graph::capture::dependencies;
711//!
712//! fn show_info() -> Result<(), Box<dyn std::error::Error>> {
713//! #[dependencies]
714//! fn data() {}
715//! println!("{} v{}", data.package.name, data.package.version);
716//! println!("target : {}", data.build.target);
717//! println!("lock : {}", data.cargo_lock_sha256);
718//! Ok(())
719//! }
720//!
721//! fn raw_bytes() -> &'static [u8] {
722//! #[dependencies(bytes)]
723//! fn raw() {}
724//! raw
725//! }
726//! ```
727
728#[allow(unused)]
729use proc_macro::TokenStream;
730
731#[cfg(feature = "socket-server")]
732mod mechanism;
733#[cfg(feature = "socket-client")]
734mod request_macro;
735#[cfg(feature = "serialization")]
736mod serialization_macro;
737
738// ─── socket-server ────────────────────────────────────────────────────────────
739
740/// Declare a server-side route.
741///
742/// Available when the `socket-server` feature is enabled.
743/// Re-exported as `toolkit_zero::socket::server::mechanism`.
744///
745/// Full documentation, parameter table, and worked examples are in the
746/// [crate-level `#[mechanism]` section](self#mechanism--server-side-route-declaration).
747#[cfg(feature = "socket-server")]
748#[proc_macro_attribute]
749pub fn mechanism(attr: TokenStream, item: TokenStream) -> TokenStream {
750 mechanism::expand(attr, item)
751}
752
753// ─── socket-client ────────────────────────────────────────────────────────────
754
755/// Emit an inline HTTP request and bind the response.
756///
757/// Available when the `socket-client` feature is enabled.
758/// Re-exported as `toolkit_zero::socket::client::request`.
759///
760/// Full documentation, parameter table, and worked examples are in the
761/// [crate-level `#[request]` section](self#request--client-side-request-shorthand).
762#[cfg(feature = "socket-client")]
763#[proc_macro_attribute]
764pub fn request(attr: TokenStream, item: TokenStream) -> TokenStream {
765 request_macro::expand(attr, item)
766}
767
768// ─── serialization ───────────────────────────────────────────────────────────
769
770/// Derive `bincode::Encode + bincode::Decode` and inject `seal` / `open` methods.
771///
772/// Available when the `serialization` feature is enabled.
773/// Re-exported as `toolkit_zero::serialization::serializable`.
774///
775/// By default (no args) both `seal` and `open` are derived. Pass `SEAL`,
776/// `OPEN`, or `SEAL, OPEN` to control which methods are generated:
777///
778/// ```text
779/// #[serializable] // both
780/// #[serializable(SEAL)] // only seal
781/// #[serializable(OPEN)] // only open
782/// #[serializable(SEAL, OPEN)] // both (explicit)
783/// ```
784///
785/// Named struct fields annotated with `#[serializable(key = "literal")]`
786/// additionally receive a `seal_<field>` helper when `SEAL` is active.
787/// Keys are moved in and wrapped in `Zeroizing<String>`, wiping memory on drop.
788///
789/// Full documentation, expansion details, and examples are in the
790/// [crate-level `#[serializable]` section](self#serializable--derive--inject-sealopen).
791#[cfg(feature = "serialization")]
792#[proc_macro_attribute]
793pub fn serializable(attr: TokenStream, item: TokenStream) -> TokenStream {
794 serialization_macro::expand_serializable(attr, item)
795}
796
797/// Emit an inline `seal()` call, binding the result or writing it to a file.
798///
799/// Available when the `serialization` feature is enabled.
800/// Re-exported as `toolkit_zero::serialization::serialize`.
801///
802/// - **Variable mode** — the function name becomes the binding name; the
803/// return type annotation (required) becomes the type of the `let` binding.
804/// The function body is discarded.
805/// ```text
806/// #[serialize(source, key = my_key)] fn blob() -> Vec<u8> {}
807/// // expands to: let blob: Vec<u8> = seal(&source, Some(my_key))?;
808/// ```
809///
810/// - **File write mode** — triggered by `path = "..."`. Emits `fs::write(path, seal(…)?)?"`.
811/// The function name and return type are ignored.
812/// ```text
813/// #[serialize(source, path = "out.bin")] fn _() {}
814/// ```
815///
816/// Full documentation and examples are in the
817/// [crate-level `#[serialize]` section](self#serialize--inline-seal-statement).
818#[cfg(feature = "serialization")]
819#[proc_macro_attribute]
820pub fn serialize(attr: TokenStream, item: TokenStream) -> TokenStream {
821 serialization_macro::expand_serialize(attr, item)
822}
823
824/// Emit an inline `open()` call, decoding a blob or reading from a file.
825///
826/// Available when the `serialization` feature is enabled.
827/// Re-exported as `toolkit_zero::serialization::deserialize`.
828///
829/// - **Variable mode** — `blob_expr` must be in scope; the function name
830/// becomes the binding name; the return type (required) is used as the
831/// turbofish type `T` in `open::<T>(…)`. The function body is discarded.
832/// ```text
833/// #[deserialize(blob)] fn config() -> Config {}
834/// // expands to: let config: Config = open::<Config>(&blob, None)?;
835/// ```
836///
837/// - **File read mode** — triggered by `path = "..."`. Reads the file first,
838/// then passes the bytes to `open::<T>`. Return type still required.
839/// ```text
840/// #[deserialize(path = "config.bin")] fn config() -> Config {}
841/// ```
842///
843/// Full documentation and examples are in the
844/// [crate-level `#[deserialize]` section](self#deserialize--inline-open-statement).
845#[cfg(feature = "serialization")]
846#[proc_macro_attribute]
847pub fn deserialize(attr: TokenStream, item: TokenStream) -> TokenStream {
848 serialization_macro::expand_deserialize(attr, item)
849}
850
851// ─── location-browser ────────────────────────────────────────────────────────
852
853#[cfg(feature = "location-browser")]
854mod browser_macro;
855
856/// Replace a decorated `fn` with an inline location-capture statement.
857///
858/// Available when the `location` (or `location-browser`) feature is enabled.
859/// Re-exported as `toolkit_zero::location::browser`.
860///
861/// The macro wraps either [`__location__`] (with `sync`) or
862/// [`__location_async__`] (default) and builds the [`PageTemplate`] for you.
863///
864/// [`__location__`]: toolkit_zero::location::browser::__location__
865/// [`__location_async__`]: toolkit_zero::location::browser::__location_async__
866/// [`PageTemplate`]: toolkit_zero::location::browser::PageTemplate
867///
868/// ## Syntax
869///
870/// ```text
871/// #[browser] // async, Default template
872/// #[browser(sync)] // blocking, Default template
873/// #[browser(title = "My App")] // async, custom title
874/// #[browser(title = "T", body = "B")] // async, title + body
875/// #[browser(tickbox)] // async, Tickbox template
876/// #[browser(tickbox, title = "T", consent = "C")] // async, Tickbox with args
877/// #[browser(html = "<html>…</html>")] // async, Custom template
878/// #[browser(sync, title = "T")] // sync + any variant
879/// ```
880///
881/// All arguments are optional and may appear in **any order**.
882///
883/// ## Arguments
884///
885/// | Argument | Type | Notes |
886/// |---|---|---|
887/// | `sync` | flag | Use the blocking `__location__` function; default uses `__location_async__().await` |
888/// | `tickbox` | flag | Use `PageTemplate::Tickbox`; default is `PageTemplate::Default` |
889/// | `title = "…"` | string literal | Sets the `title` field on Default or Tickbox |
890/// | `body = "…"` | string literal | Sets the `body_text` field on Default or Tickbox |
891/// | `consent = "…"` | string literal | Sets the `consent_text` field on Tickbox only |
892/// | `html = "…"` | string literal | Use `PageTemplate::Custom`; **mutually exclusive** with tickbox/title/body/consent |
893///
894/// ## What the macro expands to
895///
896/// The function's **name** becomes the binding; the body is discarded.
897/// A `?` propagates any [`LocationError`](toolkit_zero::location::browser::LocationError).
898///
899/// ```rust,ignore
900/// // Input:
901/// #[browser(title = "My App")]
902/// fn loc() -> LocationData {}
903///
904/// // Expanded to:
905/// let loc = ::toolkit_zero::location::browser::__location_async__(
906/// ::toolkit_zero::location::browser::PageTemplate::Default {
907/// title: ::std::option::Option::Some("My App".to_string()),
908/// body_text: ::std::option::Option::None,
909/// }
910/// ).await?;
911/// ```
912///
913/// Full documentation and examples are in the
914/// [crate-level `#[browser]` section](self#browser--inline-location-capture).
915#[cfg(feature = "location-browser")]
916#[proc_macro_attribute]
917pub fn browser(attr: TokenStream, item: TokenStream) -> TokenStream {
918 browser_macro::expand_browser(attr, item)
919}
920
921// ─── enc-timelock-* ──────────────────────────────────────────────────────────
922
923#[cfg(any(
924 feature = "enc-timelock-keygen-now",
925 feature = "enc-timelock-keygen-input",
926 feature = "enc-timelock-async-keygen-now",
927 feature = "enc-timelock-async-keygen-input",
928))]
929mod timelock_macro;
930
931#[cfg(feature = "dep-graph-capture")]
932mod dependencies_macro;
933
934/// Replace a decorated `fn` with an inline time-locked key derivation call.
935///
936/// Available when any `enc-timelock-*` feature is enabled.
937/// Re-exported as `toolkit_zero::encryption::timelock::timelock` (the macro).
938///
939/// Routes to either [`timelock`] (sync) or [`timelock_async`] (async — add
940/// the `async` flag) and builds the positional `Option<…>` arguments for you.
941///
942/// [`timelock`]: toolkit_zero::encryption::timelock::timelock
943/// [`timelock_async`]: toolkit_zero::encryption::timelock::timelock_async
944///
945/// ## Two paths
946///
947/// | Path | When to use | Required args |
948/// |---|---|---|
949/// | **Encryption** | Generate a key from an explicit time | `precision`, `format`, `time`, `salts`, `kdf` |
950/// | **Decryption** | Re-derive a key from a stored header | `params` |
951///
952/// ## Syntax
953///
954/// ```text
955/// // Encryption path
956/// #[timelock(precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k)]
957/// #[timelock(precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k,
958/// cadence = DayOfWeek(Tuesday))]
959/// #[timelock(async, precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k)]
960///
961/// // Decryption path
962/// #[timelock(params = header)]
963/// #[timelock(async, params = header)]
964/// ```
965///
966/// ## Arguments
967///
968/// | Argument | Type | Notes |
969/// |---|---|---|
970/// | `async` | flag | Use `timelock_async().await?`; default uses `timelock()?` |
971/// | `params = expr` | [`TimeLockParams`] | **Decryption path**. Mutually exclusive with all other args |
972/// | `precision = …` | `Hour` \| `Quarter` \| `Minute` | Encryption path. Time quantisation level |
973/// | `format = …` | `Hour12` \| `Hour24` | Encryption path. Clock representation |
974/// | `time(h, m)` | two int literals | Encryption path. Target time (24-hour `h`, `m`) |
975/// | `cadence = …` | variant or `None` | Optional; defaults to `None` (`TimeLockCadence::None`) |
976/// | `salts = expr` | [`TimeLockSalts`] | Encryption path. Three 32-byte KDF salts |
977/// | `kdf = expr` | [`KdfParams`] | Encryption path. KDF work-factor parameters |
978///
979/// [`TimeLockParams`]: toolkit_zero::encryption::timelock::TimeLockParams
980/// [`TimeLockSalts`]: toolkit_zero::encryption::timelock::TimeLockSalts
981/// [`KdfParams`]: toolkit_zero::encryption::timelock::KdfParams
982///
983/// ## Cadence variants
984///
985/// ```text
986/// cadence = None
987/// cadence = DayOfWeek(Tuesday)
988/// cadence = DayOfMonth(15)
989/// cadence = MonthOfYear(January)
990/// cadence = DayOfWeekInMonth(Tuesday, January)
991/// cadence = DayOfMonthInMonth(15, January)
992/// cadence = DayOfWeekAndDayOfMonth(Tuesday, 15)
993/// ```
994///
995/// ## What the macro expands to
996///
997/// ```rust,ignore
998/// // Encryption path:
999/// #[timelock(precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k)]
1000/// fn enc_key() {}
1001/// // expands to:
1002/// // let enc_key = ::toolkit_zero::encryption::timelock::timelock(
1003/// // Some(TimeLockCadence::None),
1004/// // Some(TimeLockTime::new(14, 37).unwrap()),
1005/// // Some(TimePrecision::Minute),
1006/// // Some(TimeFormat::Hour24),
1007/// // Some(s), Some(k), None,
1008/// // )?;
1009///
1010/// // Decryption path:
1011/// #[timelock(params = header)]
1012/// fn dec_key() {}
1013/// // expands to:
1014/// // let dec_key = ::toolkit_zero::encryption::timelock::timelock(
1015/// // None, None, None, None, None, None,
1016/// // Some(header),
1017/// // )?;
1018/// ```
1019///
1020/// Full documentation and examples are in the
1021/// [crate-level `#[timelock]` section](self#timelock--inline-key-derivation).
1022#[cfg(any(
1023 feature = "enc-timelock-keygen-now",
1024 feature = "enc-timelock-keygen-input",
1025 feature = "enc-timelock-async-keygen-now",
1026 feature = "enc-timelock-async-keygen-input",
1027))]
1028#[proc_macro_attribute]
1029pub fn timelock(attr: TokenStream, item: TokenStream) -> TokenStream {
1030 timelock_macro::expand_timelock(attr, item)
1031}
1032
1033// ─── #[dependencies] ──────────────────────────────────────────────────────────
1034
1035/// Embed and parse (or read the raw bytes of) the build-time
1036/// `fingerprint.json` fingerprint in one expression.
1037///
1038/// Apply to an empty `fn`; the function name becomes the `let` binding in
1039/// the expansion. Requires the `dependency-graph-capture` feature.
1040///
1041/// ## Parse mode (default)
1042///
1043/// ```rust,ignore
1044/// use toolkit_zero::dependency_graph::capture::{dependencies, BuildTimeFingerprintData};
1045///
1046/// fn show() -> Result<(), Box<dyn std::error::Error>> {
1047/// #[dependencies]
1048/// fn data() {}
1049/// // expands to:
1050/// // const __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__: &str =
1051/// // include_str!(concat!(env!("OUT_DIR"), "/fingerprint.json"));
1052/// // let data = capture::parse(__TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__)?;
1053///
1054/// println!("{} v{}", data.package.name, data.package.version);
1055/// Ok(())
1056/// }
1057/// ```
1058///
1059/// ## Bytes mode
1060///
1061/// ```rust,ignore
1062/// fn raw_bytes() -> &'static [u8] {
1063/// #[dependencies(bytes)]
1064/// fn raw() {}
1065/// // expands to:
1066/// // const __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__: &str =
1067/// // include_str!(concat!(env!("OUT_DIR"), "/fingerprint.json"));
1068/// // let raw: &'static [u8] = __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__.as_bytes();
1069/// raw
1070/// }
1071/// ```
1072///
1073/// Full documentation is in the
1074/// [crate-level `#[dependencies]` section](self#dependencies--build-time-fingerprint-capture).
1075#[cfg(feature = "dep-graph-capture")]
1076#[proc_macro_attribute]
1077pub fn dependencies(attr: TokenStream, item: TokenStream) -> TokenStream {
1078 dependencies_macro::expand_dependencies(attr, item)
1079}