Skip to main content

forge_macros/
lib.rs

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