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