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