forge_core/schema/
field.rs

1use serde::{Deserialize, Serialize};
2
3use super::types::{RustType, SqlType};
4
5/// Definition of a model field.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct FieldDef {
8    /// Field name in Rust (snake_case).
9    pub name: String,
10
11    /// Column name in SQL (may differ from field name).
12    pub column_name: String,
13
14    /// Rust type.
15    pub rust_type: RustType,
16
17    /// SQL type.
18    pub sql_type: SqlType,
19
20    /// Whether the field is nullable.
21    pub nullable: bool,
22
23    /// Field attributes.
24    pub attributes: Vec<FieldAttribute>,
25
26    /// Default value expression (SQL).
27    pub default: Option<String>,
28
29    /// Documentation comment.
30    pub doc: Option<String>,
31}
32
33impl FieldDef {
34    /// Create a new field definition.
35    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    /// Check if this field is a primary key.
53    pub fn is_primary_key(&self) -> bool {
54        self.attributes
55            .iter()
56            .any(|a| matches!(a, FieldAttribute::Id | FieldAttribute::IdAuto))
57    }
58
59    /// Check if this field is indexed.
60    pub fn is_indexed(&self) -> bool {
61        self.attributes
62            .iter()
63            .any(|a| matches!(a, FieldAttribute::Indexed))
64    }
65
66    /// Check if this field is unique.
67    pub fn is_unique(&self) -> bool {
68        self.attributes
69            .iter()
70            .any(|a| matches!(a, FieldAttribute::Unique))
71    }
72
73    /// Check if this field is encrypted.
74    pub fn is_encrypted(&self) -> bool {
75        self.attributes
76            .iter()
77            .any(|a| matches!(a, FieldAttribute::Encrypted))
78    }
79
80    /// Check if this field auto-updates on modification.
81    pub fn is_updated_at(&self) -> bool {
82        self.attributes
83            .iter()
84            .any(|a| matches!(a, FieldAttribute::UpdatedAt))
85    }
86
87    /// Generate SQL column definition.
88    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    /// Generate TypeScript field.
113    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/// Field type representation for simpler cases.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122pub enum FieldType {
123    Scalar,
124    Relation,
125    Computed,
126}
127
128/// Field attributes applied via proc macros.
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub enum FieldAttribute {
131    /// Primary key (UUID).
132    Id,
133    /// Auto-incrementing primary key.
134    IdAuto,
135    /// Create an index on this field.
136    Indexed,
137    /// Unique constraint.
138    Unique,
139    /// Encrypt at rest.
140    Encrypted,
141    /// Store as JSONB.
142    Jsonb,
143    /// Auto-update on modification.
144    UpdatedAt,
145    /// Maximum length for strings.
146    MaxLength(u32),
147    /// Foreign key relation.
148    BelongsTo(String),
149    /// One-to-many relation.
150    HasMany(String),
151    /// One-to-one relation.
152    HasOne(String),
153    /// Many-to-many relation with join table.
154    ManyToMany { target: String, through: String },
155}
156
157/// Convert a string to snake_case.
158fn 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
173/// Convert a string to camelCase.
174fn 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}