elif_codegen/
generator.rs

1use 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 { message: 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}