1use crate::build::Schema;
24use crate::migrate::types::ColumnType;
25use std::fs;
26
27pub fn generate_to_file(schema_path: &str, output_path: &str) -> Result<(), String> {
29 let schema = Schema::parse_file(schema_path)?;
30 let code = generate_schema_code(&schema);
31 fs::write(output_path, &code)
32 .map_err(|e| format!("Failed to write output: {}", e))?;
33 Ok(())
34}
35
36pub fn generate_from_file(schema_path: &str) -> Result<String, String> {
38 let schema = Schema::parse_file(schema_path)?;
39 Ok(generate_schema_code(&schema))
40}
41
42pub fn generate_schema_code(schema: &Schema) -> String {
44 let mut code = String::new();
45
46 code.push_str("//! Auto-generated by `qail types`\n");
48 code.push_str("//! Do not edit manually.\n\n");
49 code.push_str("#![allow(dead_code)]\n\n");
50 code.push_str("use qail_core::typed::{Table, TypedColumn, RequiresRls, DirectBuild, Bucket, Queue, Topic};\n\n");
51
52 let mut table_names: Vec<_> = schema.tables.keys().collect();
54 table_names.sort();
55
56 for table_name in &table_names {
57 if let Some(table) = schema.tables.get(*table_name) {
58 code.push_str(&generate_table_module(table_name, table));
59 code.push('\n');
60 }
61 }
62
63 code.push_str("/// Re-export all table types\n");
65 code.push_str("pub mod tables {\n");
66
67 for table_name in &table_names {
68 let struct_name = to_pascal_case(table_name);
69 code.push_str(&format!(
70 " pub use super::{}::{};\n",
71 table_name, struct_name
72 ));
73 }
74 code.push_str("}\n\n");
75
76 let mut resource_names: Vec<_> = schema.resources.keys().collect();
78 resource_names.sort();
79
80 for res_name in &resource_names {
81 if let Some(resource) = schema.resources.get(*res_name) {
82 code.push_str(&generate_resource_module(res_name, resource));
83 code.push('\n');
84 }
85 }
86
87 if !resource_names.is_empty() {
89 code.push_str("/// Re-export all resource types\n");
90 code.push_str("pub mod resources {\n");
91 for res_name in &resource_names {
92 let struct_name = to_pascal_case(res_name);
93 code.push_str(&format!(
94 " pub use super::{}::{};\n",
95 res_name, struct_name
96 ));
97 }
98 code.push_str("}\n");
99 }
100
101 code
102}
103
104fn generate_resource_module(resource_name: &str, resource: &crate::build::ResourceSchema) -> String {
106 let mut code = String::new();
107 let struct_name = to_pascal_case(resource_name);
108 let kind = &resource.kind;
109
110 code.push_str(&format!("/// {} resource: {}\n", kind, resource_name));
111 code.push_str(&format!("pub mod {} {{\n", resource_name));
112 code.push_str(" use super::*;\n\n");
113
114 code.push_str(&format!(" /// Type-safe reference to {} `{}`\n", kind, resource_name));
116 code.push_str(" #[derive(Debug, Clone, Copy, Default)]\n");
117 code.push_str(&format!(" pub struct {};\n\n", struct_name));
118
119 let (trait_name, method_name) = match kind.as_str() {
121 "bucket" => ("Bucket", "bucket_name"),
122 "queue" => ("Queue", "queue_name"),
123 "topic" => ("Topic", "topic_name"),
124 _ => ("Bucket", "bucket_name"), };
126
127 code.push_str(&format!(" impl {} for {} {{\n", trait_name, struct_name));
128 code.push_str(&format!(
129 " fn {}() -> &'static str {{ \"{}\" }}\n",
130 method_name, resource_name
131 ));
132 code.push_str(" }\n");
133
134 if let Some(ref provider) = resource.provider {
136 code.push_str(&format!("\n pub const PROVIDER: &str = \"{}\";\n", provider));
137 }
138
139 for (key, value) in &resource.properties {
141 let const_name = key.to_uppercase();
142 code.push_str(&format!(" pub const {}: &str = \"{}\";\n", const_name, value));
143 }
144
145 code.push_str("}\n");
146 code
147}
148
149fn generate_table_module(table_name: &str, table: &crate::build::TableSchema) -> String {
150 let mut code = String::new();
151 let struct_name = to_pascal_case(table_name);
152
153 code.push_str(&format!("/// Table: {}\n", table_name));
154 code.push_str(&format!("pub mod {} {{\n", table_name));
155 code.push_str(" use super::*;\n\n");
156
157 code.push_str(&format!(" /// Type-safe reference to `{}`\n", table_name));
159 code.push_str(" #[derive(Debug, Clone, Copy, Default)]\n");
160 code.push_str(&format!(" pub struct {};\n\n", struct_name));
161
162 code.push_str(&format!(" impl Table for {} {{\n", struct_name));
163 code.push_str(&format!(
164 " fn table_name() -> &'static str {{ \"{}\" }}\n",
165 table_name
166 ));
167 code.push_str(" }\n\n");
168
169 code.push_str(&format!(" impl From<{}> for String {{\n", struct_name));
171 code.push_str(&format!(" fn from(_: {}) -> String {{ \"{}\".to_string() }}\n", struct_name, table_name));
172 code.push_str(" }\n\n");
173
174 code.push_str(&format!(" impl AsRef<str> for {} {{\n", struct_name));
176 code.push_str(&format!(" fn as_ref(&self) -> &str {{ \"{}\" }}\n", table_name));
177 code.push_str(" }\n\n");
178
179 if table.rls_enabled {
181 code.push_str(" /// This table has `operator_id` — queries require `.with_rls()` proof\n");
182 code.push_str(&format!(" impl RequiresRls for {} {{}}\n\n", struct_name));
183 } else {
184 code.push_str(&format!(" impl DirectBuild for {} {{}}\n\n", struct_name));
185 }
186
187 let mut col_names: Vec<_> = table.columns.keys().collect();
189 col_names.sort();
190
191 for col_name in &col_names {
192 if let Some(col_type) = table.columns.get(*col_name) {
193 let rust_type = column_type_to_rust(col_type);
194 let fn_name = escape_keyword(col_name);
195 code.push_str(&format!(
196 " /// Column `{}` ({})\n",
197 col_name, col_type.to_pg_type()
198 ));
199 code.push_str(&format!(
200 " pub fn {}() -> TypedColumn<{}> {{ TypedColumn::new(\"{}\", \"{}\") }}\n\n",
201 fn_name, rust_type, table_name, col_name
202 ));
203 }
204 }
205
206 code.push_str("}\n");
207
208 code
209}
210
211fn column_type_to_rust(col_type: &ColumnType) -> &'static str {
214 match col_type {
215 ColumnType::Uuid => "uuid::Uuid",
216 ColumnType::Text | ColumnType::Varchar(_) => "String",
217 ColumnType::Int | ColumnType::BigInt | ColumnType::Serial | ColumnType::BigSerial => "i64",
218 ColumnType::Bool => "bool",
219 ColumnType::Float | ColumnType::Decimal(_) => "f64",
220 ColumnType::Jsonb => "serde_json::Value",
221 ColumnType::Timestamp | ColumnType::Timestamptz | ColumnType::Date | ColumnType::Time => "chrono::DateTime<chrono::Utc>",
222 ColumnType::Bytea => "Vec<u8>",
223 ColumnType::Array(_) => "Vec<serde_json::Value>",
224 ColumnType::Enum { .. } => "String",
225 ColumnType::Range(_) => "String",
226 ColumnType::Interval => "String",
227 ColumnType::Cidr | ColumnType::Inet => "String",
228 ColumnType::MacAddr => "String",
229 }
230}
231
232fn to_pascal_case(s: &str) -> String {
234 s.split('_')
235 .map(|word| {
236 let mut chars = word.chars();
237 match chars.next() {
238 None => String::new(),
239 Some(c) => c.to_uppercase().chain(chars).collect(),
240 }
241 })
242 .collect()
243}
244
245fn escape_keyword(name: &str) -> String {
247 const KEYWORDS: &[&str] = &[
248 "as", "break", "const", "continue", "crate", "else", "enum", "extern",
249 "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod",
250 "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
251 "super", "trait", "true", "type", "unsafe", "use", "where", "while",
252 "async", "await", "dyn", "abstract", "become", "box", "do", "final",
253 "macro", "override", "priv", "try", "typeof", "unsized", "virtual", "yield",
254 ];
255
256 if KEYWORDS.contains(&name) {
257 format!("r#{}", name)
258 } else {
259 name.to_string()
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_pascal_case() {
269 assert_eq!(to_pascal_case("users"), "Users");
270 assert_eq!(to_pascal_case("user_profiles"), "UserProfiles");
271 }
272
273 #[test]
274 fn test_column_type_mapping() {
275 assert_eq!(column_type_to_rust(&ColumnType::Int), "i64");
276 assert_eq!(column_type_to_rust(&ColumnType::Text), "String");
277 assert_eq!(column_type_to_rust(&ColumnType::Uuid), "uuid::Uuid");
278 assert_eq!(column_type_to_rust(&ColumnType::Bool), "bool");
279 assert_eq!(column_type_to_rust(&ColumnType::Jsonb), "serde_json::Value");
280 assert_eq!(column_type_to_rust(&ColumnType::BigInt), "i64");
281 assert_eq!(column_type_to_rust(&ColumnType::Float), "f64");
282 assert_eq!(column_type_to_rust(&ColumnType::Timestamp), "chrono::DateTime<chrono::Utc>");
283 assert_eq!(column_type_to_rust(&ColumnType::Bytea), "Vec<u8>");
284 }
285}