graphql_codegen_rust/generator/
diesel.rs

1use std::collections::HashMap;
2
3use crate::cli::DatabaseType;
4use crate::config::Config;
5use crate::generator::{
6    CodeGenerator, MigrationFile, diesel_column_type_for_field, rust_type_for_field,
7    sql_type_for_field, to_snake_case,
8};
9use crate::parser::{ParsedEnum, ParsedSchema, ParsedType};
10
11pub struct DieselGenerator;
12
13impl DieselGenerator {
14    pub fn new() -> Self {
15        Self
16    }
17}
18
19impl Default for DieselGenerator {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl CodeGenerator for DieselGenerator {
26    fn generate_schema(&self, schema: &ParsedSchema, config: &Config) -> anyhow::Result<String> {
27        // Handle empty schemas gracefully
28        if schema.types.is_empty() && schema.enums.is_empty() {
29            return Ok("// No GraphQL types or enums found in schema\n".to_string());
30        }
31
32        let mut output = String::new();
33
34        // Add imports
35        output.push_str("use diesel::prelude::*;\n\n");
36
37        // Generate table! macros for each type
38        for (type_name, parsed_type) in &schema.types {
39            if !matches!(parsed_type.kind, crate::parser::TypeKind::Object) {
40                continue; // Skip interfaces and unions for Diesel schema
41            }
42            output.push_str(
43                &self
44                    .generate_table_macro(type_name, parsed_type, config)
45                    .map_err(|e| {
46                        anyhow::anyhow!(
47                            "Failed to generate table macro for type '{}': {}",
48                            type_name,
49                            e
50                        )
51                    })?,
52            );
53            output.push('\n');
54        }
55
56        // Generate enum types if needed
57        for (enum_name, parsed_enum) in &schema.enums {
58            output.push_str(
59                &self
60                    .generate_enum_type(enum_name, parsed_enum)
61                    .map_err(|e| {
62                        anyhow::anyhow!("Failed to generate enum type '{}': {}", enum_name, e)
63                    })?,
64            );
65            output.push('\n');
66        }
67
68        Ok(output)
69    }
70
71    fn generate_entities(
72        &self,
73        schema: &ParsedSchema,
74        config: &Config,
75    ) -> anyhow::Result<HashMap<String, String>> {
76        let mut entities = HashMap::new();
77
78        // Handle empty schemas gracefully
79        if schema.types.is_empty() && schema.enums.is_empty() {
80            return Ok(entities);
81        }
82
83        // Generate entities for Object types (not interfaces or unions)
84        for (type_name, parsed_type) in &schema.types {
85            if matches!(parsed_type.kind, crate::parser::TypeKind::Object) {
86                let entity_code = self
87                    .generate_entity_struct(type_name, parsed_type, config)
88                    .map_err(|e| {
89                        anyhow::anyhow!(
90                            "Failed to generate entity struct for type '{}': {}",
91                            type_name,
92                            e
93                        )
94                    })?;
95                entities.insert(format!("{}.rs", to_snake_case(type_name)), entity_code);
96            }
97        }
98
99        // Generate enums
100        for (enum_name, parsed_enum) in &schema.enums {
101            let enum_code = self
102                .generate_enum_type(enum_name, parsed_enum)
103                .map_err(|e| {
104                    anyhow::anyhow!("Failed to generate enum type '{}': {}", enum_name, e)
105                })?;
106            entities.insert(format!("{}.rs", to_snake_case(enum_name)), enum_code);
107        }
108
109        Ok(entities)
110    }
111
112    fn generate_migrations(
113        &self,
114        schema: &ParsedSchema,
115        config: &Config,
116    ) -> anyhow::Result<Vec<MigrationFile>> {
117        let mut migrations = Vec::new();
118
119        // Handle empty schemas gracefully
120        if schema.types.is_empty() && schema.enums.is_empty() {
121            return Ok(migrations);
122        }
123
124        // Generate migrations for Object types (not interfaces or unions)
125        for (type_name, parsed_type) in &schema.types {
126            if matches!(parsed_type.kind, crate::parser::TypeKind::Object) {
127                let migration = self
128                    .generate_table_migration(type_name, parsed_type, config)
129                    .map_err(|e| {
130                        anyhow::anyhow!(
131                            "Failed to generate migration for type '{}': {}",
132                            type_name,
133                            e
134                        )
135                    })?;
136                migrations.push(migration);
137            }
138        }
139
140        Ok(migrations)
141    }
142}
143
144impl DieselGenerator {
145    fn generate_table_macro(
146        &self,
147        type_name: &str,
148        parsed_type: &ParsedType,
149        config: &Config,
150    ) -> anyhow::Result<String> {
151        let table_name = to_snake_case(type_name);
152        let mut output = format!("table! {{\n    {} (", table_name);
153
154        // Primary key - assume first field named 'id' or add one
155        let id_field = parsed_type
156            .fields
157            .iter()
158            .find(|f| f.name == "id")
159            .or_else(|| parsed_type.fields.first());
160
161        if let Some(id_field) = id_field {
162            output.push_str(&format!("{}\n    ) {{\n", id_field.name));
163        } else {
164            output.push_str("id\n    ) {\n");
165        }
166
167        // Generate columns
168        for field in &parsed_type.fields {
169            let column_name = to_snake_case(&field.name);
170            let column_type =
171                diesel_column_type_for_field(field, &config.db, &config.type_mappings);
172
173            let nullable = if field.is_nullable { "" } else { ".not_null()" };
174            output.push_str(&format!(
175                "        {} -> {}{},\n",
176                column_name, column_type, nullable
177            ));
178        }
179
180        output.push_str("    }\n}\n");
181        Ok(output)
182    }
183
184    fn generate_entity_struct(
185        &self,
186        type_name: &str,
187        parsed_type: &ParsedType,
188        config: &Config,
189    ) -> anyhow::Result<String> {
190        let struct_name = type_name.to_string();
191        let table_name = to_snake_case(type_name);
192
193        let mut output = String::new();
194
195        // Add imports
196        output.push_str("#[macro_use]\nextern crate diesel;\n\n");
197        output.push_str("use diesel::prelude::*;\n");
198        output.push_str(&format!("use super::{}::*;\n\n", table_name));
199
200        // Generate the struct
201        output.push_str("#[derive(Queryable, Debug)]\n");
202        output.push_str(&format!("pub struct {} {{\n", struct_name));
203
204        for field in &parsed_type.fields {
205            let field_name = to_snake_case(&field.name);
206            let field_type = rust_type_for_field(field, &config.db, &config.type_mappings);
207            output.push_str(&format!("    pub {}: {},\n", field_name, field_type));
208        }
209
210        output.push_str("}\n\n");
211
212        // Generate Insertable struct
213        output.push_str("#[derive(Insertable)]\n");
214        output.push_str(&format!("#[table_name = \"{}\"]\n", table_name));
215        output.push_str(&format!("pub struct New{} {{\n", struct_name));
216
217        for field in &parsed_type.fields {
218            if field.name != "id" {
219                // Skip id for inserts
220                let field_name = to_snake_case(&field.name);
221                let field_type = rust_type_for_field(field, &config.db, &config.type_mappings);
222                output.push_str(&format!("    pub {}: {},\n", field_name, field_type));
223            }
224        }
225
226        output.push_str("}\n\n");
227
228        // Generate relationships based on detected foreign keys
229        // For now, we'll add a comment about potential relationships
230        // Full relationship generation would require schema-wide analysis
231        output.push_str("// TODO: Generate joinable! macros for relationships\n");
232
233        Ok(output)
234    }
235
236    fn generate_enum_type(
237        &self,
238        enum_name: &str,
239        parsed_enum: &ParsedEnum,
240    ) -> anyhow::Result<String> {
241        let mut output = String::new();
242
243        if let Some(description) = &parsed_enum.description {
244            output.push_str(&format!("/// {}\n", description));
245        }
246
247        output.push_str("#[derive(Debug, Clone, PartialEq, Eq, Hash)]\n");
248        output.push_str("#[derive(diesel::deserialize::FromSqlRow, diesel::serialize::ToSql)]\n");
249        output.push_str("#[sql_type = \"diesel::sql_types::Text\"]\n");
250        output.push_str(&format!("pub enum {} {{\n", enum_name));
251
252        for value in &parsed_enum.values {
253            output.push_str(&format!("    {},\n", value));
254        }
255
256        output.push_str("}\n");
257
258        Ok(output)
259    }
260
261    fn generate_table_migration(
262        &self,
263        type_name: &str,
264        parsed_type: &ParsedType,
265        config: &Config,
266    ) -> anyhow::Result<MigrationFile> {
267        let table_name = to_snake_case(type_name);
268        let migration_name = format!("create_{}_table", table_name);
269
270        let mut up_sql = format!("CREATE TABLE {} (\n", table_name);
271
272        let mut columns = Vec::new();
273
274        // Add id column if not present
275        let has_id = parsed_type.fields.iter().any(|f| f.name == "id");
276        if !has_id {
277            let id_type = match config.db {
278                DatabaseType::Sqlite => "INTEGER PRIMARY KEY AUTOINCREMENT",
279                DatabaseType::Postgres => "UUID PRIMARY KEY DEFAULT gen_random_uuid()",
280                DatabaseType::Mysql => "INT UNSIGNED PRIMARY KEY AUTO_INCREMENT",
281            };
282            columns.push(format!("    id {}", id_type));
283        }
284
285        for field in &parsed_type.fields {
286            let column_name = to_snake_case(&field.name);
287            let sql_type = sql_type_for_field(field, &config.db, &config.type_mappings);
288
289            let nullable = if field.is_nullable { "" } else { " NOT NULL" };
290            let primary_key = if field.name == "id" {
291                " PRIMARY KEY"
292            } else {
293                ""
294            };
295
296            columns.push(format!(
297                "    {} {}{}{}",
298                column_name, sql_type, nullable, primary_key
299            ));
300        }
301
302        up_sql.push_str(&columns.join(",\n"));
303        up_sql.push_str("\n);");
304
305        // Add indexes for foreign keys (simplified)
306        for field in &parsed_type.fields {
307            if let crate::parser::FieldType::Reference(_) = &field.field_type {
308                let column_name = to_snake_case(&field.name);
309                up_sql.push_str(&format!(
310                    "\n\nCREATE INDEX idx_{}_{} ON {} ({});",
311                    table_name, column_name, table_name, column_name
312                ));
313            }
314        }
315
316        let down_sql = format!("DROP TABLE {};", table_name);
317
318        Ok(MigrationFile {
319            name: migration_name,
320            up_sql,
321            down_sql,
322        })
323    }
324}