elif-codegen 0.4.0

Code generation utilities for the elif.rs web framework
Documentation
use elif_core::{ElifError, ResourceSpec, FieldSpec};
use crate::templates::{render_template, MODEL_TEMPLATE, HANDLER_TEMPLATE, MIGRATION_TEMPLATE, TEST_TEMPLATE};
use crate::writer::CodeWriter;
use std::path::PathBuf;
use std::collections::HashMap;

pub struct ResourceGenerator<'a> {
    project_root: &'a PathBuf,
    spec: &'a ResourceSpec,
    writer: CodeWriter,
}

impl<'a> ResourceGenerator<'a> {
    pub fn new(project_root: &'a PathBuf, spec: &'a ResourceSpec) -> Self {
        Self {
            project_root,
            spec,
            writer: CodeWriter::new(),
        }
    }
    
    pub fn generate_model(&self) -> Result<(), ElifError> {
        let model_path = self.project_root
            .join("crates/orm/src/models")
            .join(format!("{}.rs", self.spec.name.to_lowercase()));
        
        let mut context = HashMap::new();
        context.insert("name", self.spec.name.clone());
        context.insert("table", self.spec.storage.table.clone());
        context.insert("fields", self.format_model_fields());
        
        let content = render_template(MODEL_TEMPLATE, &context)?;
        self.writer.write_if_changed(&model_path, &content)?;
        
        Ok(())
    }
    
    pub fn generate_handler(&self) -> Result<(), ElifError> {
        let handler_path = self.project_root
            .join("apps/api/src/routes")
            .join(format!("{}.rs", self.spec.name.to_lowercase()));
        
        let mut context = HashMap::new();
        context.insert("name", self.spec.name.clone());
        context.insert("route", self.spec.route.clone());
        context.insert("operations", self.format_operations());
        
        let content = render_template(HANDLER_TEMPLATE, &context)?;
        self.writer.write_preserving_markers(&handler_path, &content)?;
        
        Ok(())
    }
    
    pub fn generate_migration(&self) -> Result<(), ElifError> {
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map_err(|e| ElifError::Codegen { message: format!("Time error: {}", e) })?
            .as_secs();
        
        let migration_path = self.project_root
            .join("migrations")
            .join(format!("{}_create_{}.sql", timestamp, self.spec.storage.table));
        
        let mut context = HashMap::new();
        context.insert("table", self.spec.storage.table.clone());
        context.insert("fields", self.format_migration_fields());
        context.insert("indexes", self.format_migration_indexes());
        
        let content = render_template(MIGRATION_TEMPLATE, &context)?;
        self.writer.write_if_changed(&migration_path, &content)?;
        
        Ok(())
    }
    
    pub fn generate_test(&self) -> Result<(), ElifError> {
        let test_path = self.project_root
            .join("tests")
            .join(format!("{}_http.rs", self.spec.name.to_lowercase()));
        
        let mut context = HashMap::new();
        context.insert("name", self.spec.name.clone());
        context.insert("route", self.spec.route.clone());
        
        let content = render_template(TEST_TEMPLATE, &context)?;
        self.writer.write_if_changed(&test_path, &content)?;
        
        Ok(())
    }
    
    fn format_model_fields(&self) -> String {
        self.spec.storage.fields.iter()
            .map(|field| self.format_model_field(field))
            .collect::<Vec<_>>()
            .join("\n    ")
    }
    
    fn format_model_field(&self, field: &FieldSpec) -> String {
        let rust_type = self.map_field_type(&field.field_type);
        let optional = if field.required { rust_type } else { format!("Option<{}>", rust_type) };
        
        format!("pub {}: {},", field.name, optional)
    }
    
    fn format_migration_fields(&self) -> String {
        let mut fields = self.spec.storage.fields.iter()
            .map(|field| self.format_migration_field(field))
            .collect::<Vec<_>>();
            
        if self.spec.storage.timestamps {
            fields.push("    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),".to_string());
            fields.push("    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()".to_string());
        }
        
        fields.join("\n")
    }
    
    fn format_migration_field(&self, field: &FieldSpec) -> String {
        let sql_type = self.map_field_type_to_sql(&field.field_type);
        let nullable = if field.required { "NOT NULL" } else { "" };
        let default = field.default.as_ref()
            .map(|d| format!("DEFAULT {}", d))
            .unwrap_or_default();
        let pk = if field.pk { "PRIMARY KEY" } else { "" };
        
        format!("    {} {} {} {} {},", field.name, sql_type, pk, nullable, default).trim().to_string()
    }
    
    fn format_migration_indexes(&self) -> String {
        self.spec.indexes.iter()
            .map(|idx| format!(
                "CREATE INDEX {} ON {} ({});",
                idx.name,
                self.spec.storage.table,
                idx.fields.join(", ")
            ))
            .collect::<Vec<_>>()
            .join("\n")
    }
    
    fn format_operations(&self) -> String {
        self.spec.api.operations.iter()
            .map(|op| format!("{} {}", op.method, op.path))
            .collect::<Vec<_>>()
            .join(", ")
    }
    
    fn map_field_type(&self, field_type: &str) -> String {
        match field_type {
            "uuid" => "uuid::Uuid".to_string(),
            "text" | "string" => "String".to_string(),
            "bool" => "bool".to_string(),
            "int" => "i32".to_string(),
            "bigint" => "i64".to_string(),
            "float" => "f64".to_string(),
            "timestamp" => "chrono::DateTime<chrono::Utc>".to_string(),
            "json" => "serde_json::Value".to_string(),
            _ => "String".to_string(),
        }
    }
    
    fn map_field_type_to_sql(&self, field_type: &str) -> String {
        match field_type {
            "uuid" => "UUID".to_string(),
            "text" | "string" => "TEXT".to_string(),
            "bool" => "BOOLEAN".to_string(),
            "int" => "INTEGER".to_string(),
            "bigint" => "BIGINT".to_string(),
            "float" => "DOUBLE PRECISION".to_string(),
            "timestamp" => "TIMESTAMPTZ".to_string(),
            "json" => "JSONB".to_string(),
            _ => "TEXT".to_string(),
        }
    }
}