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