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
63/// - `tables = ["users", "projects"]` - Explicit table dependencies (for dynamic SQL)
64///
65/// # Table Dependency Extraction
66/// By default, table dependencies are automatically extracted from SQL strings
67/// in the function body at compile time. This enables accurate reactive
68/// subscription invalidation for queries that join multiple tables.
69///
70/// For dynamic SQL (e.g., table names built at runtime), use the `tables`
71/// attribute to explicitly specify dependencies.
72///
73/// # Example
74/// ```ignore
75/// #[forge::query] // Requires authenticated user (default)
76/// pub async fn get_user(ctx: &QueryContext, user_id: Uuid) -> Result<User> {
77/// // Tables automatically extracted from SQL
78/// }
79///
80/// #[forge::query(public)] // No auth required
81/// pub async fn get_public_data(ctx: &QueryContext) -> Result<Data> {
82/// // ...
83/// }
84///
85/// #[forge::query(require_role("admin"), cache = "5m", log)]
86/// pub async fn admin_stats(ctx: &QueryContext) -> Result<Stats> {
87/// // Requires admin role
88/// }
89///
90/// #[forge::query(tables = ["users", "audit_log"])]
91/// pub async fn dynamic_query(ctx: &QueryContext, table: String) -> Result<Vec<Row>> {
92/// // Explicit tables for dynamic SQL
93/// }
94/// ```
95#[proc_macro_attribute]
96pub fn query(attr: TokenStream, item: TokenStream) -> TokenStream {
97 query::expand_query(attr, item)
98}
99
100/// Marks a function as a mutation (transactional write).
101///
102/// Mutations run within a database transaction. All changes commit together or roll back on error.
103///
104/// # Authentication
105/// By default, mutations require an authenticated user. Override with:
106/// - `public` - No authentication required
107/// - `require_role("admin")` - Require specific role
108///
109/// # Attributes
110/// - `log` - Enable logging for this mutation
111/// - `timeout = 30` - Timeout in seconds
112///
113/// # Example
114/// ```ignore
115/// #[forge::mutation] // Requires authenticated user (default)
116/// pub async fn create_project(ctx: &MutationContext, input: CreateProjectInput) -> Result<Project> {
117/// let user_id = ctx.require_user_id()?;
118/// // ...
119/// }
120///
121/// #[forge::mutation(public)] // No auth required
122/// pub async fn submit_feedback(ctx: &MutationContext, input: FeedbackInput) -> Result<()> {
123/// // ...
124/// }
125///
126/// #[forge::mutation(require_role("admin"), log)]
127/// pub async fn delete_user(ctx: &MutationContext, user_id: Uuid) -> Result<()> {
128/// // Requires admin role
129/// }
130/// ```
131#[proc_macro_attribute]
132pub fn mutation(attr: TokenStream, item: TokenStream) -> TokenStream {
133 mutation::expand_mutation(attr, item)
134}
135
136/// Marks a function as an MCP tool.
137///
138/// MCP tools are explicitly opt-in and exposed through the MCP endpoint.
139///
140/// # Attributes
141/// - `name = "tool_name"` - Override the exposed tool name
142/// - `title = "Human title"` - Display title for MCP clients
143/// - `description = "..."` - Tool description
144/// - `public` - No authentication required
145/// - `require_role("admin")` - Require specific role
146/// - `timeout = 30` - Timeout in seconds
147/// - `rate_limit(requests = 100, per = "1m", key = "user")`
148/// - `read_only` - Annotation hint for clients
149/// - `destructive` - Annotation hint for clients
150/// - `idempotent` - Annotation hint for clients
151/// - `open_world` - Annotation hint for clients
152/// - Parameter `#[schemars(...)]` / `#[serde(...)]` attrs - Included in generated input schema
153#[proc_macro_attribute]
154pub fn mcp_tool(attr: TokenStream, item: TokenStream) -> TokenStream {
155 mcp_tool::expand_mcp_tool(attr, item)
156}
157
158/// Marks a function as a background job.
159///
160/// Jobs are durable background tasks that survive server restarts and automatically retry on failure.
161///
162/// # Authentication
163/// By default, jobs require an authenticated user to dispatch. Override with:
164/// - `public` - Can be dispatched without authentication
165/// - `require_role("admin")` - Requires specific role to dispatch
166///
167/// # Attributes
168/// - `timeout = "30m"` - Job timeout (supports s, m, h suffixes)
169/// - `priority = "normal"` - background, low, normal, high, critical
170/// - `max_attempts = 3` - Maximum retry attempts
171/// - `backoff = "exponential"` - fixed, linear, or exponential
172/// - `max_backoff = "5m"` - Maximum backoff duration
173/// - `retry(max_attempts = 3, backoff = "exponential", max_backoff = "5m")` - Grouped retry config
174/// - `worker_capability = "media"` - Required worker capability
175/// - `idempotent` - Mark job as idempotent
176/// - `idempotent(key = "input.id")` - Idempotent with custom key
177/// - `name = "custom_name"` - Override job name
178///
179/// # Example
180/// ```ignore
181/// #[forge::job(timeout = "30m", priority = "high")] // Requires authenticated user (default)
182/// pub async fn send_welcome_email(ctx: &JobContext, input: SendEmailInput) -> Result<()> {
183/// // ...
184/// }
185///
186/// #[forge::job(public)] // Can be dispatched without auth
187/// pub async fn process_webhook(ctx: &JobContext, input: WebhookInput) -> Result<()> {
188/// // ...
189/// }
190///
191/// #[forge::job(retry(max_attempts = 5, backoff = "exponential"), require_role("admin"))]
192/// pub async fn process_payment(ctx: &JobContext, input: PaymentInput) -> Result<()> {
193/// // Requires admin role to dispatch
194/// }
195/// ```
196#[proc_macro_attribute]
197pub fn job(attr: TokenStream, item: TokenStream) -> TokenStream {
198 job::job_impl(attr, item)
199}
200
201/// Marks a function as a scheduled cron task.
202///
203/// Cron jobs run on a schedule, exactly once per scheduled time across the cluster.
204///
205/// # Attributes
206/// All attributes are specified inline within the macro:
207/// - First argument: Cron schedule expression (required)
208/// - `timezone = "UTC"` - Timezone for the schedule
209/// - `timeout = "1h"` - Execution timeout
210/// - `catch_up` - Run missed executions after downtime
211/// - `catch_up_limit = 10` - Maximum number of catch-up runs
212///
213/// # Example
214/// ```ignore
215/// #[forge::cron("0 0 * * *", timezone = "America/New_York", timeout = "30m", catch_up)]
216/// pub async fn daily_cleanup(ctx: &CronContext) -> Result<()> {
217/// // ...
218/// }
219/// ```
220#[proc_macro_attribute]
221pub fn cron(attr: TokenStream, item: TokenStream) -> TokenStream {
222 cron::cron_impl(attr, item)
223}
224
225/// Marks a function as a durable workflow.
226///
227/// Workflows are multi-step processes that survive restarts and handle failures with compensation.
228///
229/// # Authentication
230/// By default, workflows require an authenticated user to start. Override with:
231/// - `public` - Can be started without authentication
232/// - `require_role("admin")` - Requires specific role to start
233///
234/// # Attributes
235/// - `version = 1` - Workflow version (increment for breaking changes)
236/// - `timeout = "24h"` - Maximum execution time
237/// - `name = "custom_name"` - Override workflow name
238///
239/// # Example
240/// ```ignore
241/// #[forge::workflow(version = 1, timeout = "24h")] // Requires authenticated user (default)
242/// pub async fn user_onboarding(ctx: &WorkflowContext, input: OnboardingInput) -> Result<OnboardingResult> {
243/// let user = ctx.step("create_user", || async { /* ... */ }).await?;
244/// ctx.step("send_welcome", || async { /* ... */ }).await;
245/// Ok(OnboardingResult { user })
246/// }
247///
248/// #[forge::workflow(public)] // Can be started without auth
249/// pub async fn process_webhook(ctx: &WorkflowContext, input: WebhookInput) -> Result<()> {
250/// // ...
251/// }
252///
253/// #[forge::workflow(version = 2, require_role("admin"))]
254/// pub async fn admin_workflow(ctx: &WorkflowContext, input: AdminInput) -> Result<AdminResult> {
255/// // Requires admin role to start
256/// }
257/// ```
258#[proc_macro_attribute]
259pub fn workflow(attr: TokenStream, item: TokenStream) -> TokenStream {
260 workflow::workflow_impl(attr, item)
261}
262
263/// Marks a function as a long-running daemon.
264///
265/// Daemons are singleton background tasks that run continuously. They support
266/// leader election (only one instance runs across the cluster), automatic restart
267/// on panic, and graceful shutdown handling.
268///
269/// # Attributes
270/// - `leader_elected = true` - Only one instance runs across cluster (default: true)
271/// - `restart_on_panic = true` - Restart if daemon panics (default: true)
272/// - `restart_delay = "5s"` - Delay before restart after failure
273/// - `startup_delay = "10s"` - Delay before first execution after startup
274/// - `max_restarts = 10` - Maximum restart attempts (default: unlimited)
275///
276/// # Shutdown Handling
277/// Daemons must handle graceful shutdown by checking `ctx.shutdown_signal()`:
278///
279/// ```ignore
280/// loop {
281/// // Do work
282/// tokio::select! {
283/// _ = tokio::time::sleep(Duration::from_secs(60)) => {}
284/// _ = ctx.shutdown_signal() => break,
285/// }
286/// }
287/// ```
288///
289/// # Example
290/// ```ignore
291/// #[forge::daemon(startup_delay = "5s", restart_on_panic = true)]
292/// pub async fn heartbeat_daemon(ctx: &DaemonContext) -> Result<()> {
293/// loop {
294/// // Update heartbeat
295/// sqlx::query("UPDATE app_status SET heartbeat = NOW()").execute(ctx.db()).await?;
296///
297/// tokio::select! {
298/// _ = tokio::time::sleep(Duration::from_secs(30)) => {}
299/// _ = ctx.shutdown_signal() => break,
300/// }
301/// }
302/// Ok(())
303/// }
304///
305/// #[forge::daemon(leader_elected = false, max_restarts = 5)]
306/// pub async fn worker_daemon(ctx: &DaemonContext) -> Result<()> {
307/// // Runs on all nodes, limited restarts
308/// }
309/// ```
310#[proc_macro_attribute]
311pub fn daemon(attr: TokenStream, item: TokenStream) -> TokenStream {
312 daemon::daemon_impl(attr, item)
313}
314
315/// Marks a function as a webhook handler.
316///
317/// Webhooks are HTTP endpoints for receiving external events (e.g., from Stripe, GitHub).
318/// They support signature validation, idempotency, and bypass authentication middleware.
319///
320/// # Attributes
321/// - `path = "/webhooks/stripe"` - URL path (required)
322/// - `signature = WebhookSignature::hmac_sha256("Header", "SECRET_ENV")` - Signature validation
323/// - `idempotency = "header:X-Request-Id"` - Idempotency key source
324/// - `timeout = "30s"` - Request timeout
325///
326/// # Signature Validation
327/// Use `WebhookSignature` helper for common patterns:
328/// - `WebhookSignature::hmac_sha256("X-Hub-Signature-256", "GITHUB_SECRET")` - GitHub
329/// - `WebhookSignature::hmac_sha256("X-Stripe-Signature", "STRIPE_SECRET")` - Stripe
330/// - `WebhookSignature::hmac_sha1("X-Signature", "SECRET")` - Legacy SHA1
331///
332/// # Idempotency
333/// Specify source as `"header:Header-Name"` or `"body:$.json.path"`:
334/// - `"header:X-GitHub-Delivery"` - From header
335/// - `"body:$.id"` - From JSON body field
336///
337/// # Example
338/// ```ignore
339/// #[forge::webhook(
340/// path = "/webhooks/github",
341/// signature = WebhookSignature::hmac_sha256("X-Hub-Signature-256", "GITHUB_SECRET"),
342/// idempotency = "header:X-GitHub-Delivery",
343/// )]
344/// pub async fn github_webhook(ctx: &WebhookContext, payload: Value) -> Result<WebhookResult> {
345/// let event_type = ctx.header("X-GitHub-Event").unwrap_or("unknown");
346/// ctx.dispatch_job("process_github_event", &payload).await?;
347/// Ok(WebhookResult::Accepted)
348/// }
349///
350/// #[forge::webhook(path = "/webhooks/stripe", timeout = "60s")]
351/// pub async fn stripe_webhook(ctx: &WebhookContext, payload: Value) -> Result<WebhookResult> {
352/// // Process Stripe event
353/// Ok(WebhookResult::Ok)
354/// }
355/// ```
356#[proc_macro_attribute]
357pub fn webhook(attr: TokenStream, item: TokenStream) -> TokenStream {
358 webhook::webhook_impl(attr, item)
359}