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    /// Documentation comment.
24    pub doc: Option<String>,
25}
26
27impl FieldDef {
28    /// Create a new field definition.
29    pub fn new(name: &str, rust_type: RustType) -> Self {
30        let sql_type = rust_type.to_sql_type();
31        let nullable = rust_type.is_nullable();
32        let column_name = to_snake_case(name);
33
34        Self {
35            name: name.to_string(),
36            column_name,
37            rust_type,
38            sql_type,
39            nullable,
40            doc: None,
41        }
42    }
43
44    /// Generate TypeScript field.
45    pub fn to_typescript(&self) -> String {
46        let ts_type = self.rust_type.to_typescript();
47        let optional = if self.nullable { "?" } else { "" };
48        format!("  {}{}: {};", to_camel_case(&self.name), optional, ts_type)
49    }
50}
51
52/// Convert a string to snake_case.
53fn to_snake_case(s: &str) -> String {
54    let mut result = String::new();
55    for (i, c) in s.chars().enumerate() {
56        if c.is_uppercase() {
57            if i > 0 {
58                result.push('_');
59            }
60            result.push(c.to_lowercase().next().unwrap());
61        } else {
62            result.push(c);
63        }
64    }
65    result
66}
67
68/// Convert a string to camelCase.
69fn to_camel_case(s: &str) -> String {
70    let mut result = String::new();
71    let mut capitalize_next = false;
72    for c in s.chars() {
73        if c == '_' {
74            capitalize_next = true;
75        } else if capitalize_next {
76            result.push(c.to_uppercase().next().unwrap());
77            capitalize_next = false;
78        } else {
79            result.push(c);
80        }
81    }
82    result
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_field_def_basic() {
91        let field = FieldDef::new("email", RustType::String);
92        assert_eq!(field.name, "email");
93        assert_eq!(field.column_name, "email");
94        assert!(!field.nullable);
95    }
96
97    #[test]
98    fn test_field_def_nullable() {
99        let field = FieldDef::new("avatar_url", RustType::Option(Box::new(RustType::String)));
100        assert!(field.nullable);
101    }
102
103    #[test]
104    fn test_to_snake_case() {
105        assert_eq!(to_snake_case("createdAt"), "created_at");
106        assert_eq!(to_snake_case("userId"), "user_id");
107        assert_eq!(to_snake_case("HTTPServer"), "h_t_t_p_server");
108    }
109
110    #[test]
111    fn test_to_camel_case() {
112        assert_eq!(to_camel_case("created_at"), "createdAt");
113        assert_eq!(to_camel_case("user_id"), "userId");
114    }
115}