use std::io::{Seek, SeekFrom, Write};
use serde_json::Value as JsonValue;
use crate::convex::{ConvexFunction, ConvexFunctions, ConvexSchema, ConvexTable};
use crate::errors::ConvexTypeGeneratorError;
pub(crate) fn generate_code(path: &str, 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#"// This file is generated by convex-typegen. Do not modify directly.
// You can find more information about convex-typegen at https://github.com/JamalLyons/convex-typegen
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
use serde::{Serialize, Deserialize};
"#;
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));
}
for function in data.1 {
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() {
if 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" => "serde_json::Value".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() {
let value_type = props
.values()
.next()
.map(|v| convex_type_to_rust_type(v, None, None))
.unwrap_or_else(|| "serde_json::Value".to_string());
format!("std::collections::BTreeMap<String, {}>", value_type)
} else {
"serde_json::Value".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 {
"serde_json::Value".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(),
_ => "serde_json::Value".to_string(), }
}
fn generate_function_code(function: ConvexFunction) -> String
{
let mut code = String::new();
let struct_name = format!("{}Args", capitalize_first_letter(&function.name));
code.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\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 From<{}> for std::collections::BTreeMap<String, serde_json::Value> {{\n",
struct_name
));
code.push_str(&format!(" fn from(_args: {}) -> Self {{\n", struct_name));
if function.params.is_empty() {
code.push_str(" std::collections::BTreeMap::new()\n");
} else {
code.push_str(" let mut map = std::collections::BTreeMap::new();\n");
for param in &function.params {
code.push_str(&format!(
" map.insert(\"{}\".to_string(), serde_json::to_value(_args.{}).unwrap());\n",
param.name, param.name
));
}
code.push_str(" map\n");
}
code.push_str(" }\n");
code.push_str("}\n\n");
code
}
fn capitalize_first_letter(s: &str) -> String
{
if s.is_empty() {
return String::new();
}
let mut chars = s.chars();
let first_char = chars.next().expect("Expected a character but got none");
let rest = chars.collect::<String>();
first_char.to_uppercase().to_string() + &rest
}
fn to_pascal_case(s: &str) -> String
{
s.split(|c: char| !c.is_alphanumeric())
.filter(|s| !s.is_empty())
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + &chars.collect::<String>().to_lowercase(),
}
})
.collect()
}