Skip to main content

forge_core/schema/
registry.rs

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