forge_core/schema/
field.rs1use serde::{Deserialize, Serialize};
2
3use super::types::{RustType, SqlType};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct FieldDef {
8 pub name: String,
10
11 pub column_name: String,
13
14 pub rust_type: RustType,
16
17 pub sql_type: SqlType,
19
20 pub nullable: bool,
22
23 pub attributes: Vec<FieldAttribute>,
25
26 pub default: Option<String>,
28
29 pub doc: Option<String>,
31}
32
33impl FieldDef {
34 pub fn new(name: &str, rust_type: RustType) -> Self {
36 let sql_type = rust_type.to_sql_type();
37 let nullable = rust_type.is_nullable();
38 let column_name = to_snake_case(name);
39
40 Self {
41 name: name.to_string(),
42 column_name,
43 rust_type,
44 sql_type,
45 nullable,
46 attributes: Vec::new(),
47 default: None,
48 doc: None,
49 }
50 }
51
52 pub fn is_primary_key(&self) -> bool {
54 self.attributes
55 .iter()
56 .any(|a| matches!(a, FieldAttribute::Id | FieldAttribute::IdAuto))
57 }
58
59 pub fn is_indexed(&self) -> bool {
61 self.attributes
62 .iter()
63 .any(|a| matches!(a, FieldAttribute::Indexed))
64 }
65
66 pub fn is_unique(&self) -> bool {
68 self.attributes
69 .iter()
70 .any(|a| matches!(a, FieldAttribute::Unique))
71 }
72
73 pub fn is_encrypted(&self) -> bool {
75 self.attributes
76 .iter()
77 .any(|a| matches!(a, FieldAttribute::Encrypted))
78 }
79
80 pub fn is_updated_at(&self) -> bool {
82 self.attributes
83 .iter()
84 .any(|a| matches!(a, FieldAttribute::UpdatedAt))
85 }
86
87 pub fn to_sql_column(&self) -> String {
89 let mut parts = vec![self.column_name.clone(), self.sql_type.to_sql()];
90
91 if self.is_primary_key() {
92 parts.push("PRIMARY KEY".to_string());
93 }
94
95 if !self.nullable && !self.is_primary_key() {
96 parts.push("NOT NULL".to_string());
97 }
98
99 if self.is_unique() && !self.is_primary_key() {
100 parts.push("UNIQUE".to_string());
101 }
102
103 if let Some(ref default) = self.default {
104 parts.push(format!("DEFAULT {}", default));
105 } else if self.is_primary_key() && matches!(self.sql_type, SqlType::Uuid) {
106 parts.push("DEFAULT gen_random_uuid()".to_string());
107 }
108
109 parts.join(" ")
110 }
111
112 pub fn to_typescript(&self) -> String {
114 let ts_type = self.rust_type.to_typescript();
115 let optional = if self.nullable { "?" } else { "" };
116 format!(" {}{}: {};", to_camel_case(&self.name), optional, ts_type)
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122pub enum FieldType {
123 Scalar,
124 Relation,
125 Computed,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub enum FieldAttribute {
131 Id,
133 IdAuto,
135 Indexed,
137 Unique,
139 Encrypted,
141 Jsonb,
143 UpdatedAt,
145 MaxLength(u32),
147 BelongsTo(String),
149 HasMany(String),
151 HasOne(String),
153 ManyToMany { target: String, through: String },
155}
156
157fn to_snake_case(s: &str) -> String {
159 let mut result = String::new();
160 for (i, c) in s.chars().enumerate() {
161 if c.is_uppercase() {
162 if i > 0 {
163 result.push('_');
164 }
165 result.push(c.to_lowercase().next().unwrap());
166 } else {
167 result.push(c);
168 }
169 }
170 result
171}
172
173fn to_camel_case(s: &str) -> String {
175 let mut result = String::new();
176 let mut capitalize_next = false;
177 for c in s.chars() {
178 if c == '_' {
179 capitalize_next = true;
180 } else if capitalize_next {
181 result.push(c.to_uppercase().next().unwrap());
182 capitalize_next = false;
183 } else {
184 result.push(c);
185 }
186 }
187 result
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_field_def_basic() {
196 let field = FieldDef::new("email", RustType::String);
197 assert_eq!(field.name, "email");
198 assert_eq!(field.column_name, "email");
199 assert!(!field.nullable);
200 }
201
202 #[test]
203 fn test_field_def_nullable() {
204 let field = FieldDef::new("avatar_url", RustType::Option(Box::new(RustType::String)));
205 assert!(field.nullable);
206 }
207
208 #[test]
209 fn test_field_to_sql_column() {
210 let mut field = FieldDef::new("id", RustType::Uuid);
211 field.attributes.push(FieldAttribute::Id);
212 let sql = field.to_sql_column();
213 assert!(sql.contains("PRIMARY KEY"));
214 assert!(sql.contains("DEFAULT gen_random_uuid()"));
215 }
216
217 #[test]
218 fn test_to_snake_case() {
219 assert_eq!(to_snake_case("createdAt"), "created_at");
220 assert_eq!(to_snake_case("userId"), "user_id");
221 assert_eq!(to_snake_case("HTTPServer"), "h_t_t_p_server");
222 }
223
224 #[test]
225 fn test_to_camel_case() {
226 assert_eq!(to_camel_case("created_at"), "createdAt");
227 assert_eq!(to_camel_case("user_id"), "userId");
228 }
229}