forge_core/schema/
registry.rs

1use std::collections::HashMap;
2use std::sync::RwLock;
3
4use super::function::FunctionDef;
5use super::model::TableDef;
6
7/// Global registry of all schema definitions.
8/// This is populated at compile time by the proc macros.
9pub struct SchemaRegistry {
10    /// All registered tables by name.
11    tables: RwLock<HashMap<String, TableDef>>,
12
13    /// All registered enums by name.
14    enums: RwLock<HashMap<String, EnumDef>>,
15
16    /// All registered functions by name.
17    functions: RwLock<HashMap<String, FunctionDef>>,
18}
19
20impl SchemaRegistry {
21    /// Create a new empty registry.
22    pub fn new() -> Self {
23        Self {
24            tables: RwLock::new(HashMap::new()),
25            enums: RwLock::new(HashMap::new()),
26            functions: RwLock::new(HashMap::new()),
27        }
28    }
29
30    /// Register a table definition.
31    pub fn register_table(&self, table: TableDef) {
32        let mut tables = self.tables.write().unwrap();
33        tables.insert(table.name.clone(), table);
34    }
35
36    /// Register an enum definition.
37    pub fn register_enum(&self, enum_def: EnumDef) {
38        let mut enums = self.enums.write().unwrap();
39        enums.insert(enum_def.name.clone(), enum_def);
40    }
41
42    /// Register a function definition.
43    pub fn register_function(&self, func: FunctionDef) {
44        let mut functions = self.functions.write().unwrap();
45        functions.insert(func.name.clone(), func);
46    }
47
48    /// Get a table by name.
49    pub fn get_table(&self, name: &str) -> Option<TableDef> {
50        let tables = self.tables.read().unwrap();
51        tables.get(name).cloned()
52    }
53
54    /// Get an enum by name.
55    pub fn get_enum(&self, name: &str) -> Option<EnumDef> {
56        let enums = self.enums.read().unwrap();
57        enums.get(name).cloned()
58    }
59
60    /// Get a function by name.
61    pub fn get_function(&self, name: &str) -> Option<FunctionDef> {
62        let functions = self.functions.read().unwrap();
63        functions.get(name).cloned()
64    }
65
66    /// Get all registered tables.
67    pub fn all_tables(&self) -> Vec<TableDef> {
68        let tables = self.tables.read().unwrap();
69        tables.values().cloned().collect()
70    }
71
72    /// Get all registered enums.
73    pub fn all_enums(&self) -> Vec<EnumDef> {
74        let enums = self.enums.read().unwrap();
75        enums.values().cloned().collect()
76    }
77
78    /// Get all registered functions.
79    pub fn all_functions(&self) -> Vec<FunctionDef> {
80        let functions = self.functions.read().unwrap();
81        functions.values().cloned().collect()
82    }
83
84    /// Clear all registrations (useful for testing).
85    pub fn clear(&self) {
86        self.tables.write().unwrap().clear();
87        self.enums.write().unwrap().clear();
88        self.functions.write().unwrap().clear();
89    }
90}
91
92impl Default for SchemaRegistry {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98/// Enum type definition.
99#[derive(Debug, Clone)]
100pub struct EnumDef {
101    /// Enum name in Rust.
102    pub name: String,
103
104    /// Type name in SQL (lowercase).
105    pub sql_name: String,
106
107    /// Enum variants.
108    pub variants: Vec<EnumVariant>,
109
110    /// Documentation comment.
111    pub doc: Option<String>,
112}
113
114impl EnumDef {
115    /// Create a new enum definition.
116    pub fn new(name: &str) -> Self {
117        Self {
118            name: name.to_string(),
119            sql_name: to_snake_case(name),
120            variants: Vec::new(),
121            doc: None,
122        }
123    }
124
125    /// Generate CREATE TYPE SQL.
126    pub fn to_create_type_sql(&self) -> String {
127        let values: Vec<String> = self
128            .variants
129            .iter()
130            .map(|v| format!("'{}'", v.sql_value))
131            .collect();
132
133        format!(
134            "CREATE TYPE {} AS ENUM (\n    {}\n);",
135            self.sql_name,
136            values.join(",\n    ")
137        )
138    }
139
140    /// Generate TypeScript union type.
141    pub fn to_typescript(&self) -> String {
142        let values: Vec<String> = self
143            .variants
144            .iter()
145            .map(|v| format!("'{}'", v.sql_value))
146            .collect();
147
148        format!("export type {} = {};", self.name, values.join(" | "))
149    }
150}
151
152/// Enum variant definition.
153#[derive(Debug, Clone)]
154pub struct EnumVariant {
155    /// Variant name in Rust.
156    pub name: String,
157
158    /// Value in SQL (lowercase).
159    pub sql_value: String,
160
161    /// Optional integer value.
162    pub int_value: Option<i32>,
163
164    /// Documentation comment.
165    pub doc: Option<String>,
166}
167
168impl EnumVariant {
169    /// Create a new variant.
170    pub fn new(name: &str) -> Self {
171        Self {
172            name: name.to_string(),
173            sql_value: to_snake_case(name),
174            int_value: None,
175            doc: None,
176        }
177    }
178}
179
180/// Convert a string to snake_case.
181fn to_snake_case(s: &str) -> String {
182    let mut result = String::new();
183    for (i, c) in s.chars().enumerate() {
184        if c.is_uppercase() {
185            if i > 0 {
186                result.push('_');
187            }
188            result.push(c.to_lowercase().next().unwrap());
189        } else {
190            result.push(c);
191        }
192    }
193    result
194}
195
196/// Global schema registry instance.
197/// Models register themselves here when their constructors are called.
198#[allow(dead_code)]
199static GLOBAL_REGISTRY: std::sync::LazyLock<SchemaRegistry> =
200    std::sync::LazyLock::new(SchemaRegistry::new);
201
202/// Get the global schema registry.
203#[allow(dead_code)]
204pub fn global_registry() -> &'static SchemaRegistry {
205    &GLOBAL_REGISTRY
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::schema::field::FieldDef;
212    use crate::schema::model::TableDef;
213    use crate::schema::types::RustType;
214
215    #[test]
216    fn test_registry_basic() {
217        let registry = SchemaRegistry::new();
218
219        let mut table = TableDef::new("users", "User");
220        table.fields.push(FieldDef::new("id", RustType::Uuid));
221
222        registry.register_table(table.clone());
223
224        let retrieved = registry.get_table("users").unwrap();
225        assert_eq!(retrieved.name, "users");
226        assert_eq!(retrieved.struct_name, "User");
227    }
228
229    #[test]
230    fn test_enum_def() {
231        let mut enum_def = EnumDef::new("ProjectStatus");
232        enum_def.variants.push(EnumVariant::new("Draft"));
233        enum_def.variants.push(EnumVariant::new("Active"));
234        enum_def.variants.push(EnumVariant::new("Completed"));
235
236        let sql = enum_def.to_create_type_sql();
237        assert!(sql.contains("CREATE TYPE project_status AS ENUM"));
238        assert!(sql.contains("'draft'"));
239        assert!(sql.contains("'active'"));
240
241        let ts = enum_def.to_typescript();
242        assert!(ts.contains("export type ProjectStatus"));
243        assert!(ts.contains("'draft'"));
244    }
245}