forge_core/function/
context.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use uuid::Uuid;
5
6use super::dispatch::{JobDispatch, WorkflowDispatch};
7use crate::env::{EnvAccess, EnvProvider, RealEnvProvider};
8
9/// Authentication context available to all functions.
10#[derive(Debug, Clone)]
11pub struct AuthContext {
12    /// The authenticated user ID (if any).
13    user_id: Option<Uuid>,
14    /// User roles.
15    roles: Vec<String>,
16    /// Custom claims from JWT.
17    claims: HashMap<String, serde_json::Value>,
18    /// Whether the request is authenticated.
19    authenticated: bool,
20}
21
22impl AuthContext {
23    /// Create an unauthenticated context.
24    pub fn unauthenticated() -> Self {
25        Self {
26            user_id: None,
27            roles: Vec::new(),
28            claims: HashMap::new(),
29            authenticated: false,
30        }
31    }
32
33    /// Create an authenticated context.
34    pub fn authenticated(
35        user_id: Uuid,
36        roles: Vec<String>,
37        claims: HashMap<String, serde_json::Value>,
38    ) -> Self {
39        Self {
40            user_id: Some(user_id),
41            roles,
42            claims,
43            authenticated: true,
44        }
45    }
46
47    /// Check if the user is authenticated.
48    pub fn is_authenticated(&self) -> bool {
49        self.authenticated
50    }
51
52    /// Get the user ID if authenticated.
53    pub fn user_id(&self) -> Option<Uuid> {
54        self.user_id
55    }
56
57    /// Get the user ID, returning an error if not authenticated.
58    pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
59        self.user_id
60            .ok_or_else(|| crate::error::ForgeError::Unauthorized("Authentication required".into()))
61    }
62
63    /// Check if the user has a specific role.
64    pub fn has_role(&self, role: &str) -> bool {
65        self.roles.iter().any(|r| r == role)
66    }
67
68    /// Require a specific role, returning an error if not present.
69    pub fn require_role(&self, role: &str) -> crate::error::Result<()> {
70        if self.has_role(role) {
71            Ok(())
72        } else {
73            Err(crate::error::ForgeError::Forbidden(format!(
74                "Required role '{}' not present",
75                role
76            )))
77        }
78    }
79
80    /// Get a custom claim value.
81    pub fn claim(&self, key: &str) -> Option<&serde_json::Value> {
82        self.claims.get(key)
83    }
84
85    /// Get all roles.
86    pub fn roles(&self) -> &[String] {
87        &self.roles
88    }
89}
90
91/// Request metadata available to all functions.
92#[derive(Debug, Clone)]
93pub struct RequestMetadata {
94    /// Unique request ID for tracing.
95    pub request_id: Uuid,
96    /// Trace ID for distributed tracing.
97    pub trace_id: String,
98    /// Client IP address.
99    pub client_ip: Option<String>,
100    /// User agent string.
101    pub user_agent: Option<String>,
102    /// Request timestamp.
103    pub timestamp: chrono::DateTime<chrono::Utc>,
104}
105
106impl RequestMetadata {
107    /// Create new request metadata.
108    pub fn new() -> Self {
109        Self {
110            request_id: Uuid::new_v4(),
111            trace_id: Uuid::new_v4().to_string(),
112            client_ip: None,
113            user_agent: None,
114            timestamp: chrono::Utc::now(),
115        }
116    }
117
118    /// Create with a specific trace ID.
119    pub fn with_trace_id(trace_id: String) -> Self {
120        Self {
121            request_id: Uuid::new_v4(),
122            trace_id,
123            client_ip: None,
124            user_agent: None,
125            timestamp: chrono::Utc::now(),
126        }
127    }
128}
129
130impl Default for RequestMetadata {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136/// Context for query functions (read-only database access).
137pub struct QueryContext {
138    /// Authentication context.
139    pub auth: AuthContext,
140    /// Request metadata.
141    pub request: RequestMetadata,
142    /// Database pool for read operations.
143    db_pool: sqlx::PgPool,
144    /// Environment variable provider.
145    env_provider: Arc<dyn EnvProvider>,
146}
147
148impl QueryContext {
149    /// Create a new query context.
150    pub fn new(db_pool: sqlx::PgPool, auth: AuthContext, request: RequestMetadata) -> Self {
151        Self {
152            auth,
153            request,
154            db_pool,
155            env_provider: Arc::new(RealEnvProvider::new()),
156        }
157    }
158
159    /// Create a query context with a custom environment provider.
160    pub fn with_env(
161        db_pool: sqlx::PgPool,
162        auth: AuthContext,
163        request: RequestMetadata,
164        env_provider: Arc<dyn EnvProvider>,
165    ) -> Self {
166        Self {
167            auth,
168            request,
169            db_pool,
170            env_provider,
171        }
172    }
173
174    /// Get a reference to the database pool.
175    pub fn db(&self) -> &sqlx::PgPool {
176        &self.db_pool
177    }
178
179    /// Get the authenticated user ID or return an error.
180    pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
181        self.auth.require_user_id()
182    }
183}
184
185impl EnvAccess for QueryContext {
186    fn env_provider(&self) -> &dyn EnvProvider {
187        self.env_provider.as_ref()
188    }
189}
190
191/// Context for mutation functions (transactional database access).
192pub struct MutationContext {
193    /// Authentication context.
194    pub auth: AuthContext,
195    /// Request metadata.
196    pub request: RequestMetadata,
197    /// Database pool for transactional operations.
198    db_pool: sqlx::PgPool,
199    /// Optional job dispatcher for dispatching background jobs.
200    job_dispatch: Option<Arc<dyn JobDispatch>>,
201    /// Optional workflow dispatcher for starting workflows.
202    workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
203    /// Environment variable provider.
204    env_provider: Arc<dyn EnvProvider>,
205}
206
207impl MutationContext {
208    /// Create a new mutation context.
209    pub fn new(db_pool: sqlx::PgPool, auth: AuthContext, request: RequestMetadata) -> Self {
210        Self {
211            auth,
212            request,
213            db_pool,
214            job_dispatch: None,
215            workflow_dispatch: None,
216            env_provider: Arc::new(RealEnvProvider::new()),
217        }
218    }
219
220    /// Create a mutation context with dispatch capabilities.
221    pub fn with_dispatch(
222        db_pool: sqlx::PgPool,
223        auth: AuthContext,
224        request: RequestMetadata,
225        job_dispatch: Option<Arc<dyn JobDispatch>>,
226        workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
227    ) -> Self {
228        Self {
229            auth,
230            request,
231            db_pool,
232            job_dispatch,
233            workflow_dispatch,
234            env_provider: Arc::new(RealEnvProvider::new()),
235        }
236    }
237
238    /// Create a mutation context with a custom environment provider.
239    pub fn with_env(
240        db_pool: sqlx::PgPool,
241        auth: AuthContext,
242        request: RequestMetadata,
243        job_dispatch: Option<Arc<dyn JobDispatch>>,
244        workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
245        env_provider: Arc<dyn EnvProvider>,
246    ) -> Self {
247        Self {
248            auth,
249            request,
250            db_pool,
251            job_dispatch,
252            workflow_dispatch,
253            env_provider,
254        }
255    }
256
257    /// Get a reference to the database pool.
258    pub fn db(&self) -> &sqlx::PgPool {
259        &self.db_pool
260    }
261
262    /// Get the authenticated user ID or return an error.
263    pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
264        self.auth.require_user_id()
265    }
266
267    /// Dispatch a background job.
268    ///
269    /// # Arguments
270    /// * `job_type` - The registered name of the job type
271    /// * `args` - The arguments for the job (will be serialized to JSON)
272    ///
273    /// # Returns
274    /// The UUID of the dispatched job, or an error if dispatch is not available.
275    pub async fn dispatch_job<T: serde::Serialize>(
276        &self,
277        job_type: &str,
278        args: T,
279    ) -> crate::error::Result<Uuid> {
280        let dispatcher = self.job_dispatch.as_ref().ok_or_else(|| {
281            crate::error::ForgeError::Internal("Job dispatch not available".into())
282        })?;
283        let args_json = serde_json::to_value(args)?;
284        dispatcher.dispatch_by_name(job_type, args_json).await
285    }
286
287    /// Start a workflow.
288    ///
289    /// # Arguments
290    /// * `workflow_name` - The registered name of the workflow
291    /// * `input` - The input for the workflow (will be serialized to JSON)
292    ///
293    /// # Returns
294    /// The UUID of the started workflow run, or an error if dispatch is not available.
295    pub async fn start_workflow<T: serde::Serialize>(
296        &self,
297        workflow_name: &str,
298        input: T,
299    ) -> crate::error::Result<Uuid> {
300        let dispatcher = self.workflow_dispatch.as_ref().ok_or_else(|| {
301            crate::error::ForgeError::Internal("Workflow dispatch not available".into())
302        })?;
303        let input_json = serde_json::to_value(input)?;
304        dispatcher.start_by_name(workflow_name, input_json).await
305    }
306}
307
308impl EnvAccess for MutationContext {
309    fn env_provider(&self) -> &dyn EnvProvider {
310        self.env_provider.as_ref()
311    }
312}
313
314/// Context for action functions (can call external APIs).
315pub struct ActionContext {
316    /// Authentication context.
317    pub auth: AuthContext,
318    /// Request metadata.
319    pub request: RequestMetadata,
320    /// Database pool for database operations.
321    db_pool: sqlx::PgPool,
322    /// HTTP client for external requests.
323    http_client: reqwest::Client,
324    /// Optional job dispatcher for dispatching background jobs.
325    job_dispatch: Option<Arc<dyn JobDispatch>>,
326    /// Optional workflow dispatcher for starting workflows.
327    workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
328    /// Environment variable provider.
329    env_provider: Arc<dyn EnvProvider>,
330}
331
332impl ActionContext {
333    /// Create a new action context.
334    pub fn new(
335        db_pool: sqlx::PgPool,
336        auth: AuthContext,
337        request: RequestMetadata,
338        http_client: reqwest::Client,
339    ) -> Self {
340        Self {
341            auth,
342            request,
343            db_pool,
344            http_client,
345            job_dispatch: None,
346            workflow_dispatch: None,
347            env_provider: Arc::new(RealEnvProvider::new()),
348        }
349    }
350
351    /// Create an action context with dispatch capabilities.
352    pub fn with_dispatch(
353        db_pool: sqlx::PgPool,
354        auth: AuthContext,
355        request: RequestMetadata,
356        http_client: reqwest::Client,
357        job_dispatch: Option<Arc<dyn JobDispatch>>,
358        workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
359    ) -> Self {
360        Self {
361            auth,
362            request,
363            db_pool,
364            http_client,
365            job_dispatch,
366            workflow_dispatch,
367            env_provider: Arc::new(RealEnvProvider::new()),
368        }
369    }
370
371    /// Create an action context with a custom environment provider.
372    pub fn with_env(
373        db_pool: sqlx::PgPool,
374        auth: AuthContext,
375        request: RequestMetadata,
376        http_client: reqwest::Client,
377        job_dispatch: Option<Arc<dyn JobDispatch>>,
378        workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
379        env_provider: Arc<dyn EnvProvider>,
380    ) -> Self {
381        Self {
382            auth,
383            request,
384            db_pool,
385            http_client,
386            job_dispatch,
387            workflow_dispatch,
388            env_provider,
389        }
390    }
391
392    /// Get a reference to the database pool.
393    pub fn db(&self) -> &sqlx::PgPool {
394        &self.db_pool
395    }
396
397    /// Get a reference to the HTTP client.
398    pub fn http(&self) -> &reqwest::Client {
399        &self.http_client
400    }
401
402    /// Get the authenticated user ID or return an error.
403    pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
404        self.auth.require_user_id()
405    }
406
407    /// Dispatch a background job.
408    ///
409    /// # Arguments
410    /// * `job_type` - The registered name of the job type
411    /// * `args` - The arguments for the job (will be serialized to JSON)
412    ///
413    /// # Returns
414    /// The UUID of the dispatched job, or an error if dispatch is not available.
415    pub async fn dispatch_job<T: serde::Serialize>(
416        &self,
417        job_type: &str,
418        args: T,
419    ) -> crate::error::Result<Uuid> {
420        let dispatcher = self.job_dispatch.as_ref().ok_or_else(|| {
421            crate::error::ForgeError::Internal("Job dispatch not available".into())
422        })?;
423        let args_json = serde_json::to_value(args)?;
424        dispatcher.dispatch_by_name(job_type, args_json).await
425    }
426
427    /// Start a workflow.
428    ///
429    /// # Arguments
430    /// * `workflow_name` - The registered name of the workflow
431    /// * `input` - The input for the workflow (will be serialized to JSON)
432    ///
433    /// # Returns
434    /// The UUID of the started workflow run, or an error if dispatch is not available.
435    pub async fn start_workflow<T: serde::Serialize>(
436        &self,
437        workflow_name: &str,
438        input: T,
439    ) -> crate::error::Result<Uuid> {
440        let dispatcher = self.workflow_dispatch.as_ref().ok_or_else(|| {
441            crate::error::ForgeError::Internal("Workflow dispatch not available".into())
442        })?;
443        let input_json = serde_json::to_value(input)?;
444        dispatcher.start_by_name(workflow_name, input_json).await
445    }
446}
447
448impl EnvAccess for ActionContext {
449    fn env_provider(&self) -> &dyn EnvProvider {
450        self.env_provider.as_ref()
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn test_auth_context_unauthenticated() {
460        let ctx = AuthContext::unauthenticated();
461        assert!(!ctx.is_authenticated());
462        assert!(ctx.user_id().is_none());
463        assert!(ctx.require_user_id().is_err());
464    }
465
466    #[test]
467    fn test_auth_context_authenticated() {
468        let user_id = Uuid::new_v4();
469        let ctx = AuthContext::authenticated(
470            user_id,
471            vec!["admin".to_string(), "user".to_string()],
472            HashMap::new(),
473        );
474
475        assert!(ctx.is_authenticated());
476        assert_eq!(ctx.user_id(), Some(user_id));
477        assert!(ctx.require_user_id().is_ok());
478        assert!(ctx.has_role("admin"));
479        assert!(ctx.has_role("user"));
480        assert!(!ctx.has_role("superadmin"));
481        assert!(ctx.require_role("admin").is_ok());
482        assert!(ctx.require_role("superadmin").is_err());
483    }
484
485    #[test]
486    fn test_auth_context_with_claims() {
487        let mut claims = HashMap::new();
488        claims.insert("org_id".to_string(), serde_json::json!("org-123"));
489
490        let ctx = AuthContext::authenticated(Uuid::new_v4(), vec![], claims);
491
492        assert_eq!(ctx.claim("org_id"), Some(&serde_json::json!("org-123")));
493        assert!(ctx.claim("nonexistent").is_none());
494    }
495
496    #[test]
497    fn test_request_metadata() {
498        let meta = RequestMetadata::new();
499        assert!(!meta.trace_id.is_empty());
500        assert!(meta.client_ip.is_none());
501
502        let meta2 = RequestMetadata::with_trace_id("trace-123".to_string());
503        assert_eq!(meta2.trace_id, "trace-123");
504    }
505}