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> {
83 let mut schema = Schema::new();
84 let mut current_table: Option<TableDef> = None;
85
86 for line in input.lines() {
87 let line = line.trim();
88
89 if line.is_empty() || line.starts_with("--") {
91 continue;
92 }
93
94 if let Some(rest) = line.strip_prefix("table ") {
96 if let Some(t) = current_table.take() {
98 schema.tables.push(t);
99 }
100
101 let name = rest
103 .split('(')
104 .next()
105 .map(|s| s.trim())
106 .ok_or_else(|| format!("Invalid table line: {}", line))?;
107
108 current_table = Some(TableDef::new(name));
109 }
110 else if line == ")" {
112 if let Some(t) = current_table.take() {
113 schema.tables.push(t);
114 }
115 }
116 else if let Some(ref mut table) = current_table {
118 let line = line.trim_end_matches(',');
120
121 let parts: Vec<&str> = line.split_whitespace().collect();
122 if parts.len() >= 2 {
123 let col_name = parts[0];
124 let col_type = parts[1];
125 let not_null = parts.len() > 2
126 && parts.iter().any(|&p| p.eq_ignore_ascii_case("not"))
127 && parts.iter().any(|&p| p.eq_ignore_ascii_case("null"));
128
129 table.columns.push(ColumnDef {
130 name: col_name.to_string(),
131 typ: col_type.to_string(),
132 nullable: !not_null,
133 primary_key: false,
134 });
135 }
136 }
137 }
138
139 if let Some(t) = current_table {
141 schema.tables.push(t);
142 }
143
144 Ok(schema)
145 }
146
147 pub fn from_file(path: &std::path::Path) -> Result<Self, String> {
149 let content = std::fs::read_to_string(path)
150 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
151
152 if path.extension().map(|e| e == "json").unwrap_or(false) {
154 Self::from_json(&content).map_err(|e| e.to_string())
155 } else {
156 Self::from_qail_schema(&content)
157 }
158 }
159}
160
161impl Default for Schema {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl TableDef {
168 pub fn new(name: &str) -> Self {
170 Self {
171 name: name.to_string(),
172 columns: Vec::new(),
173 }
174 }
175
176 pub fn add_column(&mut self, col: ColumnDef) {
178 self.columns.push(col);
179 }
180
181 pub fn column(mut self, name: &str, typ: &str) -> Self {
183 self.columns.push(ColumnDef {
184 name: name.to_string(),
185 typ: typ.to_string(),
186 nullable: true,
187 primary_key: false,
188 });
189 self
190 }
191
192 pub fn pk(mut self, name: &str, typ: &str) -> Self {
194 self.columns.push(ColumnDef {
195 name: name.to_string(),
196 typ: typ.to_string(),
197 nullable: false,
198 primary_key: true,
199 });
200 self
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_schema_from_json() {
210 let json = r#"{
211 "tables": [{
212 "name": "users",
213 "columns": [
214 { "name": "id", "type": "uuid", "nullable": false, "primary_key": true },
215 { "name": "email", "type": "varchar", "nullable": false }
216 ]
217 }]
218 }"#;
219
220 let schema = Schema::from_json(json).unwrap();
221 assert_eq!(schema.tables.len(), 1);
222 assert_eq!(schema.tables[0].name, "users");
223 assert_eq!(schema.tables[0].columns.len(), 2);
224 }
225
226 #[test]
227 fn test_schema_to_validator() {
228 let schema = Schema {
229 tables: vec![
230 TableDef::new("users")
231 .pk("id", "uuid")
232 .column("email", "varchar"),
233 ],
234 };
235
236 let validator = schema.to_validator();
237 assert!(validator.validate_table("users").is_ok());
238 assert!(validator.validate_column("users", "id").is_ok());
239 assert!(validator.validate_column("users", "email").is_ok());
240 }
241
242 #[test]
243 fn test_table_builder() {
244 let table = TableDef::new("orders")
245 .pk("id", "uuid")
246 .column("total", "decimal")
247 .column("status", "varchar");
248
249 assert_eq!(table.columns.len(), 3);
250 assert!(table.columns[0].primary_key);
251 }
252}