forge_core/schema/
registry.rs1use std::collections::BTreeMap;
15use std::sync::RwLock;
16
17use super::function::FunctionDef;
18use super::model::TableDef;
19
20pub struct SchemaRegistry {
24 tables: RwLock<BTreeMap<String, TableDef>>,
26
27 enums: RwLock<BTreeMap<String, EnumDef>>,
29
30 functions: RwLock<BTreeMap<String, FunctionDef>>,
32}
33
34impl SchemaRegistry {
35 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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
132pub struct EnumDef {
133 pub name: String,
135
136 pub sql_name: String,
138
139 pub variants: Vec<EnumVariant>,
141
142 pub doc: Option<String>,
144}
145
146impl EnumDef {
147 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 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 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#[derive(Debug, Clone)]
186pub struct EnumVariant {
187 pub name: String,
189
190 pub sql_value: String,
192
193 pub int_value: Option<i32>,
195
196 pub doc: Option<String>,
198}
199
200impl EnumVariant {
201 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
212fn 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}