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 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 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 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 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 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#[allow(dead_code)]
178pub fn is_foreign_key_field(field: &ParsedField) -> Option<String> {
179 let field_name = &field.name;
180
181 if field_name.ends_with("Id") && field_name.len() > 2 {
183 let related_type_base = &field_name[..field_name.len() - 2];
185 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 return None;
199 }
200
201 None
202}
203
204#[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 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}