graphql_codegen_rust/generator/
diesel.rs1use 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 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 output.push_str("use diesel::prelude::*;\n\n");
36
37 for (type_name, parsed_type) in &schema.types {
39 if !matches!(parsed_type.kind, crate::parser::TypeKind::Object) {
40 continue; }
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 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 if schema.types.is_empty() && schema.enums.is_empty() {
80 return Ok(entities);
81 }
82
83 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 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 if schema.types.is_empty() && schema.enums.is_empty() {
121 return Ok(migrations);
122 }
123
124 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 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 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 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 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 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 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 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 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 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}