Skip to main content

Crate toolkit_zero_macros

Crate toolkit_zero_macros 

Source
Expand description

Procedural macros for toolkit-zero.

This crate is an internal implementation detail of toolkit-zero. Do not depend on it directly — all macros are re-exported through the relevant toolkit-zero module:

MacroEnable withImport from
[mechanism]features = ["socket-server"]toolkit_zero::socket::server
[request]features = ["socket-client"]toolkit_zero::socket::client
[serializable]features = ["serialization"]toolkit_zero::serialization
[serialize]features = ["serialization"]toolkit_zero::serialization
[deserialize]features = ["serialization"]toolkit_zero::serialization
[browser]features = ["location"]toolkit_zero::location
[timelock]any enc-timelock-* featuretoolkit_zero::encryption::timelock
[dependencies]features = ["dependency-graph-capture"]toolkit_zero::dependency_graph::capture

§#[mechanism] — server-side route declaration

Replaces a decorated fn item with a server.mechanism(…) builder statement. The function body is transplanted verbatim into the .onconnect(…) closure; all variables from the enclosing scope are accessible via move capture.

§Syntax

#[mechanism(server, METHOD, "/path")]
#[mechanism(server, METHOD, "/path", json)]
#[mechanism(server, METHOD, "/path", query)]
#[mechanism(server, METHOD, "/path", encrypted(<key_expr>))]
#[mechanism(server, METHOD, "/path", encrypted_query(<key_expr>))]
#[mechanism(server, METHOD, "/path", state(<state_expr>))]
#[mechanism(server, METHOD, "/path", state(<state_expr>), json)]
#[mechanism(server, METHOD, "/path", state(<state_expr>), query)]
#[mechanism(server, METHOD, "/path", state(<state_expr>), encrypted(<key_expr>))]
#[mechanism(server, METHOD, "/path", state(<state_expr>), encrypted_query(<key_expr>))]

The first three arguments (server, METHOD, "/path") are positional and required. Keywords after the path (json, query, state(…), encrypted(…), encrypted_query(…)) may appear in any order.

§Parameters

ArgumentTypeDescription
serveridentThe Server variable in scope
METHODidentHTTP verb: GET POST PUT DELETE PATCH HEAD OPTIONS
"/path"string literalRoute path
jsonkeywordDeserialise JSON body; fn receives (body: T)
querykeywordDeserialise URL query params; fn receives (params: T)
encrypted(key)keyword + exprDecrypt body (ChaCha20-Poly1305); fn receives (body: T)
encrypted_query(key)keyword + exprDecrypt query (ChaCha20-Poly1305); fn receives (params: T)
state(expr)keyword + exprClone state into handler; fn first param is (state: S)

When state is combined with a body mode, the function receives two parameters: state first, then body/params.

§Function signature rules

  • The function may be async or non-async — it is always wrapped in async move { … }.
  • The return type annotation is ignored; Rust infers it from the reply! macro inside the body.
  • The number of parameters must match the chosen mode exactly (compile error otherwise).

§What the macro expands to

// Input:
#[mechanism(server, POST, "/items", json)]
async fn create(body: NewItem) {
    reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
}

// Expanded to:
server.mechanism(
    toolkit_zero::socket::server::ServerMechanism::post("/items")
        .json::<NewItem>()
        .onconnect(|body: NewItem| async move {
            reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
        })
);

§Example

use toolkit_zero::socket::server::{Server, mechanism, reply, Status, SerializationKey};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};

#[derive(Deserialize, Serialize, Clone)] struct Item    { id: u32, name: String }
#[derive(Deserialize)]                   struct NewItem  { name: String }
#[derive(Deserialize)]                   struct Filter   { page: u32 }

#[tokio::main]
async fn main() {
    let mut server = Server::default();
    let db: Arc<Mutex<Vec<Item>>> = Arc::new(Mutex::new(vec![]));

    // Plain GET — no body
    #[mechanism(server, GET, "/health")]
    async fn health() { reply!() }

    // JSON body
    #[mechanism(server, POST, "/items", json)]
    async fn create(body: NewItem) {
        reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
    }

    // URL query params
    #[mechanism(server, GET, "/items", query)]
    async fn list(filter: Filter) {
        let _ = filter.page;
        reply!()
    }

    // Shared state + JSON body
    #[mechanism(server, POST, "/items/add", state(db.clone()), json)]
    async fn add(db: Arc<Mutex<Vec<Item>>>, body: NewItem) {
        let id = db.lock().unwrap().len() as u32 + 1;
        db.lock().unwrap().push(Item { id, name: body.name.clone() });
        reply!(json => Item { id, name: body.name }, status => Status::Created)
    }

    // ChaCha20-Poly1305-encrypted body
    #[mechanism(server, POST, "/secure", encrypted(SerializationKey::Default))]
    async fn secure(body: NewItem) {
        reply!(json => Item { id: 99, name: body.name })
    }

    server.serve(([127, 0, 0, 1], 8080)).await;
}

§#[request] — client-side request shorthand

Replaces a decorated fn item with an inline let binding that performs an HTTP request. The function name becomes the binding name; the return type becomes R in the .send::<R>() turbofish. The function body is discarded entirely.

§Syntax

#[request(client, METHOD, "/path", async|sync)]
#[request(client, METHOD, "/path", json(<body_expr>), async|sync)]
#[request(client, METHOD, "/path", query(<params_expr>), async|sync)]
#[request(client, METHOD, "/path", encrypted(<body_expr>, <key_expr>), async|sync)]
#[request(client, METHOD, "/path", encrypted_query(<params_expr>, <key_expr>), async|sync)]

The first three arguments are positional. The mode keyword (if any) comes before the mandatory async or sync terminator.

§Parameters

ArgumentDescription
clientThe Client variable in scope
METHODHTTP verb: GET POST PUT DELETE PATCH HEAD OPTIONS
"/path"Endpoint path string literal
json(expr)Serialise expr as a JSON body (Content-Type: application/json)
query(expr)Serialise expr as URL query parameters
encrypted(body, key)Seal body (ChaCha20-Poly1305) with key before sending
encrypted_query(params, key)Seal params (ChaCha20-Poly1305), send as ?data=<base64url>
asyncFinalise with .send::<R>().await?
syncFinalise with .send_sync::<R>()?

§Requirements

  • A return type annotation is required — it is used as R in the turbofish. Omitting it is a compile error.
  • The enclosing function must return Result<_, E> where E: From<reqwest::Error> (plain/json/query modes) or E: From<ClientError> (encrypted modes), so that ? can propagate.

§What the macro expands to

// Input:
#[request(client, POST, "/items", json(NewItem { name: "widget".into() }), async)]
async fn created() -> Item {}

// Expanded to:
let created: Item = client.post("/items")
    .json(NewItem { name: "widget".into() })
    .send::<Item>()
    .await?;

§Example

use toolkit_zero::socket::client::{Client, Target, request};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)] struct Item    { id: u32, name: String }
#[derive(Serialize)]              struct NewItem  { name: String }
#[derive(Serialize)]              struct Filter   { page: u32 }

async fn example() -> Result<(), reqwest::Error> {
    let client = Client::new_async(Target::Localhost(8080));

    // GET → let items: Vec<Item> = client.get("/items").send::<Vec<Item>>().await?
    #[request(client, GET, "/items", async)]
    async fn items() -> Vec<Item> {}

    // POST with JSON body
    #[request(client, POST, "/items", json(NewItem { name: "widget".into() }), async)]
    async fn created() -> Item {}

    // GET with query params
    #[request(client, GET, "/items", query(Filter { page: 2 }), async)]
    async fn page() -> Vec<Item> {}

    // Synchronous DELETE
    #[request(client, DELETE, "/items/1", sync)]
    fn deleted() -> Item {}

    Ok(())
}

§#[serializable] — derive + inject seal/open

Automatically derives bincode::Encode + bincode::Decode on a struct or enum and injects three methods:

fn seal(&self, key: Option<String>) -> Result<Vec<u8>, SerializationError>
fn open(bytes: &[u8], key: Option<String>) -> Result<Self, SerializationError>

The key is moved in and internally wrapped in Zeroizing<String>, wiping it from memory on drop. Pass None to use the built-in default key.

Field-level #[serializable(key = "literal")] additionally generates per-field helpers with the key baked in:

fn seal_<field>(&self)        -> Result<Vec<u8>, SerializationError>
fn open_<field>(bytes: &[u8]) -> Result<FieldType, SerializationError>

§Syntax

#[serializable]                      // on a struct or enum
struct Foo { … }

#[serializable]                      // field annotation inside a struct
struct Bar {
    pub normal: String,
    #[serializable(key = "my-key")]  // generates seal_secret / open_secret
    pub secret: String,
}

Note: #[serializable] on an enum does not scan fields for the field-level annotation — only named struct fields are supported for per-field helpers.

§What the macro expands to

// Input:
#[serializable]
struct Config { host: String, port: u16 }

// Expanded to:
#[derive(::toolkit_zero::serialization::Encode, ::toolkit_zero::serialization::Decode)]
struct Config { host: String, port: u16 }

impl Config {
    pub fn seal(&self, key: Option<String>) -> Result<Vec<u8>, SerializationError> {
        ::toolkit_zero::serialization::seal(self, key)
    }
    pub fn open(bytes: &[u8], key: Option<String>) -> Result<Self, SerializationError> {
        ::toolkit_zero::serialization::open::<Self>(bytes, key)
    }
}

§Example

use toolkit_zero::serialization::serializable;

#[serializable]
struct Config { host: String, port: u16 }

let c = Config { host: "localhost".into(), port: 8080 };

// Seal / open via struct methods
let blob = c.seal(None).unwrap();
let back = Config::open(&blob, None).unwrap();
assert_eq!(c.host, back.host);

// Custom key — moved in, zeroized on drop
let blob2 = c.seal(Some("secret".to_string())).unwrap();
let back2 = Config::open(&blob2, Some("secret".to_string())).unwrap();

// Per-field annotation
#[serializable]
struct Creds {
    pub user: String,
    #[serializable(key = "field-secret")]
    pub password: String,
}

let creds = Creds { user: "alice".into(), password: "hunter2".into() };
let pw_blob = creds.seal_password().unwrap();    // key baked in
let pw_back = Creds::open_password(&pw_blob).unwrap();
assert_eq!("hunter2", pw_back);

§#[serialize] — inline seal statement

Replaces a fn item with an inline seal statement. Two modes are selected by the presence or absence of path:

  • Variable mode (path absent) — emits a let binding. The function name becomes the variable name; the return type is required and becomes the type annotation. The function body is discarded.
  • File write mode (path = "..." present) — emits std::fs::write(path, seal(…)?)?. The function name and return type are ignored.

§Syntax

// Variable mode
#[serialize(source_expr)]
#[serialize(source_expr, key = key_expr)]

// File write mode  
#[serialize(source_expr, path = "file.bin")]
#[serialize(source_expr, path = "file.bin", key = key_expr)]
ArgumentRequiredDescription
source_expryesExpression to seal (must be bincode::Encode)
key = exprnoKey expression — Option<String> moved into seal()
path = "..."noFile path — switches to file write mode

key and path may appear in any order after source_expr.

§What the macro expands to

// Variable mode:
#[serialize(cfg, key = my_key)]
fn blob() -> Vec<u8> {}
// →
let blob: Vec<u8> = ::toolkit_zero::serialization::seal(&cfg, Some(my_key))?;

// Variable mode, default key:
#[serialize(cfg)]
fn blob() -> Vec<u8> {}
// →
let blob: Vec<u8> = ::toolkit_zero::serialization::seal(&cfg, None)?;

// File write mode:
#[serialize(cfg, path = "out.bin", key = my_key)]
fn _() {}
// →
::std::fs::write("out.bin", ::toolkit_zero::serialization::seal(&cfg, Some(my_key))?)?;

§Example

use toolkit_zero::serialization::{serializable, serialize};

#[serializable]
struct Config { threshold: f64 }

fn save(cfg: &Config, key: String) -> Result<(), Box<dyn std::error::Error>> {
    // Seal to a variable
    #[serialize(cfg, key = key.clone())]
    fn blob() -> Vec<u8> {}
    // blob: Vec<u8> is now in scope

    // Write directly to a file
    #[serialize(cfg, path = "config.bin", key = key)]
    fn _() {}

    Ok(())
}

§#[deserialize] — inline open statement

Replaces a fn item with an inline open statement. Two modes:

  • Variable mode (path absent) — opens from a blob expression already in scope. The function name becomes the binding name; the return type is required and used as the turbofish type T in open::<T>.
  • File read mode (path = "..." present) — reads the file first, then opens. Same name/return-type rules apply.

§Syntax

// Variable mode
#[deserialize(blob_expr)]
#[deserialize(blob_expr, key = key_expr)]

// File read mode
#[deserialize(path = "file.bin")]
#[deserialize(path = "file.bin", key = key_expr)]
ArgumentRequiredDescription
blob_expryes (variable mode)Expression whose value is &[u8] (reference taken automatically)
path = "..."yes (file mode)File to read; switches to file read mode
key = exprnoKey expression — Option<String> moved into open()

§What the macro expands to

// Variable mode:
#[deserialize(blob, key = my_key)]
fn config() -> Config {}
// →
let config: Config = ::toolkit_zero::serialization::open::<Config>(&blob, Some(my_key))?;

// Variable mode, default key:
#[deserialize(blob)]
fn config() -> Config {}
// →
let config: Config = ::toolkit_zero::serialization::open::<Config>(&blob, None)?;

// File read mode:
#[deserialize(path = "config.bin", key = my_key)]
fn config() -> Config {}
// →
let config: Config = ::toolkit_zero::serialization::open::<Config>(
    &::std::fs::read("config.bin")?,
    Some(my_key),
)?;

§Example

use toolkit_zero::serialization::{serializable, serialize, deserialize};

#[serializable]
struct Config { threshold: f64 }

fn round_trip(cfg: &Config, key: String) -> Result<Config, Box<dyn std::error::Error>> {
    // Write to disk
    #[serialize(cfg, path = "config.bin", key = key.clone())]
    fn _() {}

    // Read back
    #[deserialize(path = "config.bin", key = key)]
    fn loaded() -> Config {}

    Ok(loaded)
}

fn from_bytes(blob: Vec<u8>) -> Result<Config, Box<dyn std::error::Error>> {
    #[deserialize(blob)]
    fn cfg() -> Config {}

    Ok(cfg)
}

§#[browser] — inline location capture

Replaces a fn item with an inline call to either __location__ (sync) or __location_async__ (async, default). The PageTemplate is built from the macro arguments — no need to construct it manually.

The function name becomes the binding that holds the resulting LocationData. The function body is discarded. A ? is appended so any LocationError propagates to the enclosing function.

§Syntax

#[browser]                                        // async, Default template
#[browser(sync)]                                  // blocking, Default template
#[browser(title = "My App")]                      // async, Default, custom title
#[browser(title = "T", body = "B")]               // async, Default, title + body
#[browser(tickbox)]                               // async, Tickbox template (all defaults)
#[browser(tickbox, title = "T", consent = "C")]   // async, Tickbox with arguments
#[browser(html = "<html>…</html>")]               // async, Custom template
#[browser(sync, html = "…")]                      // sync + Custom template

All arguments are optional and may appear in any order.

§Arguments

ArgumentTypeNotes
syncflagUse the blocking __location__ function; default uses __location_async__().await
tickboxflagUse PageTemplate::Tickbox; incompatible with html
title = "…"string literalTab/heading title for Default or Tickbox
body = "…"string literalBody paragraph text for Default or Tickbox
consent = "…"string literalCheckbox label for Tickbox only
html = "…"string literalUse PageTemplate::Custom; mutually exclusive with all other template args

§Expansion examples

use toolkit_zero::location::{browser, LocationData};

// Async, Default template with a custom title:
async fn get_loc() -> Result<LocationData, Box<dyn std::error::Error>> {
    #[browser(title = "My App")]
    fn loc() {}
    // expands to:
    // let loc = ::toolkit_zero::location::browser::__location_async__(
    //     ::toolkit_zero::location::browser::PageTemplate::Default {
    //         title:     Some("My App".to_string()),
    //         body_text: None,
    //     }
    // ).await?;
    Ok(loc)
}

// Sync, Tickbox with consent text:
fn get_loc_sync() -> Result<LocationData, Box<dyn std::error::Error>> {
    #[browser(sync, tickbox, consent = "I agree")]
    fn loc() {}
    // expands to:
    // let loc = ::toolkit_zero::location::browser::__location__(
    //     ::toolkit_zero::location::browser::PageTemplate::Tickbox {
    //         title:        None,
    //         body_text:    None,
    //         consent_text: Some("I agree".to_string()),
    //     }
    // )?;
    Ok(loc)
}

§#[timelock] — inline key derivation

Replaces a fn item with an inline call to either timelock (sync) or timelock_async (async — add the async flag). The 7 positional Option<…> arguments are built from named keyword arguments, and the correct path (encryption or decryption) is selected automatically.

The function name becomes the binding; the function body is discarded. A ? propagates any TimeLockError.

§Two paths

PathSupplyRequired args
EncryptionExplicit time → derive keyprecision, format, time, salts, kdf
DecryptionStored header → same keyparams (a TimeLockParams)

§Syntax

// Encryption path
#[timelock(precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k)]
#[timelock(precision = Hour, format = Hour24, time(9, 0), salts = s, kdf = k,
           cadence = DayOfWeek(Tuesday))]
#[timelock(async, precision = Minute, format = Hour24, time(14, 37), salts = s, kdf = k)]

// Decryption path
#[timelock(params = header)]
#[timelock(async, params = header)]

All arguments are keyword-based and may appear in any order.

§Arguments

ArgumentTypeNotes
asyncflagUse timelock_async().await?; default uses timelock()?
params = exprTimeLockParamsDecryption path. Mutually exclusive with all other args
precision = …Hour | Quarter | MinuteEncryption. Time quantisation level
format = …Hour12 | Hour24Encryption. Clock representation
time(h, m)two int literalsEncryption. Target time (24-hour h 0–23, m 0–59)
cadence = …see belowOptional. Calendar constraint; defaults to None
salts = exprTimeLockSaltsEncryption. Three 32-byte KDF salts
kdf = exprKdfParamsEncryption. KDF work-factor parameters

§Cadence variants

cadence = None
cadence = DayOfWeek(Tuesday)
cadence = DayOfMonth(15)
cadence = MonthOfYear(January)
cadence = DayOfWeekInMonth(Tuesday, January)
cadence = DayOfMonthInMonth(15, January)
cadence = DayOfWeekAndDayOfMonth(Tuesday, 15)

§Expansion examples

use toolkit_zero::encryption::timelock::*;

// Encryption — derive key for 14:37 with Minute precision.
fn encrypt() -> Result<TimeLockKey, TimeLockError> {
    let salts = TimeLockSalts::generate();
    let kdf   = KdfPreset::Balanced.params();

    #[timelock(precision = Minute, format = Hour24, time(14, 37), salts = salts, kdf = kdf)]
    fn enc_key() {}
    // let enc_key = timelock(
    //     Some(TimeLockCadence::None),
    //     Some(TimeLockTime::new(14, 37).unwrap()),
    //     Some(TimePrecision::Minute),
    //     Some(TimeFormat::Hour24),
    //     Some(salts), Some(kdf), None,
    // )?;
    Ok(enc_key)
}

// Decryption — re-derive from a stored header.
fn decrypt(header: TimeLockParams) -> Result<TimeLockKey, TimeLockError> {
    #[timelock(params = header)]
    fn dec_key() {}
    // let dec_key = timelock(None, None, None, None, None, None, Some(header))?;
    Ok(dec_key)
}

// Async encryption with a calendar cadence (Tuesdays only).
async fn async_encrypt() -> Result<TimeLockKey, TimeLockError> {
    let salts = TimeLockSalts::generate();
    let kdf   = KdfPreset::BalancedMac.params();

    #[timelock(async,
               precision = Minute, format = Hour24, time(14, 37),
               cadence = DayOfWeek(Tuesday),
               salts = salts, kdf = kdf)]
    fn enc_key() {}
    Ok(enc_key)
}

§#[dependencies] — build-time fingerprint capture

Embeds the fingerprint.json produced by generate_fingerprint() (feature dependency-graph-build) into the binary and binds either the parsed BuildTimeFingerprintData or the raw &'static [u8] JSON bytes.

Apply the attribute to an empty fn inside a function body; the function name becomes the let binding name. Requires the dependency-graph-capture feature.

§Syntax

#[dependencies]           // parse mode  → BuildTimeFingerprintData (propagates ?)
#[dependencies(bytes)]    // bytes mode  → &'static [u8]  (infallible)

§Expansion

Both modes expand to a const embedding followed by a let binding:

// Parse mode:
// const __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__: &str =
//     include_str!(concat!(env!("OUT_DIR"), "/fingerprint.json"));
// let <binding> = capture::parse(__TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__)?;

// Bytes mode:
// const __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__: &str =
//     include_str!(concat!(env!("OUT_DIR"), "/fingerprint.json"));
// let <binding>: &'static [u8] = __TOOLKIT_ZERO_BUILD_TIME_FINGERPRINT__.as_bytes();

§Examples

use toolkit_zero::dependency_graph::capture::dependencies;

fn show_info() -> Result<(), Box<dyn std::error::Error>> {
    #[dependencies]
    fn data() {}
    println!("{} v{}", data.package.name, data.package.version);
    println!("target  : {}", data.build.target);
    println!("lock    : {}", data.cargo_lock_sha256);
    Ok(())
}

fn raw_bytes() -> &'static [u8] {
    #[dependencies(bytes)]
    fn raw() {}
    raw
}