Skip to main content

forge_core/function/
traits.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::time::Duration;
4
5use serde::{Serialize, de::DeserializeOwned};
6
7use super::context::{MutationContext, QueryContext};
8use crate::error::Result;
9
10/// Metadata for a registered query or mutation function.
11#[derive(Debug, Clone)]
12pub struct FunctionInfo {
13    pub name: &'static str,
14    pub description: Option<&'static str>,
15    pub kind: FunctionKind,
16    pub required_role: Option<&'static str>,
17    pub is_public: bool,
18    pub cache_ttl: Option<u64>,
19    /// `None` falls back to the runtime default.
20    pub timeout: Option<Duration>,
21    /// Default timeout for outbound HTTP requests via the circuit-breaker client.
22    pub http_timeout: Option<Duration>,
23    pub rate_limit_requests: Option<u32>,
24    pub rate_limit_per_secs: Option<u64>,
25    pub rate_limit_key: Option<crate::rate_limit::RateLimitKey>,
26    pub log_level: Option<LogLevel>,
27    /// Compile-time extracted tables for reactive subscriptions.
28    pub table_dependencies: &'static [&'static str],
29    /// Columns in SELECT clauses for fine-grained invalidation.
30    pub selected_columns: &'static [&'static str],
31    /// Columns written by INSERT/UPDATE for cache invalidation scoping.
32    pub changed_columns: &'static [&'static str],
33    /// Whether this mutation runs inside a database transaction.
34    pub transactional: bool,
35    /// Force reads from primary instead of replicas.
36    pub consistent: bool,
37    pub max_upload_size_bytes: Option<usize>,
38    /// When true, runtime rejects dispatch if auth context has no tenant claim.
39    pub requires_tenant_scope: bool,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[non_exhaustive]
44pub enum FunctionKind {
45    Query,
46    Mutation,
47    Webhook,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[non_exhaustive]
52pub enum LogLevel {
53    Trace,
54    Debug,
55    Info,
56    Warn,
57    Error,
58    Off,
59}
60
61impl LogLevel {
62    pub fn as_str(&self) -> &'static str {
63        match self {
64            Self::Trace => "trace",
65            Self::Debug => "debug",
66            Self::Info => "info",
67            Self::Warn => "warn",
68            Self::Error => "error",
69            Self::Off => "off",
70        }
71    }
72}
73
74impl FunctionKind {
75    pub fn as_str(&self) -> &'static str {
76        match self {
77            Self::Query => "query",
78            Self::Mutation => "mutation",
79            Self::Webhook => "webhook",
80        }
81    }
82}
83
84impl std::fmt::Display for FunctionKind {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        f.write_str(self.as_str())
87    }
88}
89
90/// A read-only, cacheable, subscribable query function.
91pub trait ForgeQuery: crate::__sealed::Sealed + Send + Sync + 'static {
92    type Args: DeserializeOwned + Serialize + Send + Sync;
93    type Output: Serialize + Send;
94
95    fn info() -> FunctionInfo;
96
97    fn execute(
98        ctx: &QueryContext,
99        args: Self::Args,
100    ) -> Pin<Box<dyn Future<Output = Result<Self::Output>> + Send + '_>>;
101}
102
103/// A transactional write function.
104pub trait ForgeMutation: crate::__sealed::Sealed + Send + Sync + 'static {
105    type Args: DeserializeOwned + Serialize + Send + Sync;
106    type Output: Serialize + Send;
107
108    fn info() -> FunctionInfo;
109
110    fn execute(
111        ctx: &MutationContext,
112        args: Self::Args,
113    ) -> Pin<Box<dyn Future<Output = Result<Self::Output>> + Send + '_>>;
114}
115
116#[cfg(test)]
117#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_function_kind_display() {
123        assert_eq!(format!("{}", FunctionKind::Query), "query");
124        assert_eq!(format!("{}", FunctionKind::Mutation), "mutation");
125        assert_eq!(format!("{}", FunctionKind::Webhook), "webhook");
126    }
127
128    #[test]
129    fn test_function_info() {
130        let info = FunctionInfo {
131            name: "get_user",
132            description: Some("Get a user by ID"),
133            kind: FunctionKind::Query,
134            required_role: None,
135            is_public: false,
136            cache_ttl: Some(300),
137            timeout: Some(Duration::from_secs(30)),
138            http_timeout: Some(Duration::from_secs(5)),
139            rate_limit_requests: Some(100),
140            rate_limit_per_secs: Some(60),
141            rate_limit_key: Some(crate::rate_limit::RateLimitKey::User),
142            log_level: Some(LogLevel::Debug),
143            table_dependencies: &["users"],
144            selected_columns: &["id", "name", "email"],
145            changed_columns: &[],
146            transactional: false,
147            consistent: false,
148            max_upload_size_bytes: None,
149            requires_tenant_scope: false,
150        };
151
152        assert_eq!(info.name, "get_user");
153        assert_eq!(info.kind, FunctionKind::Query);
154        assert_eq!(info.cache_ttl, Some(300));
155        assert_eq!(info.http_timeout, Some(Duration::from_secs(5)));
156        assert_eq!(info.rate_limit_requests, Some(100));
157        assert_eq!(info.log_level, Some(LogLevel::Debug));
158        assert_eq!(info.table_dependencies, &["users"]);
159    }
160}