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