forge_core/function/
traits.rs

1use std::future::Future;
2use std::pin::Pin;
3
4use serde::{Serialize, de::DeserializeOwned};
5
6use super::context::{MutationContext, QueryContext};
7use crate::error::Result;
8
9/// Information about a registered function.
10#[derive(Debug, Clone)]
11pub struct FunctionInfo {
12    /// Function name (used for routing).
13    pub name: &'static str,
14    /// Human-readable description.
15    pub description: Option<&'static str>,
16    /// Kind of function.
17    pub kind: FunctionKind,
18    /// Whether authentication is required.
19    pub requires_auth: bool,
20    /// Required role (if any).
21    pub required_role: Option<&'static str>,
22    /// Whether this function is public (no auth).
23    pub is_public: bool,
24    /// Cache TTL in seconds (for queries).
25    pub cache_ttl: Option<u64>,
26    /// Timeout in seconds.
27    pub timeout: Option<u64>,
28    /// Rate limit: requests per time window.
29    pub rate_limit_requests: Option<u32>,
30    /// Rate limit: time window in seconds.
31    pub rate_limit_per_secs: Option<u64>,
32    /// Rate limit: bucket key type (user, ip, tenant, global).
33    pub rate_limit_key: Option<&'static str>,
34    /// Log level for access logging: "trace", "debug", "info", "warn", "error", "off".
35    /// Defaults to "trace" if not specified.
36    pub log_level: Option<&'static str>,
37    /// Table dependencies extracted at compile time for reactive subscriptions.
38    /// Empty slice means tables could not be determined (dynamic SQL).
39    pub table_dependencies: &'static [&'static str],
40    /// Whether this mutation should be wrapped in a database transaction.
41    /// Only applies to mutations. When true, jobs are buffered and inserted
42    /// atomically with the mutation via the outbox pattern.
43    pub transactional: bool,
44}
45
46/// The kind of function.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum FunctionKind {
49    Query,
50    Mutation,
51}
52
53impl std::fmt::Display for FunctionKind {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            FunctionKind::Query => write!(f, "query"),
57            FunctionKind::Mutation => write!(f, "mutation"),
58        }
59    }
60}
61
62/// A query function (read-only, cacheable, subscribable).
63///
64/// Queries:
65/// - Can only read from the database
66/// - Are automatically cached based on arguments
67/// - Can be subscribed to for real-time updates
68/// - Should be deterministic (same inputs → same outputs)
69/// - Should not have side effects
70pub trait ForgeQuery: Send + Sync + 'static {
71    /// The input arguments type.
72    type Args: DeserializeOwned + Serialize + Send + Sync;
73    /// The output type.
74    type Output: Serialize + Send;
75
76    /// Function metadata.
77    fn info() -> FunctionInfo;
78
79    /// Execute the query.
80    fn execute(
81        ctx: &QueryContext,
82        args: Self::Args,
83    ) -> Pin<Box<dyn Future<Output = Result<Self::Output>> + Send + '_>>;
84}
85
86/// A mutation function (transactional write).
87///
88/// Mutations:
89/// - Run within a database transaction
90/// - Can read and write to the database
91/// - Should NOT call external APIs (use Actions)
92/// - Are atomic: all changes commit or none do
93pub trait ForgeMutation: Send + Sync + 'static {
94    /// The input arguments type.
95    type Args: DeserializeOwned + Serialize + Send + Sync;
96    /// The output type.
97    type Output: Serialize + Send;
98
99    /// Function metadata.
100    fn info() -> FunctionInfo;
101
102    /// Execute the mutation within a transaction.
103    fn execute(
104        ctx: &MutationContext,
105        args: Self::Args,
106    ) -> Pin<Box<dyn Future<Output = Result<Self::Output>> + Send + '_>>;
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_function_kind_display() {
115        assert_eq!(format!("{}", FunctionKind::Query), "query");
116        assert_eq!(format!("{}", FunctionKind::Mutation), "mutation");
117    }
118
119    #[test]
120    fn test_function_info() {
121        let info = FunctionInfo {
122            name: "get_user",
123            description: Some("Get a user by ID"),
124            kind: FunctionKind::Query,
125            requires_auth: true,
126            required_role: None,
127            is_public: false,
128            cache_ttl: Some(300),
129            timeout: Some(30),
130            rate_limit_requests: Some(100),
131            rate_limit_per_secs: Some(60),
132            rate_limit_key: Some("user"),
133            log_level: Some("debug"),
134            table_dependencies: &["users"],
135            transactional: false,
136        };
137
138        assert_eq!(info.name, "get_user");
139        assert_eq!(info.kind, FunctionKind::Query);
140        assert!(info.requires_auth);
141        assert_eq!(info.cache_ttl, Some(300));
142        assert_eq!(info.rate_limit_requests, Some(100));
143        assert_eq!(info.log_level, Some("debug"));
144        assert_eq!(info.table_dependencies, &["users"]);
145    }
146}