forge_core/function/
context.rs

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