use std::collections::HashSet;
use std::io::{Seek, SeekFrom, Write};
use std::path::PathBuf;
use serde_json::Value as JsonValue;
use crate::convex::types::{ConvexFunction, ConvexFunctions, ConvexSchema, ConvexTable};
use crate::convex::utils::{capitalize_first_letter, function_args_struct_name, to_pascal_case};
use crate::error::ConvexTypeGeneratorError;
pub(crate) fn run_codegen(path: &PathBuf, data: (ConvexSchema, ConvexFunctions)) -> Result<(), ConvexTypeGeneratorError>
{
let mut file = std::fs::File::create(path)?;
file.set_len(0)?;
file.seek(SeekFrom::Start(0))?;
let file_header = r#"// Generated by convex-typegen — do not edit by hand.
// Repo: https://github.com/JamalLyons/convex-typegen
// Regenerated on each `cargo build` when inputs change (see your `build.rs` rerun-if-changed lines).
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
use convex_typegen::prelude::*;
"#;
file.write_all(file_header.as_bytes())?;
let mut code = String::new();
for table in &data.0.tables {
code.push_str(&generate_table_enums(table));
}
for table in data.0.tables {
code.push_str(&generate_table_code(table));
}
let mut emitted_args_structs = HashSet::new();
for function in data.1 {
let struct_name = function_args_struct_name(&function.file_name, &function.name);
if !emitted_args_structs.insert(struct_name.clone()) {
return Err(ConvexTypeGeneratorError::InvalidSchema {
context: format!("function_{}:{}", function.file_name, function.name),
details: format!("Duplicate generated args struct name `{struct_name}`"),
});
}
code.push_str(&generate_function_code(function));
}
file.write_all(code.as_bytes())?;
Ok(())
}
fn generate_table_enums(table: &ConvexTable) -> String
{
let mut code = String::new();
for column in &table.columns {
if let Some("union") = column.data_type["type"].as_str() {
let enum_name = format!(
"{}{}",
capitalize_first_letter(&table.name),
capitalize_first_letter(&column.name)
);
code.push_str("#[derive(Debug, Clone)]\n");
code.push_str(&format!("pub enum {} {{\n", enum_name));
if let Some(variants) = column.data_type["variants"].as_array() {
for variant in variants {
match variant["type"].as_str() {
Some("literal") => {
if let Some(value) = variant["value"]["value"].as_str() {
code.push_str(&format!(" {},\n", to_pascal_case(value)));
}
}
Some(type_name) => {
let rust_type = convex_type_to_rust_type(variant, Some(&table.name), Some(&column.name));
code.push_str(&format!(" {}({}),\n", to_pascal_case(type_name), rust_type));
}
None => continue,
}
}
}
code.push_str("}\n\n");
}
if let Some("optional") = column.data_type["type"].as_str()
&& let Some("union") = column.data_type["inner"]["type"].as_str()
{
let enum_name = format!(
"{}Optional{}",
capitalize_first_letter(&table.name),
capitalize_first_letter(&column.name)
);
code.push_str("#[derive(Debug, Clone)]\n");
code.push_str(&format!("pub enum {} {{\n", enum_name));
if let Some(variants) = column.data_type["inner"]["variants"].as_array() {
for variant in variants {
match variant["type"].as_str() {
Some("literal") => {
if let Some(value) = variant["value"]["value"].as_str() {
code.push_str(&format!(" {},\n", to_pascal_case(value)));
}
}
Some(type_name) => {
let rust_type = convex_type_to_rust_type(variant, Some(&table.name), Some(&column.name));
code.push_str(&format!(" {}({}),\n", to_pascal_case(type_name), rust_type));
}
None => continue,
}
}
}
code.push_str("}\n\n");
}
}
code
}
fn generate_table_code(table: ConvexTable) -> String
{
let mut code = String::new();
let table_struct_name = format!("{}Table", capitalize_first_letter(&table.name));
code.push_str("#[derive(Debug, Clone)]\n");
code.push_str(&format!("pub struct {} {{\n", table_struct_name));
for column in table.columns {
let rust_type = if column.data_type["type"].as_str() == Some("union") {
format!(
"{}{}",
capitalize_first_letter(&table.name),
capitalize_first_letter(&column.name)
)
} else {
convex_type_to_rust_type(&column.data_type, Some(&table.name), Some(&column.name))
};
code.push_str(&format!(" pub {}: {},\n", column.name, rust_type));
}
code.push_str("}\n\n");
code
}
fn convex_type_to_rust_type(data_type: &JsonValue, table_name: Option<&str>, field_name: Option<&str>) -> String
{
let type_str = data_type["type"].as_str().unwrap_or("unknown");
match type_str {
"string" => "String".to_string(),
"number" => "f64".to_string(),
"boolean" => "bool".to_string(),
"null" => "()".to_string(),
"int64" => "i64".to_string(),
"bytes" => "Vec<u8>".to_string(),
"any" => "ConvexJsonValue".to_string(),
"array" => {
let element_type = convex_type_to_rust_type(&data_type["elements"], None, None);
format!("Vec<{}>", element_type)
}
"object" => {
if let Some(props) = data_type["properties"].as_object() {
if props.is_empty() {
return "ConvexJsonValue".to_string();
}
let mut keys: Vec<&String> = props.keys().collect();
keys.sort();
let rust_types: Vec<String> = keys
.iter()
.filter_map(|k| props.get(*k).map(|v| convex_type_to_rust_type(v, None, None)))
.collect();
if rust_types.is_empty() {
return "ConvexJsonValue".to_string();
}
let first = rust_types[0].clone();
if rust_types[1..].iter().all(|t| t == &first) {
format!("std::collections::BTreeMap<String, {}>", first)
} else {
"ConvexJsonValue".to_string()
}
} else {
"ConvexJsonValue".to_string()
}
}
"record" => {
let key_type = convex_type_to_rust_type(&data_type["keyType"], None, None);
let value_type = convex_type_to_rust_type(&data_type["valueType"], None, None);
format!("std::collections::HashMap<{}, {}>", key_type, value_type)
}
"optional" => {
let inner_type = match data_type["inner"]["type"].as_str() {
Some("union") => {
if let (Some(table), Some(field)) = (table_name, field_name) {
format!("{}Optional{}", capitalize_first_letter(table), capitalize_first_letter(field))
} else {
"ConvexJsonValue".to_string()
}
}
_ => convex_type_to_rust_type(&data_type["inner"], None, None),
};
format!("Option<{}>", inner_type)
}
"literal" => {
if let Some(value) = data_type["value"]["value"].as_str() {
format!("\"{}\"", value)
} else {
"String".to_string()
}
}
"id" => "String".to_string(),
_ => "ConvexJsonValue".to_string(), }
}
fn convex_arg_root_is_optional(data_type: &JsonValue) -> bool
{
data_type["type"].as_str() == Some("optional")
}
fn generate_function_code(function: ConvexFunction) -> String
{
let mut code = String::new();
let struct_name = function_args_struct_name(&function.file_name, &function.name);
code.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
code.push_str("#[serde(crate = \"convex_typegen::serde\")]\n");
code.push_str(&format!("pub struct {} {{\n", struct_name));
for param in &function.params {
let rust_type = convex_type_to_rust_type(¶m.data_type, None, None);
code.push_str(&format!(" pub {}: {},\n", param.name, rust_type));
}
code.push_str("}\n\n");
code.push_str(&format!("impl {} {{\n", struct_name));
code.push_str(" pub const FUNCTION_PATH: &'static str = ");
code.push_str(&format!("\"{}:{}\";\n", function.file_name, function.name));
code.push_str("}\n\n");
code.push_str(&format!(
"impl std::convert::TryFrom<{}> for std::collections::BTreeMap<String, ConvexJsonValue> {{\n",
struct_name
));
code.push_str(" type Error = ConvexJsonError;\n\n");
code.push_str(&format!(
" fn try_from(_args: {}) -> Result<Self, Self::Error> {{\n",
struct_name
));
if function.params.is_empty() {
code.push_str(" Ok(std::collections::BTreeMap::new())\n");
} else {
code.push_str(" let mut map = std::collections::BTreeMap::new();\n");
for param in &function.params {
if convex_arg_root_is_optional(¶m.data_type) {
code.push_str(&format!(
" if let Some(__v) = _args.{} {{\n map.insert(\"{}\".to_string(), \
convex_typegen::serde_json::to_value(__v)?);\n }}\n",
param.name, param.name
));
} else {
code.push_str(&format!(
" map.insert(\"{}\".to_string(), convex_typegen::serde_json::to_value(_args.{})?);\n",
param.name, param.name
));
}
}
code.push_str(" Ok(map)\n");
}
code.push_str(" }\n");
code.push_str("}\n\n");
code
}
#[cfg(test)]
mod convex_arg_root_is_optional_tests
{
use serde_json::json;
use super::convex_arg_root_is_optional;
#[test]
fn true_for_optional_root()
{
assert!(convex_arg_root_is_optional(&json!({
"type": "optional",
"inner": { "type": "boolean" }
})));
}
#[test]
fn false_for_non_optional()
{
assert!(!convex_arg_root_is_optional(&json!({ "type": "string" })));
}
}
#[cfg(test)]
mod convex_type_to_rust_type_tests
{
use serde_json::json;
use super::convex_type_to_rust_type;
#[test]
fn primitives_and_container_shapes()
{
assert_eq!(convex_type_to_rust_type(&json!({ "type": "string" }), None, None), "String");
assert_eq!(convex_type_to_rust_type(&json!({ "type": "boolean" }), None, None), "bool");
assert_eq!(convex_type_to_rust_type(&json!({ "type": "number" }), None, None), "f64");
assert_eq!(convex_type_to_rust_type(&json!({ "type": "int64" }), None, None), "i64");
assert_eq!(convex_type_to_rust_type(&json!({ "type": "bytes" }), None, None), "Vec<u8>");
assert_eq!(
convex_type_to_rust_type(&json!({ "type": "any" }), None, None),
"ConvexJsonValue"
);
}
#[test]
fn array_wraps_element()
{
let t = convex_type_to_rust_type(&json!({ "type": "array", "elements": { "type": "string" } }), None, None);
assert_eq!(t, "Vec<String>");
}
#[test]
fn optional_union_uses_table_field_enum_names()
{
let t = convex_type_to_rust_type(
&json!({
"type": "optional",
"inner": { "type": "union", "variants": [] }
}),
Some("items"),
Some("status"),
);
assert_eq!(t, "Option<ItemsOptionalStatus>");
}
#[test]
fn homogeneous_object_becomes_btreemap()
{
let t = convex_type_to_rust_type(
&json!({
"type": "object",
"properties": {
"a": { "type": "string" },
"b": { "type": "string" }
}
}),
None,
None,
);
assert_eq!(t, "std::collections::BTreeMap<String, String>");
}
#[test]
fn heterogeneous_object_falls_back_to_json_value()
{
let t = convex_type_to_rust_type(
&json!({
"type": "object",
"properties": {
"a": { "type": "string" },
"b": { "type": "number" }
}
}),
None,
None,
);
assert_eq!(t, "ConvexJsonValue");
}
#[test]
fn unknown_type_maps_to_json_value()
{
assert_eq!(
convex_type_to_rust_type(&json!({ "type": "future_validator" }), None, None),
"ConvexJsonValue"
);
}
}
#[cfg(test)]
mod generate_table_enums_tests
{
use serde_json::json;
use super::generate_table_enums;
use crate::convex::types::{ConvexColumn, ConvexTable};
#[test]
fn union_column_emits_enum()
{
let table = ConvexTable {
name: "doc".into(),
columns: vec![ConvexColumn {
name: "state".into(),
data_type: json!({
"type": "union",
"variants": [
{ "type": "literal", "value": { "value": "on" } },
{ "type": "string" }
]
}),
}],
};
let code = generate_table_enums(&table);
assert!(code.contains("pub enum DocState"));
assert!(code.contains("On"));
assert!(code.contains("String(String)"));
}
}
#[cfg(test)]
mod generate_table_code_tests
{
use serde_json::json;
use super::generate_table_code;
use crate::convex::types::{ConvexColumn, ConvexTable};
#[test]
fn struct_fields_use_expected_rust_types()
{
let table = ConvexTable {
name: "user".into(),
columns: vec![ConvexColumn {
name: "name".into(),
data_type: json!({ "type": "string" }),
}],
};
let code = generate_table_code(table);
assert!(code.contains("pub struct UserTable"));
assert!(code.contains("pub name: String"));
}
}
#[cfg(test)]
mod generate_function_code_tests
{
use serde_json::json;
use super::generate_function_code;
use crate::convex::types::{ConvexFunction, ConvexFunctionParam};
#[test]
fn emits_function_path_and_try_from()
{
let f = ConvexFunction {
name: "ping".into(),
params: vec![ConvexFunctionParam {
name: "msg".into(),
data_type: json!({ "type": "string" }),
}],
type_: "query".into(),
file_name: "net".into(),
};
let code = generate_function_code(f);
assert!(code.contains("pub struct NetPingArgs"));
assert!(code.contains("\"net:ping\""));
assert!(code.contains("TryFrom<NetPingArgs>"));
assert!(code.contains("map.insert(\"msg\""));
}
#[test]
fn optional_root_param_skips_null_insert_path()
{
let f = ConvexFunction {
name: "opt".into(),
params: vec![ConvexFunctionParam {
name: "x".into(),
data_type: json!({ "type": "optional", "inner": { "type": "boolean" } }),
}],
type_: "mutation".into(),
file_name: "m".into(),
};
let code = generate_function_code(f);
assert!(code.contains("if let Some(__v) = _args.x"));
}
#[test]
fn empty_params_try_from_returns_empty_map()
{
let f = ConvexFunction {
name: "noop".into(),
params: vec![],
type_: "query".into(),
file_name: "api".into(),
};
let code = generate_function_code(f);
assert!(code.contains("BTreeMap::new()"));
}
}
#[cfg(test)]
mod run_codegen_tests
{
use std::fs;
use serde_json::json;
use tempfile::tempdir;
use super::run_codegen;
use crate::convex::types::{ConvexColumn, ConvexFunction, ConvexSchema, ConvexTable};
#[test]
fn writes_header_and_table_and_function_snippets()
{
let tmp = tempdir().unwrap();
let out = tmp.path().join("out.rs");
let schema = ConvexSchema {
tables: vec![ConvexTable {
name: "t".into(),
columns: vec![ConvexColumn {
name: "n".into(),
data_type: json!({ "type": "number" }),
}],
}],
};
let functions = vec![ConvexFunction {
name: "f".into(),
params: vec![],
type_: "query".into(),
file_name: "api".into(),
}];
run_codegen(&out, (schema, functions)).unwrap();
let body = fs::read_to_string(&out).unwrap();
assert!(body.contains("convex-typegen"));
assert!(body.contains("TTable"));
assert!(body.contains("ApiFArgs"));
}
#[test]
fn run_codegen_errors_on_duplicate_qualified_struct_name()
{
let tmp = tempdir().unwrap();
let out = tmp.path().join("out.rs");
let schema = ConvexSchema { tables: vec![] };
let functions = vec![
ConvexFunction {
name: "list".into(),
params: vec![],
type_: "query".into(),
file_name: "mod".into(),
},
ConvexFunction {
name: "list".into(),
params: vec![],
type_: "query".into(),
file_name: "mod".into(),
},
];
let err = run_codegen(&out, (schema, functions)).unwrap_err();
assert!(matches!(err, crate::error::ConvexTypeGeneratorError::InvalidSchema { .. }));
}
}