use crate::build::Schema;
use crate::migrate::types::ColumnType;
use std::fs;
pub fn generate_to_file(schema_path: &str, output_path: &str) -> Result<(), String> {
let schema = Schema::parse_file(schema_path)?;
let code = generate_schema_code(&schema);
fs::write(output_path, &code).map_err(|e| format!("Failed to write output: {}", e))?;
Ok(())
}
pub fn generate_from_file(schema_path: &str) -> Result<String, String> {
let schema = Schema::parse_file(schema_path)?;
Ok(generate_schema_code(&schema))
}
pub fn generate_schema_code(schema: &Schema) -> String {
let mut code = String::new();
code.push_str("//! Auto-generated by `qail types`\n");
code.push_str("//! Do not edit manually.\n\n");
code.push_str("#![allow(dead_code)]\n\n");
code.push_str("use qail_core::typed::{Table, TypedColumn, RequiresRls, DirectBuild, Bucket, Queue, Topic};\n\n");
let mut table_names: Vec<_> = schema.tables.keys().collect();
table_names.sort();
for table_name in &table_names {
if let Some(table) = schema.tables.get(*table_name) {
code.push_str(&generate_table_module(table_name, table));
code.push('\n');
}
}
code.push_str("/// Re-export all table types\n");
code.push_str("pub mod tables {\n");
for table_name in &table_names {
let struct_name = to_pascal_case(table_name);
code.push_str(&format!(
" pub use super::{}::{};\n",
table_name, struct_name
));
}
code.push_str("}\n\n");
let mut resource_names: Vec<_> = schema.resources.keys().collect();
resource_names.sort();
for res_name in &resource_names {
if let Some(resource) = schema.resources.get(*res_name) {
code.push_str(&generate_resource_module(res_name, resource));
code.push('\n');
}
}
if !resource_names.is_empty() {
code.push_str("/// Re-export all resource types\n");
code.push_str("pub mod resources {\n");
for res_name in &resource_names {
let struct_name = to_pascal_case(res_name);
code.push_str(&format!(
" pub use super::{}::{};\n",
res_name, struct_name
));
}
code.push_str("}\n");
}
code
}
fn generate_resource_module(
resource_name: &str,
resource: &crate::build::ResourceSchema,
) -> String {
let mut code = String::new();
let struct_name = to_pascal_case(resource_name);
let kind = &resource.kind;
code.push_str(&format!("/// {} resource: {}\n", kind, resource_name));
code.push_str(&format!("pub mod {} {{\n", resource_name));
code.push_str(" use super::*;\n\n");
code.push_str(&format!(
" /// Type-safe reference to {} `{}`\n",
kind, resource_name
));
code.push_str(" #[derive(Debug, Clone, Copy, Default)]\n");
code.push_str(&format!(" pub struct {};\n\n", struct_name));
let (trait_name, method_name) = match kind.as_str() {
"bucket" => ("Bucket", "bucket_name"),
"queue" => ("Queue", "queue_name"),
"topic" => ("Topic", "topic_name"),
_ => ("Bucket", "bucket_name"), };
code.push_str(&format!(" impl {} for {} {{\n", trait_name, struct_name));
code.push_str(&format!(
" fn {}() -> &'static str {{ \"{}\" }}\n",
method_name, resource_name
));
code.push_str(" }\n");
if let Some(ref provider) = resource.provider {
code.push_str(&format!(
"\n pub const PROVIDER: &str = \"{}\";\n",
provider
));
}
for (key, value) in &resource.properties {
let const_name = key.to_uppercase();
code.push_str(&format!(
" pub const {}: &str = \"{}\";\n",
const_name, value
));
}
code.push_str("}\n");
code
}
fn generate_table_module(table_name: &str, table: &crate::build::TableSchema) -> String {
let mut code = String::new();
let struct_name = to_pascal_case(table_name);
code.push_str(&format!("/// Table: {}\n", table_name));
code.push_str(&format!("pub mod {} {{\n", table_name));
code.push_str(" use super::*;\n\n");
code.push_str(&format!(
" /// Type-safe reference to `{}`\n",
table_name
));
code.push_str(" #[derive(Debug, Clone, Copy, Default)]\n");
code.push_str(&format!(" pub struct {};\n\n", struct_name));
code.push_str(&format!(" impl Table for {} {{\n", struct_name));
code.push_str(&format!(
" fn table_name() -> &'static str {{ \"{}\" }}\n",
table_name
));
code.push_str(" }\n\n");
code.push_str(&format!(" impl From<{}> for String {{\n", struct_name));
code.push_str(&format!(
" fn from(_: {}) -> String {{ \"{}\".to_string() }}\n",
struct_name, table_name
));
code.push_str(" }\n\n");
code.push_str(&format!(" impl AsRef<str> for {} {{\n", struct_name));
code.push_str(&format!(
" fn as_ref(&self) -> &str {{ \"{}\" }}\n",
table_name
));
code.push_str(" }\n\n");
if table.rls_enabled {
code.push_str(" /// This table has `tenant_id` — queries require `.with_rls()` proof\n");
code.push_str(&format!(
" impl RequiresRls for {} {{}}\n\n",
struct_name
));
} else {
code.push_str(&format!(
" impl DirectBuild for {} {{}}\n\n",
struct_name
));
}
let mut col_names: Vec<_> = table.columns.keys().collect();
col_names.sort();
for col_name in &col_names {
if let Some(col_type) = table.columns.get(*col_name) {
let rust_type = column_type_to_rust(col_type);
let fn_name = escape_keyword(col_name);
code.push_str(&format!(
" /// Column `{}` ({})\n",
col_name,
col_type.to_pg_type()
));
code.push_str(&format!(
" pub fn {}() -> TypedColumn<{}> {{ TypedColumn::new(\"{}\", \"{}\") }}\n\n",
fn_name, rust_type, table_name, col_name
));
}
}
code.push_str("}\n");
code
}
fn column_type_to_rust(col_type: &ColumnType) -> &'static str {
match col_type {
ColumnType::Uuid => "uuid::Uuid",
ColumnType::Text | ColumnType::Varchar(_) => "String",
ColumnType::Int | ColumnType::BigInt | ColumnType::Serial | ColumnType::BigSerial => "i64",
ColumnType::Bool => "bool",
ColumnType::Float | ColumnType::Decimal(_) => "f64",
ColumnType::Jsonb => "serde_json::Value",
ColumnType::Timestamp | ColumnType::Timestamptz | ColumnType::Date | ColumnType::Time => {
"chrono::DateTime<chrono::Utc>"
}
ColumnType::Bytea => "Vec<u8>",
ColumnType::Array(_) => "Vec<serde_json::Value>",
ColumnType::Enum { .. } => "String",
ColumnType::Range(_) => "String",
ColumnType::Interval => "String",
ColumnType::Cidr | ColumnType::Inet => "String",
ColumnType::MacAddr => "String",
}
}
fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().chain(chars).collect(),
}
})
.collect()
}
fn escape_keyword(name: &str) -> String {
const KEYWORDS: &[&str] = &[
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn",
"for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
"return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe",
"use", "where", "while", "async", "await", "dyn", "abstract", "become", "box", "do",
"final", "macro", "override", "priv", "try", "typeof", "unsized", "virtual", "yield",
];
if KEYWORDS.contains(&name) {
format!("r#{}", name)
} else {
name.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pascal_case() {
assert_eq!(to_pascal_case("users"), "Users");
assert_eq!(to_pascal_case("user_profiles"), "UserProfiles");
}
#[test]
fn test_column_type_mapping() {
assert_eq!(column_type_to_rust(&ColumnType::Int), "i64");
assert_eq!(column_type_to_rust(&ColumnType::Text), "String");
assert_eq!(column_type_to_rust(&ColumnType::Uuid), "uuid::Uuid");
assert_eq!(column_type_to_rust(&ColumnType::Bool), "bool");
assert_eq!(column_type_to_rust(&ColumnType::Jsonb), "serde_json::Value");
assert_eq!(column_type_to_rust(&ColumnType::BigInt), "i64");
assert_eq!(column_type_to_rust(&ColumnType::Float), "f64");
assert_eq!(
column_type_to_rust(&ColumnType::Timestamp),
"chrono::DateTime<chrono::Utc>"
);
assert_eq!(column_type_to_rust(&ColumnType::Bytea), "Vec<u8>");
}
}