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