use std::path::PathBuf;
use forge_core::schema::{FunctionKind, RustType, SchemaRegistry};
use super::Error;
pub struct ApiGenerator {
#[allow(dead_code)]
output_dir: PathBuf,
}
impl ApiGenerator {
pub fn new(output_dir: impl Into<PathBuf>) -> Self {
Self {
output_dir: output_dir.into(),
}
}
pub fn generate(&self, registry: &SchemaRegistry) -> Result<String, Error> {
let mut output = String::new();
output.push_str(self.generate_header());
output.push_str(self.generate_helpers());
let functions = registry.all_functions();
if !functions.is_empty() {
output.push_str("\n// ============================================================================\n");
output.push_str("// Generated API Bindings\n");
output.push_str("// ============================================================================\n\n");
for func in &functions {
if let Some(doc) = &func.doc {
output.push_str("/**\n");
for line in doc.lines() {
output.push_str(" * ");
output.push_str(line);
output.push('\n');
}
output.push_str(" */\n");
}
if !func.kind.is_client_callable() {
continue;
}
let ts_name = to_camel_case(&func.name);
let creator = match func.kind {
FunctionKind::Query => "createQuery",
FunctionKind::Mutation => "createMutation",
FunctionKind::Action => "createAction",
FunctionKind::Job | FunctionKind::Cron | FunctionKind::Workflow => continue,
};
let args_type = if func.args.is_empty() {
"Record<string, never>".to_string()
} else {
let fields: Vec<String> = func
.args
.iter()
.map(|arg| {
let ts_type = rust_type_to_ts(&arg.rust_type);
format!("{}: {}", to_camel_case(&arg.name), ts_type)
})
.collect();
format!("{{ {} }}", fields.join("; "))
};
let result_type = rust_type_to_ts(&func.return_type);
output.push_str(&format!(
"export const {} = {}<{}, {}>('{}');\n\n",
ts_name, creator, args_type, result_type, func.name
));
}
}
Ok(output)
}
fn generate_header(&self) -> &'static str {
r#"// Auto-generated by FORGE - DO NOT EDIT
import type { ForgeClient } from './client';
"#
}
fn generate_helpers(&self) -> &'static str {
r#"// ============================================================================
// API Function Types
// ============================================================================
// Query function type
export interface QueryFn<TArgs, TResult> {
(client: ForgeClient, args: TArgs): Promise<TResult>;
functionName: string;
functionType: 'query';
}
// Mutation function type
export interface MutationFn<TArgs, TResult> {
(client: ForgeClient, args: TArgs): Promise<TResult>;
functionName: string;
functionType: 'mutation';
}
// Action function type
export interface ActionFn<TArgs, TResult> {
(client: ForgeClient, args: TArgs): Promise<TResult>;
functionName: string;
functionType: 'action';
}
// Create a query function binding
export function createQuery<TArgs, TResult>(
name: string
): QueryFn<TArgs, TResult> {
const fn = async (client: ForgeClient, args: TArgs): Promise<TResult> => {
return client.call(name, args);
};
(fn as QueryFn<TArgs, TResult>).functionName = name;
(fn as QueryFn<TArgs, TResult>).functionType = 'query';
return fn as QueryFn<TArgs, TResult>;
}
// Create a mutation function binding
export function createMutation<TArgs, TResult>(
name: string
): MutationFn<TArgs, TResult> {
const fn = async (client: ForgeClient, args: TArgs): Promise<TResult> => {
return client.call(name, args);
};
(fn as MutationFn<TArgs, TResult>).functionName = name;
(fn as MutationFn<TArgs, TResult>).functionType = 'mutation';
return fn as MutationFn<TArgs, TResult>;
}
// Create an action function binding
export function createAction<TArgs, TResult>(
name: string
): ActionFn<TArgs, TResult> {
const fn = async (client: ForgeClient, args: TArgs): Promise<TResult> => {
return client.call(name, args);
};
(fn as ActionFn<TArgs, TResult>).functionName = name;
(fn as ActionFn<TArgs, TResult>).functionType = 'action';
return fn as ActionFn<TArgs, TResult>;
}
"#
}
}
fn rust_type_to_ts(rust_type: &RustType) -> String {
match rust_type {
RustType::String => "string".to_string(),
RustType::I32 | RustType::I64 | RustType::F32 | RustType::F64 => "number".to_string(),
RustType::Bool => "boolean".to_string(),
RustType::Uuid => "string".to_string(),
RustType::DateTime => "string".to_string(),
RustType::Date => "string".to_string(),
RustType::Json => "unknown".to_string(),
RustType::Bytes => "Uint8Array".to_string(),
RustType::Option(inner) => format!("{} | null", rust_type_to_ts(inner)),
RustType::Vec(inner) => format!("{}[]", rust_type_to_ts(inner)),
RustType::Custom(name) => {
if name == "()" {
return "void".to_string();
}
if let Some(inner) = name.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
return format!("{}[]", inner);
}
name.clone()
}
}
}
fn to_camel_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = false;
for c in s.chars() {
if c == '_' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_uppercase().next().unwrap_or(c));
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use forge_core::schema::{FunctionArg, FunctionDef};
#[test]
fn test_api_generator_creation() {
let gen = ApiGenerator::new("/tmp/forge");
assert_eq!(gen.output_dir, PathBuf::from("/tmp/forge"));
}
#[test]
fn test_generate_empty_registry() {
let gen = ApiGenerator::new("/tmp/forge");
let registry = SchemaRegistry::new();
let content = gen.generate(®istry).unwrap();
assert!(content.contains("QueryFn"));
assert!(content.contains("MutationFn"));
assert!(content.contains("ActionFn"));
assert!(content.contains("createQuery"));
assert!(content.contains("createMutation"));
assert!(content.contains("createAction"));
}
#[test]
fn test_generate_with_functions() {
let gen = ApiGenerator::new("/tmp/forge");
let registry = SchemaRegistry::new();
let mut func = FunctionDef::query("get_user", RustType::Custom("User".to_string()));
func.args.push(FunctionArg::new("id", RustType::Uuid));
registry.register_function(func);
let mut func = FunctionDef::mutation("create_user", RustType::Custom("User".to_string()));
func.args.push(FunctionArg::new("name", RustType::String));
func.args.push(FunctionArg::new("email", RustType::String));
registry.register_function(func);
let content = gen.generate(®istry).unwrap();
assert!(content.contains("export const getUser = createQuery"));
assert!(content.contains("export const createUser = createMutation"));
assert!(content.contains("id: string"));
assert!(content.contains("name: string"));
assert!(content.contains("email: string"));
}
#[test]
fn test_rust_type_to_ts() {
assert_eq!(rust_type_to_ts(&RustType::String), "string");
assert_eq!(rust_type_to_ts(&RustType::I32), "number");
assert_eq!(rust_type_to_ts(&RustType::Bool), "boolean");
assert_eq!(rust_type_to_ts(&RustType::Uuid), "string");
assert_eq!(
rust_type_to_ts(&RustType::Option(Box::new(RustType::String))),
"string | null"
);
assert_eq!(
rust_type_to_ts(&RustType::Vec(Box::new(RustType::I32))),
"number[]"
);
assert_eq!(rust_type_to_ts(&RustType::Custom("()".to_string())), "void");
}
#[test]
fn test_to_camel_case() {
assert_eq!(to_camel_case("get_user"), "getUser");
assert_eq!(to_camel_case("create_project_task"), "createProjectTask");
assert_eq!(to_camel_case("getUser"), "getUser");
}
}