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