elif_codegen/
generator.rs1use elif_core::{ElifError, ResourceSpec, FieldSpec};
2use crate::templates::{render_template, MODEL_TEMPLATE, HANDLER_TEMPLATE, MIGRATION_TEMPLATE, TEST_TEMPLATE};
3use crate::writer::CodeWriter;
4use std::path::PathBuf;
5use std::collections::HashMap;
6
7pub struct ResourceGenerator<'a> {
8 project_root: &'a PathBuf,
9 spec: &'a ResourceSpec,
10 writer: CodeWriter,
11}
12
13impl<'a> ResourceGenerator<'a> {
14 pub fn new(project_root: &'a PathBuf, spec: &'a ResourceSpec) -> Self {
15 Self {
16 project_root,
17 spec,
18 writer: CodeWriter::new(),
19 }
20 }
21
22 pub fn generate_model(&self) -> Result<(), ElifError> {
23 let model_path = self.project_root
24 .join("crates/orm/src/models")
25 .join(format!("{}.rs", self.spec.name.to_lowercase()));
26
27 let mut context = HashMap::new();
28 context.insert("name", self.spec.name.clone());
29 context.insert("table", self.spec.storage.table.clone());
30 context.insert("fields", self.format_model_fields());
31
32 let content = render_template(MODEL_TEMPLATE, &context)?;
33 self.writer.write_if_changed(&model_path, &content)?;
34
35 Ok(())
36 }
37
38 pub fn generate_handler(&self) -> Result<(), ElifError> {
39 let handler_path = self.project_root
40 .join("apps/api/src/routes")
41 .join(format!("{}.rs", self.spec.name.to_lowercase()));
42
43 let mut context = HashMap::new();
44 context.insert("name", self.spec.name.clone());
45 context.insert("route", self.spec.route.clone());
46 context.insert("operations", self.format_operations());
47
48 let content = render_template(HANDLER_TEMPLATE, &context)?;
49 self.writer.write_preserving_markers(&handler_path, &content)?;
50
51 Ok(())
52 }
53
54 pub fn generate_migration(&self) -> Result<(), ElifError> {
55 let timestamp = std::time::SystemTime::now()
56 .duration_since(std::time::UNIX_EPOCH)
57 .map_err(|e| ElifError::Codegen(format!("Time error: {}", e)))?
58 .as_secs();
59
60 let migration_path = self.project_root
61 .join("migrations")
62 .join(format!("{}_create_{}.sql", timestamp, self.spec.storage.table));
63
64 let mut context = HashMap::new();
65 context.insert("table", self.spec.storage.table.clone());
66 context.insert("fields", self.format_migration_fields());
67 context.insert("indexes", self.format_migration_indexes());
68
69 let content = render_template(MIGRATION_TEMPLATE, &context)?;
70 self.writer.write_if_changed(&migration_path, &content)?;
71
72 Ok(())
73 }
74
75 pub fn generate_test(&self) -> Result<(), ElifError> {
76 let test_path = self.project_root
77 .join("tests")
78 .join(format!("{}_http.rs", self.spec.name.to_lowercase()));
79
80 let mut context = HashMap::new();
81 context.insert("name", self.spec.name.clone());
82 context.insert("route", self.spec.route.clone());
83
84 let content = render_template(TEST_TEMPLATE, &context)?;
85 self.writer.write_if_changed(&test_path, &content)?;
86
87 Ok(())
88 }
89
90 fn format_model_fields(&self) -> String {
91 self.spec.storage.fields.iter()
92 .map(|field| self.format_model_field(field))
93 .collect::<Vec<_>>()
94 .join("\n ")
95 }
96
97 fn format_model_field(&self, field: &FieldSpec) -> String {
98 let rust_type = self.map_field_type(&field.field_type);
99 let optional = if field.required { rust_type } else { format!("Option<{}>", rust_type) };
100
101 format!("pub {}: {},", field.name, optional)
102 }
103
104 fn format_migration_fields(&self) -> String {
105 let mut fields = self.spec.storage.fields.iter()
106 .map(|field| self.format_migration_field(field))
107 .collect::<Vec<_>>();
108
109 if self.spec.storage.timestamps {
110 fields.push(" created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),".to_string());
111 fields.push(" updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()".to_string());
112 }
113
114 fields.join("\n")
115 }
116
117 fn format_migration_field(&self, field: &FieldSpec) -> String {
118 let sql_type = self.map_field_type_to_sql(&field.field_type);
119 let nullable = if field.required { "NOT NULL" } else { "" };
120 let default = field.default.as_ref()
121 .map(|d| format!("DEFAULT {}", d))
122 .unwrap_or_default();
123 let pk = if field.pk { "PRIMARY KEY" } else { "" };
124
125 format!(" {} {} {} {} {},", field.name, sql_type, pk, nullable, default).trim().to_string()
126 }
127
128 fn format_migration_indexes(&self) -> String {
129 self.spec.indexes.iter()
130 .map(|idx| format!(
131 "CREATE INDEX {} ON {} ({});",
132 idx.name,
133 self.spec.storage.table,
134 idx.fields.join(", ")
135 ))
136 .collect::<Vec<_>>()
137 .join("\n")
138 }
139
140 fn format_operations(&self) -> String {
141 self.spec.api.operations.iter()
142 .map(|op| format!("{} {}", op.method, op.path))
143 .collect::<Vec<_>>()
144 .join(", ")
145 }
146
147 fn map_field_type(&self, field_type: &str) -> String {
148 match field_type {
149 "uuid" => "uuid::Uuid".to_string(),
150 "text" | "string" => "String".to_string(),
151 "bool" => "bool".to_string(),
152 "int" => "i32".to_string(),
153 "bigint" => "i64".to_string(),
154 "float" => "f64".to_string(),
155 "timestamp" => "chrono::DateTime<chrono::Utc>".to_string(),
156 "json" => "serde_json::Value".to_string(),
157 _ => "String".to_string(),
158 }
159 }
160
161 fn map_field_type_to_sql(&self, field_type: &str) -> String {
162 match field_type {
163 "uuid" => "UUID".to_string(),
164 "text" | "string" => "TEXT".to_string(),
165 "bool" => "BOOLEAN".to_string(),
166 "int" => "INTEGER".to_string(),
167 "bigint" => "BIGINT".to_string(),
168 "float" => "DOUBLE PRECISION".to_string(),
169 "timestamp" => "TIMESTAMPTZ".to_string(),
170 "json" => "JSONB".to_string(),
171 _ => "TEXT".to_string(),
172 }
173 }
174}