Skip to main content

forge_macros/
lib.rs

1#![allow(clippy::unwrap_used, clippy::indexing_slicing)]
2
3use proc_macro::TokenStream;
4
5pub(crate) mod attrs;
6mod cron;
7mod daemon;
8mod enum_type;
9mod job;
10mod mcp_tool;
11mod model;
12mod mutation;
13mod query;
14mod sql_extractor;
15pub(crate) mod utils;
16mod webhook;
17mod workflow;
18
19/// Marks a struct as a FORGE model, generating schema metadata for TypeScript codegen.
20///
21/// # Example
22/// ```ignore
23/// #[forge::model]
24/// pub struct User {
25///     pub id: Uuid,
26///     pub email: String,
27///     pub name: String,
28///     pub created_at: DateTime<Utc>,
29/// }
30/// ```
31#[proc_macro_attribute]
32pub fn model(attr: TokenStream, item: TokenStream) -> TokenStream {
33    model::expand_model(attr, item)
34}
35
36/// Marks an enum for database storage as a PostgreSQL ENUM type.
37///
38/// # Example
39/// ```ignore
40/// #[forge::forge_enum]
41/// pub enum ProjectStatus {
42///     Draft,
43///     Active,
44///     Paused,
45///     Completed,
46/// }
47/// ```
48#[proc_macro_attribute]
49pub fn forge_enum(attr: TokenStream, item: TokenStream) -> TokenStream {
50    enum_type::expand_enum(attr, item)
51}
52
53/// Marks a function as a query (read-only, cacheable, subscribable).
54///
55/// # Authentication
56/// By default, queries require an authenticated user. Override with:
57/// - `public` - No authentication required
58/// - `require_role("admin")` - Require specific role
59///
60/// # Attributes
61/// - `cache = "5m"` - Cache TTL (duration like "30s", "5m", "1h")
62/// - `log` - Enable logging for this query
63/// - `timeout = 30` - Timeout in seconds. For HTTP-capable handlers, an
64///   explicit timeout also becomes the default outbound HTTP timeout for `ctx.http()`
65/// - `tables("users", "projects")` - Explicit table dependencies (for dynamic SQL)
66///
67/// # Table Dependency Extraction
68/// By default, table dependencies are automatically extracted from SQL strings
69/// in the function body at compile time. This enables accurate reactive
70/// subscription invalidation for queries that join multiple tables.
71///
72/// For dynamic SQL (e.g., table names built at runtime), use the `tables`
73/// attribute to explicitly specify dependencies.
74///
75/// # Example
76/// ```ignore
77/// #[forge::query]  // Requires authenticated user (default)
78/// pub async fn get_user(ctx: &QueryContext, user_id: Uuid) -> Result<User> {
79///     // Tables automatically extracted from SQL
80/// }
81///
82/// #[forge::query(public)]  // No auth required
83/// pub async fn get_public_data(ctx: &QueryContext) -> Result<Data> {
84///     // ...
85/// }
86///
87/// #[forge::query(require_role("admin"), cache = "5m", log)]
88/// pub async fn admin_stats(ctx: &QueryContext) -> Result<Stats> {
89///     // Requires admin role
90/// }
91///
92/// #[forge::query(tables("users", "audit_log"))]
93/// pub async fn dynamic_query(ctx: &QueryContext, table: String) -> Result<Vec<Row>> {
94///     // Explicit tables for dynamic SQL
95/// }
96/// ```
97#[proc_macro_attribute]
98pub fn query(attr: TokenStream, item: TokenStream) -> TokenStream {
99    query::expand_query(attr, item)
100}
101
102/// Marks a function as a mutation (transactional write).
103///
104/// Mutations run within a database transaction. All changes commit together or roll back on error.
105///
106/// # Authentication
107/// By default, mutations require an authenticated user. Override with:
108/// - `public` - No authentication required
109/// - `require_role("admin")` - Require specific role
110///
111/// # Attributes
112/// - `log` - Enable logging for this mutation
113/// - `timeout = 30` - Timeout in seconds. Also becomes the default outbound
114///   HTTP timeout for `ctx.http()` when explicitly set
115///
116/// # Example
117/// ```ignore
118/// #[forge::mutation]  // Requires authenticated user (default)
119/// pub async fn create_project(ctx: &MutationContext, input: CreateProjectInput) -> Result<Project> {
120///     // input.user_id is pre-validated against the JWT by the router
121///     // ...
122/// }
123///
124/// #[forge::mutation(public)]  // No auth required
125/// pub async fn submit_feedback(ctx: &MutationContext, input: FeedbackInput) -> Result<()> {
126///     // ...
127/// }
128///
129/// #[forge::mutation(require_role("admin"), log)]
130/// pub async fn delete_user(ctx: &MutationContext, user_id: Uuid) -> Result<()> {
131///     // Requires admin role
132/// }
133/// ```
134#[proc_macro_attribute]
135pub fn mutation(attr: TokenStream, item: TokenStream) -> TokenStream {
136    mutation::expand_mutation(attr, item)
137}
138
139/// Marks a function as an MCP tool.
140///
141/// MCP tools are explicitly opt-in and exposed through the MCP endpoint.
142///
143/// # Attributes
144/// - `name = "tool_name"` - Override the exposed tool name
145/// - `title = "Human title"` - Display title for MCP clients
146/// - `description = "..."` - Tool description
147/// - `public` - No authentication required
148/// - `require_role("admin")` - Require specific role
149/// - `timeout = 30` - Timeout in seconds
150/// - `rate_limit(requests = 100, per = "1m", key = "user")`
151/// - `read_only` - Annotation hint for clients
152/// - `destructive` - Annotation hint for clients
153/// - `idempotent` - Annotation hint for clients
154/// - `open_world` - Annotation hint for clients
155/// - Parameter `#[schemars(...)]` / `#[serde(...)]` attrs - Included in generated input schema
156#[proc_macro_attribute]
157pub fn mcp_tool(attr: TokenStream, item: TokenStream) -> TokenStream {
158    mcp_tool::expand_mcp_tool(attr, item)
159}
160
161/// Marks a function as a background job.
162///
163/// Jobs are durable background tasks that survive server restarts and automatically retry on failure.
164///
165/// # Authentication
166/// By default, jobs require an authenticated user to dispatch. Override with:
167/// - `public` - Can be dispatched without authentication
168/// - `require_role("admin")` - Requires specific role to dispatch
169///
170/// # Attributes
171/// - `timeout = "30m"` - Job timeout (supports s, m, h suffixes). Also becomes
172///   the default outbound HTTP timeout for `ctx.http()` when explicitly set
173/// - `priority = "normal"` - background, low, normal, high, critical
174/// - `max_attempts = 3` - Maximum retry attempts
175/// - `backoff = "exponential"` - fixed, linear, or exponential
176/// - `max_backoff = "5m"` - Maximum backoff duration
177/// - `retry(max_attempts = 3, backoff = "exponential", max_backoff = "5m")` - Grouped retry config
178/// - `worker_capability = "media"` - Required worker capability
179/// - `idempotent` - Mark job as idempotent
180/// - `idempotent(key = "input.id")` - Idempotent with custom key
181/// - `name = "custom_name"` - Override job name
182///
183/// # Example
184/// ```ignore
185/// #[forge::job(timeout = "30m", priority = "high")]  // Requires authenticated user (default)
186/// pub async fn send_welcome_email(ctx: &JobContext, input: SendEmailInput) -> Result<()> {
187///     // ...
188/// }
189///
190/// #[forge::job(public)]  // Can be dispatched without auth
191/// pub async fn process_webhook(ctx: &JobContext, input: WebhookInput) -> Result<()> {
192///     // ...
193/// }
194///
195/// #[forge::job(retry(max_attempts = 5, backoff = "exponential"), require_role("admin"))]
196/// pub async fn process_payment(ctx: &JobContext, input: PaymentInput) -> Result<()> {
197///     // Requires admin role to dispatch
198/// }
199/// ```
200#[proc_macro_attribute]
201pub fn job(attr: TokenStream, item: TokenStream) -> TokenStream {
202    job::job_impl(attr, item)
203}
204
205/// Marks a function as a scheduled cron task.
206///
207/// Cron jobs run on a schedule, exactly once per scheduled time across the cluster.
208///
209/// # Attributes
210/// All attributes are specified inline within the macro:
211/// - First argument: Cron schedule expression (required)
212/// - `timezone = "UTC"` - Timezone for the schedule
213/// - `timeout = "1h"` - Execution timeout. Also becomes the default outbound
214///   HTTP timeout for `ctx.http()` when explicitly set
215/// - `catch_up` - Run missed executions after downtime
216/// - `catch_up_limit = 10` - Maximum number of catch-up runs
217///
218/// # Example
219/// ```ignore
220/// #[forge::cron("0 0 * * *", timezone = "America/New_York", timeout = "30m", catch_up)]
221/// pub async fn daily_cleanup(ctx: &CronContext) -> Result<()> {
222///     // ...
223/// }
224/// ```
225#[proc_macro_attribute]
226pub fn cron(attr: TokenStream, item: TokenStream) -> TokenStream {
227    cron::cron_impl(attr, item)
228}
229
230/// Marks a function as a durable workflow.
231///
232/// Workflows are multi-step processes that survive restarts and handle failures with compensation.
233/// Each workflow has a stable logical name, an explicit user-facing version, and a derived
234/// signature that acts as the hard runtime safety gate for resumption.
235///
236/// # Versioning and step-name stability
237/// Step names (the string literals passed to `ctx.step()`) and wait keys (the string
238/// literals passed to `ctx.wait_for_event()`) are part of the workflow's **persisted
239/// contract**. The macro hashes them together with the version string, timeout, and
240/// input/output type names into a signature that is stored with every new run.
241///
242/// **Renaming a step or wait key under the same version is a breaking change.** Any
243/// in-flight run that tries to resume after such a rename will be blocked with
244/// `WorkflowStatus::BlockedSignatureMismatch` because the stored signature no longer
245/// matches the binary's signature. Use `cargo expand` to inspect the `forge:contract`
246/// doc comment on the generated struct — it lists every key contributing to the
247/// signature.
248///
249/// When you need to rename a step (or add/remove steps, change event contracts, or
250/// alter the timeout), create a new version instead:
251/// 1. Annotate the old function with `deprecated` — the runtime keeps it alive for draining.
252/// 2. Write a new function with a new `version` string containing your changes.
253/// 3. Remove the old function once all its in-flight runs have completed.
254///
255/// The runtime derives a signature from step keys, wait keys, timeout, and type shapes.
256/// If you change the persisted contract under the same version, registration will fail.
257///
258/// # Authentication
259/// By default, workflows require an authenticated user to start. Override with:
260/// - `public` - Can be started without authentication
261/// - `require_role("admin")` - Requires specific role to start
262///
263/// # Attributes
264/// - `name = "logical_name"` - Stable workflow name (defaults to function name)
265/// - `version = "2026-05"` - User-facing version id (dates, semver, or labels)
266/// - `active` - This is the active version; new runs start here (default if neither set)
267/// - `deprecated` - Kept for draining old runs; no new runs will start on this version
268/// - `timeout = "24h"` - Maximum execution time. Also becomes the default
269///   outbound HTTP timeout for `ctx.http()` when explicitly set
270///
271/// # Example
272/// ```ignore
273/// // Old version kept alive for draining incomplete runs
274/// #[forge::workflow(name = "user_onboarding", version = "2026-03", deprecated)]
275/// pub async fn user_onboarding_v1(ctx: &WorkflowContext, input: Input) -> Result<Output> {
276///     let user = ctx.step("create_user", || async { /* ... */ }).await?;
277///     ctx.step("send_welcome", || async { /* ... */ }).await;
278///     Ok(Output { user })
279/// }
280///
281/// // New active version with an additional step
282/// #[forge::workflow(name = "user_onboarding", version = "2026-05", active)]
283/// pub async fn user_onboarding_v2(ctx: &WorkflowContext, input: Input) -> Result<Output> {
284///     let user = ctx.step("create_user", || async { /* ... */ }).await?;
285///     ctx.step("send_welcome", || async { /* ... */ }).await;
286///     ctx.step("sync_crm", || async { /* ... */ }).await;
287///     Ok(Output { user })
288/// }
289/// ```
290#[proc_macro_attribute]
291pub fn workflow(attr: TokenStream, item: TokenStream) -> TokenStream {
292    workflow::workflow_impl(attr, item)
293}
294
295/// Marks a function as a long-running daemon.
296///
297/// Daemons are singleton background tasks that run continuously. They support
298/// leader election (only one instance runs across the cluster), automatic restart
299/// on panic, and graceful shutdown handling.
300///
301/// # Attributes
302/// - `leader_elected = true` - Only one instance runs across cluster (default: true)
303/// - `restart_on_panic = true` - Restart if daemon panics (default: true)
304/// - `timeout = "30s"` - Default outbound HTTP timeout for `ctx.http()`
305/// - `restart_delay = "5s"` - Delay before restart after failure
306/// - `startup_delay = "10s"` - Delay before first execution after startup
307/// - `max_restarts = 10` - Maximum restart attempts (default: unlimited)
308///
309/// # Shutdown Handling
310/// Daemons must handle graceful shutdown by checking `ctx.shutdown_signal()`:
311///
312/// ```ignore
313/// loop {
314///     // Do work
315///     tokio::select! {
316///         _ = tokio::time::sleep(Duration::from_secs(60)) => {}
317///         _ = ctx.shutdown_signal() => break,
318///     }
319/// }
320/// ```
321///
322/// # Example
323/// ```ignore
324/// #[forge::daemon(startup_delay = "5s", restart_on_panic = true)]
325/// pub async fn heartbeat_daemon(ctx: &DaemonContext) -> Result<()> {
326///     loop {
327///         // Update heartbeat
328///         sqlx::query("UPDATE app_status SET heartbeat = NOW()").execute(ctx.db()).await?;
329///
330///         tokio::select! {
331///             _ = tokio::time::sleep(Duration::from_secs(30)) => {}
332///             _ = ctx.shutdown_signal() => break,
333///         }
334///     }
335///     Ok(())
336/// }
337///
338/// #[forge::daemon(leader_elected = false, max_restarts = 5)]
339/// pub async fn worker_daemon(ctx: &DaemonContext) -> Result<()> {
340///     // Runs on all nodes, limited restarts
341/// }
342/// ```
343#[proc_macro_attribute]
344pub fn daemon(attr: TokenStream, item: TokenStream) -> TokenStream {
345    daemon::daemon_impl(attr, item)
346}
347
348/// Marks a function as a webhook handler.
349///
350/// Webhooks are HTTP endpoints for receiving external events (e.g., from Stripe, GitHub).
351/// They support signature validation, idempotency, and bypass authentication middleware.
352///
353/// # Attributes
354/// - `path = "/webhooks/stripe"` - URL path (required)
355/// - `signature = WebhookSignature::hmac_sha256("Header", "SECRET_ENV")` - Signature validation
356/// - `idempotency = "header:X-Request-Id"` - Idempotency key source
357/// - `timeout = "30s"` - Request timeout. Also becomes the default outbound
358///   HTTP timeout for `ctx.http()` when explicitly set
359///
360/// # Signature Validation
361/// Use `WebhookSignature` helper. Pick the constructor that matches the sender:
362/// - `WebhookSignature::hmac_sha256("Header", "SECRET_ENV")` - HMAC-SHA256 (e.g. GitHub)
363/// - `WebhookSignature::stripe_webhooks("SECRET_ENV")` - Stripe (`stripe-signature` header, 300s replay window)
364/// - `WebhookSignature::shopify_webhooks("SECRET_ENV")` - Shopify (`x-shopify-hmac-sha256`, base64)
365/// - `WebhookSignature::ed25519("Header", "PUBKEY_ENV")` - Ed25519 with a base64-encoded public key
366///
367/// # Idempotency
368/// Specify source as `"header:Header-Name"` or `"body:$.json.path"`:
369/// - `"header:X-GitHub-Delivery"` - From header
370/// - `"body:$.id"` - From JSON body field
371///
372/// # Example
373/// ```ignore
374/// #[forge::webhook(
375///     path = "/webhooks/github",
376///     signature = WebhookSignature::hmac_sha256("X-Hub-Signature-256", "GITHUB_SECRET"),
377///     idempotency = "header:X-GitHub-Delivery",
378/// )]
379/// pub async fn github_webhook(ctx: &WebhookContext, payload: Value) -> Result<WebhookResult> {
380///     let event_type = ctx.header("X-GitHub-Event").unwrap_or("unknown");
381///     ctx.dispatch_job("process_github_event", &payload).await?;
382///     Ok(WebhookResult::Accepted)
383/// }
384///
385/// #[forge::webhook(path = "/webhooks/stripe", timeout = "60s")]
386/// pub async fn stripe_webhook(ctx: &WebhookContext, payload: Value) -> Result<WebhookResult> {
387///     // Process Stripe event
388///     Ok(WebhookResult::Ok)
389/// }
390/// ```
391#[proc_macro_attribute]
392pub fn webhook(attr: TokenStream, item: TokenStream) -> TokenStream {
393    webhook::webhook_impl(attr, item)
394}