1use crate::validator::Validator;
24use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Schema {
28 pub tables: Vec<TableDef>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TableDef {
33 pub name: String,
34 pub columns: Vec<ColumnDef>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ColumnDef {
39 pub name: String,
40 #[serde(rename = "type", alias = "typ")]
41 pub typ: String,
42 #[serde(default)]
43 pub nullable: bool,
44 #[serde(default)]
45 pub primary_key: bool,
46}
47
48impl Schema {
49 pub fn new() -> Self {
51 Self { tables: Vec::new() }
52 }
53
54 pub fn add_table(&mut self, table: TableDef) {
56 self.tables.push(table);
57 }
58
59 pub fn to_validator(&self) -> Validator {
61 let mut v = Validator::new();
62 for table in &self.tables {
63 let cols: Vec<&str> = table.columns.iter().map(|c| c.name.as_str()).collect();
64 v.add_table(&table.name, &cols);
65 }
66 v
67 }
68
69 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
71 serde_json::from_str(json)
72 }
73
74 pub fn from_qail_schema(input: &str) -> Result<Self, String> {
84 let mut schema = Schema::new();
85 let mut current_table: Option<TableDef> = None;
86
87 for line in input.lines() {
88 let line = line.trim();
89
90 if line.is_empty() || line.starts_with("--") {
92 continue;
93 }
94
95 if let Some(rest) = line.strip_prefix("table ") {
97 if let Some(t) = current_table.take() {
99 schema.tables.push(t);
100 }
101
102 let name = rest
104 .split('(')
105 .next()
106 .map(|s| s.trim())
107 .ok_or_else(|| format!("Invalid table line: {}", line))?;
108
109 current_table = Some(TableDef::new(name));
110 }
111 else if line == ")" {
113 if let Some(t) = current_table.take() {
114 schema.tables.push(t);
115 }
116 }
117 else if let Some(ref mut table) = current_table {
119 let line = line.trim_end_matches(',');
121
122 let parts: Vec<&str> = line.split_whitespace().collect();
123 if parts.len() >= 2 {
124 let col_name = parts[0];
125 let col_type = parts[1];
126 let not_null = parts.len() > 2
127 && parts.iter().any(|&p| p.eq_ignore_ascii_case("not"))
128 && parts.iter().any(|&p| p.eq_ignore_ascii_case("null"));
129
130 table.columns.push(ColumnDef {
131 name: col_name.to_string(),
132 typ: col_type.to_string(),
133 nullable: !not_null,
134 primary_key: false,
135 });
136 }
137 }
138 }
139
140 if let Some(t) = current_table {
142 schema.tables.push(t);
143 }
144
145 Ok(schema)
146 }
147
148 pub fn from_file(path: &std::path::Path) -> Result<Self, String> {
150 let content = std::fs::read_to_string(path)
151 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
152
153 if path.extension().map(|e| e == "json").unwrap_or(false) {
155 Self::from_json(&content).map_err(|e| e.to_string())
156 } else {
157 Self::from_qail_schema(&content)
158 }
159 }
160}
161
162impl Default for Schema {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168impl TableDef {
169 pub fn new(name: &str) -> Self {
171 Self {
172 name: name.to_string(),
173 columns: Vec::new(),
174 }
175 }
176
177 pub fn add_column(&mut self, col: ColumnDef) {
179 self.columns.push(col);
180 }
181
182 pub fn column(mut self, name: &str, typ: &str) -> Self {
184 self.columns.push(ColumnDef {
185 name: name.to_string(),
186 typ: typ.to_string(),
187 nullable: true,
188 primary_key: false,
189 });
190 self
191 }
192
193 pub fn pk(mut self, name: &str, typ: &str) -> Self {
195 self.columns.push(ColumnDef {
196 name: name.to_string(),
197 typ: typ.to_string(),
198 nullable: false,
199 primary_key: true,
200 });
201 self
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_schema_from_json() {
211 let json = r#"{
212 "tables": [{
213 "name": "users",
214 "columns": [
215 { "name": "id", "type": "uuid", "nullable": false, "primary_key": true },
216 { "name": "email", "type": "varchar", "nullable": false }
217 ]
218 }]
219 }"#;
220
221 let schema = Schema::from_json(json).unwrap();
222 assert_eq!(schema.tables.len(), 1);
223 assert_eq!(schema.tables[0].name, "users");
224 assert_eq!(schema.tables[0].columns.len(), 2);
225 }
226
227 #[test]
228 fn test_schema_to_validator() {
229 let schema = Schema {
230 tables: vec![
231 TableDef::new("users")
232 .pk("id", "uuid")
233 .column("email", "varchar"),
234 ],
235 };
236
237 let validator = schema.to_validator();
238 assert!(validator.validate_table("users").is_ok());
239 assert!(validator.validate_column("users", "id").is_ok());
240 assert!(validator.validate_column("users", "email").is_ok());
241 }
242
243 #[test]
244 fn test_table_builder() {
245 let table = TableDef::new("orders")
246 .pk("id", "uuid")
247 .column("total", "decimal")
248 .column("status", "varchar");
249
250 assert_eq!(table.columns.len(), 3);
251 assert!(table.columns[0].primary_key);
252 }
253}