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
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}