Skip to main content

forge_core/schema/
registry.rs

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