Skip to main content

forge_core/schema/
function.rs

1//! Function definitions for FORGE schema.
2//!
3//! This module defines the structure for queries, mutations, and actions
4//! that can be registered in the schema registry.
5
6use super::types::RustType;
7
8/// Function kind (query, mutation, job, cron, workflow).
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FunctionKind {
11    /// Read-only database query.
12    Query,
13    /// Write operation that modifies database state (may include HTTP calls).
14    Mutation,
15    /// Background job with retry logic.
16    Job,
17    /// Scheduled cron task.
18    Cron,
19    /// Multi-step durable workflow.
20    Workflow,
21}
22
23impl FunctionKind {
24    /// Get the string representation.
25    pub fn as_str(&self) -> &'static str {
26        match self {
27            FunctionKind::Query => "query",
28            FunctionKind::Mutation => "mutation",
29            FunctionKind::Job => "job",
30            FunctionKind::Cron => "cron",
31            FunctionKind::Workflow => "workflow",
32        }
33    }
34
35    /// Check if this function kind is callable from the frontend.
36    pub fn is_client_callable(&self) -> bool {
37        matches!(self, FunctionKind::Query | FunctionKind::Mutation)
38    }
39}
40
41impl std::fmt::Display for FunctionKind {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "{}", self.as_str())
44    }
45}
46
47/// Function argument definition.
48#[derive(Debug, Clone)]
49pub struct FunctionArg {
50    /// Argument name (snake_case in Rust).
51    pub name: String,
52    /// Argument type.
53    pub rust_type: RustType,
54    /// Documentation comment.
55    pub doc: Option<String>,
56}
57
58impl FunctionArg {
59    /// Create a new function argument.
60    pub fn new(name: impl Into<String>, rust_type: RustType) -> Self {
61        Self {
62            name: name.into(),
63            rust_type,
64            doc: None,
65        }
66    }
67}
68
69/// Function definition.
70#[derive(Debug, Clone)]
71pub struct FunctionDef {
72    /// Function name (snake_case in Rust).
73    pub name: String,
74    /// Function kind.
75    pub kind: FunctionKind,
76    /// Input arguments.
77    pub args: Vec<FunctionArg>,
78    /// Return type.
79    pub return_type: RustType,
80    /// Documentation comment.
81    pub doc: Option<String>,
82    /// Whether the function is async.
83    pub is_async: bool,
84}
85
86impl FunctionDef {
87    /// Create a new function definition.
88    pub fn new(name: impl Into<String>, kind: FunctionKind, return_type: RustType) -> Self {
89        Self {
90            name: name.into(),
91            kind,
92            args: Vec::new(),
93            return_type,
94            doc: None,
95            is_async: true,
96        }
97    }
98
99    /// Create a query function.
100    pub fn query(name: impl Into<String>, return_type: RustType) -> Self {
101        Self::new(name, FunctionKind::Query, return_type)
102    }
103
104    /// Create a mutation function.
105    pub fn mutation(name: impl Into<String>, return_type: RustType) -> Self {
106        Self::new(name, FunctionKind::Mutation, return_type)
107    }
108
109    /// Add an argument.
110    pub fn with_arg(mut self, arg: FunctionArg) -> Self {
111        self.args.push(arg);
112        self
113    }
114
115    /// Set documentation.
116    pub fn with_doc(mut self, doc: impl Into<String>) -> Self {
117        self.doc = Some(doc.into());
118        self
119    }
120
121    /// Get the camelCase name for TypeScript.
122    pub fn ts_name(&self) -> String {
123        to_camel_case(&self.name)
124    }
125}
126
127/// Convert snake_case to camelCase.
128fn to_camel_case(s: &str) -> String {
129    let mut result = String::new();
130    let mut capitalize_next = false;
131
132    for c in s.chars() {
133        if c == '_' {
134            capitalize_next = true;
135        } else if capitalize_next {
136            result.push(c.to_uppercase().next().unwrap_or(c));
137            capitalize_next = false;
138        } else {
139            result.push(c);
140        }
141    }
142
143    result
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_function_def_query() {
152        let func = FunctionDef::query("get_user", RustType::Custom("User".to_string()))
153            .with_arg(FunctionArg::new("id", RustType::Uuid))
154            .with_doc("Get a user by ID");
155
156        assert_eq!(func.name, "get_user");
157        assert_eq!(func.kind, FunctionKind::Query);
158        assert_eq!(func.args.len(), 1);
159        assert_eq!(func.ts_name(), "getUser");
160    }
161
162    #[test]
163    fn test_function_def_mutation() {
164        let func = FunctionDef::mutation("create_user", RustType::Custom("User".to_string()));
165        assert_eq!(func.kind, FunctionKind::Mutation);
166    }
167
168    #[test]
169    fn test_to_camel_case() {
170        assert_eq!(to_camel_case("get_user"), "getUser");
171        assert_eq!(to_camel_case("create_project_task"), "createProjectTask");
172        assert_eq!(to_camel_case("getUser"), "getUser");
173    }
174}