graphql_codegen_rust/
generator.rs

1use std::collections::HashMap;
2
3use crate::cli::{DatabaseType, OrmType};
4use crate::config::Config;
5use crate::parser::{ParsedField, ParsedSchema};
6
7pub mod diesel;
8pub mod sea_orm;
9
10pub trait CodeGenerator {
11    fn generate_schema(&self, schema: &ParsedSchema, config: &Config) -> anyhow::Result<String>;
12    fn generate_entities(
13        &self,
14        schema: &ParsedSchema,
15        config: &Config,
16    ) -> anyhow::Result<HashMap<String, String>>;
17    fn generate_migrations(
18        &self,
19        schema: &ParsedSchema,
20        config: &Config,
21    ) -> anyhow::Result<Vec<MigrationFile>>;
22}
23
24#[derive(Debug)]
25pub struct MigrationFile {
26    pub name: String,
27    pub up_sql: String,
28    pub down_sql: String,
29}
30
31pub fn create_generator(orm: &OrmType) -> Box<dyn CodeGenerator> {
32    match orm {
33        OrmType::Diesel => Box::new(diesel::DieselGenerator::new()),
34        OrmType::SeaOrm => Box::new(sea_orm::SeaOrmGenerator::new()),
35    }
36}
37
38pub fn to_snake_case(s: &str) -> String {
39    let mut result = String::new();
40    let chars: Vec<char> = s.chars().collect();
41
42    for (i, &ch) in chars.iter().enumerate() {
43        if ch.is_uppercase() {
44            // Add underscore if:
45            // 1. Not the first character AND previous character exists AND either:
46            //    a. Previous was lowercase, OR
47            //    b. Previous was uppercase and next is lowercase (end of acronym)
48            if i > 0 {
49                let prev = chars[i - 1];
50                let should_add_underscore = if prev.is_lowercase() {
51                    true
52                } else if prev.is_uppercase() {
53                    // Check if next character exists and is lowercase
54                    chars.get(i + 1).is_some_and(|&next| next.is_lowercase())
55                } else {
56                    false
57                };
58
59                if should_add_underscore {
60                    result.push('_');
61                }
62            }
63            result.push(ch.to_lowercase().next().unwrap());
64        } else {
65            result.push(ch);
66        }
67    }
68
69    result
70}
71
72pub fn rust_type_for_field(
73    field: &ParsedField,
74    db_type: &DatabaseType,
75    scalar_mappings: &HashMap<String, String>,
76) -> String {
77    match &field.field_type {
78        crate::parser::FieldType::Scalar(scalar_type) => match scalar_type.as_str() {
79            "ID" => match db_type {
80                DatabaseType::Sqlite => "i32".to_string(),
81                DatabaseType::Postgres => "uuid::Uuid".to_string(),
82                DatabaseType::Mysql => "u32".to_string(),
83            },
84            "String" => "String".to_string(),
85            "Int" => "i32".to_string(),
86            "Float" => "f64".to_string(),
87            "Boolean" => "bool".to_string(),
88            custom => scalar_mappings
89                .get(custom)
90                .cloned()
91                .unwrap_or_else(|| "String".to_string()),
92        },
93        crate::parser::FieldType::Reference(_type_name) => {
94            // For references, we'll assume they're other entities
95            // In a real implementation, we'd need to handle foreign keys
96            match db_type {
97                DatabaseType::Sqlite => "i32".to_string(),
98                DatabaseType::Postgres => "uuid::Uuid".to_string(),
99                DatabaseType::Mysql => "u32".to_string(),
100            }
101        }
102        crate::parser::FieldType::Enum(enum_name) => enum_name.clone(),
103    }
104}
105
106pub fn diesel_column_type_for_field(
107    field: &ParsedField,
108    db_type: &DatabaseType,
109    scalar_mappings: &HashMap<String, String>,
110) -> String {
111    match &field.field_type {
112        crate::parser::FieldType::Scalar(scalar_type) => match scalar_type.as_str() {
113            "ID" => match db_type {
114                DatabaseType::Sqlite => "Integer".to_string(),
115                DatabaseType::Postgres => "Uuid".to_string(),
116                DatabaseType::Mysql => "Unsigned<Integer>".to_string(),
117            },
118            "String" => "Text".to_string(),
119            "Int" => "Integer".to_string(),
120            "Float" => "Double".to_string(),
121            "Boolean" => "Bool".to_string(),
122            custom => scalar_mappings
123                .get(custom)
124                .cloned()
125                .unwrap_or_else(|| "Text".to_string()),
126        },
127        crate::parser::FieldType::Reference(_) => {
128            // Foreign key
129            match db_type {
130                DatabaseType::Sqlite => "Integer".to_string(),
131                DatabaseType::Postgres => "Uuid".to_string(),
132                DatabaseType::Mysql => "Unsigned<Integer>".to_string(),
133            }
134        }
135        crate::parser::FieldType::Enum(_) => "Text".to_string(),
136    }
137}
138
139pub fn sql_type_for_field(
140    field: &ParsedField,
141    db_type: &DatabaseType,
142    scalar_mappings: &HashMap<String, String>,
143) -> String {
144    match &field.field_type {
145        crate::parser::FieldType::Scalar(scalar_type) => match scalar_type.as_str() {
146            "ID" => match db_type {
147                DatabaseType::Sqlite => "INTEGER".to_string(),
148                DatabaseType::Postgres => "UUID".to_string(),
149                DatabaseType::Mysql => "INT UNSIGNED".to_string(),
150            },
151            "String" => "TEXT".to_string(),
152            "Int" => "INTEGER".to_string(),
153            "Float" => "REAL".to_string(),
154            "Boolean" => match db_type {
155                DatabaseType::Sqlite => "INTEGER".to_string(),
156                DatabaseType::Postgres => "BOOLEAN".to_string(),
157                DatabaseType::Mysql => "TINYINT(1)".to_string(),
158            },
159            custom => scalar_mappings
160                .get(custom)
161                .cloned()
162                .unwrap_or_else(|| "TEXT".to_string()),
163        },
164        crate::parser::FieldType::Reference(_) => {
165            // Foreign key
166            match db_type {
167                DatabaseType::Sqlite => "INTEGER".to_string(),
168                DatabaseType::Postgres => "UUID".to_string(),
169                DatabaseType::Mysql => "INT UNSIGNED".to_string(),
170            }
171        }
172        crate::parser::FieldType::Enum(_) => "TEXT".to_string(),
173    }
174}
175
176/// Detect if a field is likely a foreign key relationship
177#[allow(dead_code)]
178pub fn is_foreign_key_field(field: &ParsedField) -> Option<String> {
179    let field_name = &field.name;
180
181    // Common foreign key patterns
182    if field_name.ends_with("Id") && field_name.len() > 2 {
183        // Remove "Id" suffix and convert to PascalCase
184        let related_type_base = &field_name[..field_name.len() - 2];
185        // Capitalize first letter to get the type name
186        let related_type = related_type_base
187            .chars()
188            .next()
189            .map(|c| c.to_uppercase().to_string())
190            .unwrap_or_default()
191            + &related_type_base[1..];
192        return Some(related_type);
193    }
194
195    if field_name == "id" && matches!(field.field_type, crate::parser::FieldType::Reference(_)) {
196        // For fields named "id" that are references, we can't determine the related type
197        // This would need more context from the schema
198        return None;
199    }
200
201    None
202}
203
204/// Detect relationships between types in the schema
205#[allow(dead_code)]
206pub fn detect_relationships(
207    schema: &crate::parser::ParsedSchema,
208) -> HashMap<String, Vec<Relationship>> {
209    let mut relationships = HashMap::new();
210
211    for (type_name, parsed_type) in &schema.types {
212        if !matches!(parsed_type.kind, crate::parser::TypeKind::Object) {
213            continue;
214        }
215
216        let mut type_relationships = Vec::new();
217
218        for field in &parsed_type.fields {
219            if let Some(related_type) = is_foreign_key_field(field) {
220                // Check if the related type exists in the schema
221                if schema.types.contains_key(&related_type) {
222                    let relationship = Relationship {
223                        field_name: field.name.clone(),
224                        related_type: related_type.clone(),
225                        relationship_type: RelationshipType::BelongsTo,
226                        foreign_key: true,
227                    };
228                    type_relationships.push(relationship);
229                }
230            }
231        }
232
233        if !type_relationships.is_empty() {
234            relationships.insert(type_name.clone(), type_relationships);
235        }
236    }
237
238    relationships
239}
240
241#[derive(Debug, Clone)]
242#[allow(dead_code)]
243pub struct Relationship {
244    pub field_name: String,
245    pub related_type: String,
246    pub relationship_type: RelationshipType,
247    pub foreign_key: bool,
248}
249
250#[derive(Debug, Clone)]
251#[allow(dead_code)]
252pub enum RelationshipType {
253    BelongsTo,
254    HasMany,
255    HasOne,
256}