1use regex::Regex;
26use std::collections::HashMap;
27use std::fs;
28use std::path::Path;
29use thiserror::Error;
30
31#[derive(Error, Debug)]
32pub enum CsvError {
33 #[error("I/O error: {0}")]
34 Io(#[from] std::io::Error),
35
36 #[error("CSV parsing error: {0}")]
37 Csv(#[from] csv::Error),
38
39 #[error("Column not found: {0}")]
40 ColumnNotFound(String),
41
42 #[error("Invalid WHERE clause: {0}")]
43 InvalidWhereClause(String),
44
45 #[error("Invalid SELECT clause: {0}")]
46 InvalidSelectClause(String),
47
48 #[error("Empty dataset")]
49 EmptyDataset,
50}
51
52pub type SsotError = CsvError;
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Dataset {
56 headers: Vec<String>,
57 rows: Vec<Vec<String>>,
58}
59
60impl Dataset {
61 pub fn new(headers: Vec<String>, rows: Vec<Vec<String>>) -> Self {
62 Self { headers, rows }
63 }
64
65 pub fn headers(&self) -> &[String] {
66 &self.headers
67 }
68
69 pub fn row_count(&self) -> usize {
70 self.rows.len()
71 }
72
73 pub fn column_count(&self) -> usize {
74 self.headers.len()
75 }
76
77 pub fn get_row(&self, index: usize) -> Option<&[String]> {
78 self.rows.get(index).map(|r| r.as_slice())
79 }
80
81 pub fn rows(&self) -> &[Vec<String>] {
82 &self.rows
83 }
84
85 pub fn from_csv(content: &str) -> Result<Self, CsvError> {
86 let clean_content: Vec<&str> = content
87 .lines()
88 .map(|l| l.trim())
89 .filter(|l| !l.is_empty() && !l.starts_with("%%") && !l.starts_with('#'))
90 .collect();
91
92 if clean_content.is_empty() {
93 return Err(CsvError::EmptyDataset);
94 }
95
96 let first_line = clean_content[0];
97 let delimiter = if first_line.contains('\t') {
98 b'\t'
99 } else if first_line.contains(';') && !first_line.contains(',') {
100 b';'
101 } else {
102 b','
103 };
104
105 let joined_body = clean_content.join("\n");
106 let mut reader = csv::ReaderBuilder::new()
107 .delimiter(delimiter)
108 .has_headers(true)
109 .flexible(true)
110 .trim(csv::Trim::All)
111 .from_reader(joined_body.as_bytes());
112
113 let headers = reader
114 .headers()?
115 .iter()
116 .map(|h| h.trim().to_string())
117 .collect::<Vec<_>>();
118
119 let mut rows = Vec::new();
120 for record in reader.records() {
121 let rec = record?;
122 let mut row = Vec::with_capacity(headers.len());
123 for field in rec.iter() {
124 row.push(field.trim().to_string());
125 }
126 while row.len() < headers.len() {
127 row.push(String::new());
128 }
129 rows.push(row);
130 }
131
132 Ok(Self { headers, rows })
133 }
134
135 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, CsvError> {
136 let content = fs::read_to_string(path)?;
137 Self::from_csv(&content)
138 }
139
140 pub fn to_csv(&self) -> Result<String, CsvError> {
141 let mut wtr = csv::WriterBuilder::new().from_writer(vec![]);
142 wtr.write_record(&self.headers)?;
143 for row in &self.rows {
144 wtr.write_record(row)?;
145 }
146 wtr.flush()?;
147 let bytes = wtr.into_inner().map_err(|e| e.into_error())?;
148 Ok(String::from_utf8_lossy(&bytes).to_string())
149 }
150
151 pub fn apply_select(&self, select_expr: &str) -> Result<Self, CsvError> {
152 let trimmed = select_expr.trim();
153 if trimmed.is_empty() || trimmed == "*" {
154 return Ok(self.clone());
155 }
156
157 let mut projected_headers = Vec::new();
158 let mut col_indices = Vec::new();
159
160 let re_alias = Regex::new(r"(?i)^(.+?)\s+as\s+(.+)$").unwrap();
161
162 for item in trimmed.split(',') {
163 let raw_item = item.trim();
164 if raw_item.is_empty() {
165 continue;
166 }
167
168 let (src_name, target_name) = if let Some(caps) = re_alias.captures(raw_item) {
169 let src = caps.get(1).unwrap().as_str().trim();
170 let dst = caps.get(2).unwrap().as_str().trim();
171 (src, dst)
172 } else {
173 (raw_item, raw_item)
174 };
175
176 let idx = find_pedigree_synonym_column(src_name, &self.headers)
177 .ok_or_else(|| CsvError::ColumnNotFound(src_name.to_string()))?;
178
179 col_indices.push(idx);
180 projected_headers.push(target_name.to_string());
181 }
182
183 if projected_headers.is_empty() {
184 return Err(CsvError::InvalidSelectClause(
185 "No columns selected".to_string(),
186 ));
187 }
188
189 let mut projected_rows = Vec::with_capacity(self.rows.len());
190 for row in &self.rows {
191 let mut new_row = Vec::with_capacity(col_indices.len());
192 for &idx in &col_indices {
193 new_row.push(row.get(idx).cloned().unwrap_or_default());
194 }
195 projected_rows.push(new_row);
196 }
197
198 Ok(Self {
199 headers: projected_headers,
200 rows: projected_rows,
201 })
202 }
203
204 pub fn apply_where(&self, where_expr: &str) -> Result<Self, CsvError> {
205 let trimmed = where_expr.trim();
206 if trimmed.is_empty() {
207 return Ok(self.clone());
208 }
209
210 let clauses: Vec<&str> = if trimmed.contains(" and ") {
211 trimmed.split(" and ").collect()
212 } else if trimmed.contains(" AND ") {
213 trimmed.split(" AND ").collect()
214 } else if trimmed.contains("&&") {
215 trimmed.split("&&").collect()
216 } else {
217 vec![trimmed]
218 };
219
220 let parsed_clauses: Result<Vec<Condition>, CsvError> = clauses
221 .iter()
222 .map(|c| Condition::parse(c.trim(), &self.headers))
223 .collect();
224
225 let conditions = parsed_clauses?;
226
227 let mut filtered_rows = Vec::new();
228 for row in &self.rows {
229 let mut matches_all = true;
230 for cond in &conditions {
231 if !cond.evaluate(row) {
232 matches_all = false;
233 break;
234 }
235 }
236 if matches_all {
237 filtered_rows.push(row.clone());
238 }
239 }
240
241 Ok(Self {
242 headers: self.headers.clone(),
243 rows: filtered_rows,
244 })
245 }
246
247 pub fn apply_query(
248 &self,
249 select_expr: Option<&str>,
250 where_expr: Option<&str>,
251 ) -> Result<Self, CsvError> {
252 let mut result = if let Some(w) = where_expr {
253 self.apply_where(w)?
254 } else {
255 self.clone()
256 };
257
258 if let Some(s) = select_expr {
259 result = result.apply_select(s)?;
260 }
261
262 Ok(result)
263 }
264
265 pub fn canonicalize_pedigree_pt(&self) -> Self {
266 let new_headers = self
267 .headers
268 .iter()
269 .map(|h| {
270 let lower = h.to_lowercase();
271 for (pt, _, synonyms) in PEDIGREE_SYNONYM_GROUPS {
272 if synonyms.iter().any(|&s| s == lower) {
273 return pt.to_string();
274 }
275 }
276 h.clone()
277 })
278 .collect();
279
280 Self {
281 headers: new_headers,
282 rows: self.rows.clone(),
283 }
284 }
285
286 pub fn canonicalize_pedigree_official(&self) -> Self {
287 let new_headers = self
288 .headers
289 .iter()
290 .map(|h| {
291 let lower = h.to_lowercase();
292 for (_, official, synonyms) in PEDIGREE_SYNONYM_GROUPS {
293 if synonyms.iter().any(|&s| s == lower) {
294 return official.to_string();
295 }
296 }
297 h.clone()
298 })
299 .collect();
300
301 Self {
302 headers: new_headers,
303 rows: self.rows.clone(),
304 }
305 }
306}
307
308pub fn parse_block_directives(block: &str) -> HashMap<String, String> {
309 let mut map = HashMap::new();
310 for line in block.lines() {
311 let trimmed = line.trim();
312 if trimmed.starts_with("%%") {
313 let body = trimmed.trim_start_matches('%').trim();
314 if let Some((k, v)) = body.split_once('=') {
315 map.insert(k.trim().to_lowercase(), v.trim().to_string());
316 }
317 }
318 }
319 map
320}
321
322pub fn split_block_content(block: &str) -> (HashMap<String, String>, String) {
323 let directives = parse_block_directives(block);
324 let body_lines: Vec<&str> = block
325 .lines()
326 .map(|l| l.trim())
327 .filter(|l| !l.starts_with("%%"))
328 .collect();
329 (directives, body_lines.join("\n"))
330}
331
332#[derive(Debug, Clone)]
333enum Operator {
334 Eq,
335 Neq,
336 Lte,
337 Gte,
338 Lt,
339 Gt,
340}
341
342#[derive(Debug, Clone)]
343struct Condition {
344 col_index: usize,
345 op: Operator,
346 target_value: String,
347}
348
349impl Condition {
350 fn parse(clause: &str, headers: &[String]) -> Result<Self, CsvError> {
351 let raw = clause.trim();
352 let ops = [
353 ("<=", Operator::Lte),
354 (">=", Operator::Gte),
355 ("!=", Operator::Neq),
356 ("==", Operator::Eq),
357 ("=", Operator::Eq),
358 ("<", Operator::Lt),
359 (">", Operator::Gt),
360 ];
361
362 for (sym, op) in ops {
363 if let Some((left, right)) = raw.split_once(sym) {
364 let col_name = left.trim();
365 let target = right
366 .trim()
367 .trim_matches('"')
368 .trim_matches('\'')
369 .to_string();
370
371 let col_index = find_pedigree_synonym_column(col_name, headers)
372 .ok_or_else(|| CsvError::ColumnNotFound(col_name.to_string()))?;
373
374 return Ok(Self {
375 col_index,
376 op,
377 target_value: target,
378 });
379 }
380 }
381
382 Err(CsvError::InvalidWhereClause(format!(
383 "Unrecognized operator in '{}'",
384 raw
385 )))
386 }
387
388 fn evaluate(&self, row: &[String]) -> bool {
389 let val = match row.get(self.col_index) {
390 Some(v) => v.trim(),
391 None => "",
392 };
393
394 if let (Ok(num_val), Ok(num_target)) =
395 (val.parse::<f64>(), self.target_value.parse::<f64>())
396 {
397 match self.op {
398 Operator::Eq => (num_val - num_target).abs() < f64::EPSILON,
399 Operator::Neq => (num_val - num_target).abs() >= f64::EPSILON,
400 Operator::Lte => num_val <= num_target,
401 Operator::Gte => num_val >= num_target,
402 Operator::Lt => num_val < num_target,
403 Operator::Gt => num_val > num_target,
404 }
405 } else {
406 let left = val.to_lowercase();
407 let right = self.target_value.to_lowercase();
408 match self.op {
409 Operator::Eq => left == right,
410 Operator::Neq => left != right,
411 Operator::Lte => left <= right,
412 Operator::Gte => left >= right,
413 Operator::Lt => left < right,
414 Operator::Gt => left > right,
415 }
416 }
417 }
418}
419
420pub const PEDIGREE_SYNONYM_GROUPS: &[(&str, &str, &[&str])] = &[
421 ("id", "id", &["id", "individual", "codigo", "individuo"]),
422 (
423 "pai",
424 "fid",
425 &["pai", "fid", "father", "dadid", "progenitor", "pat"],
426 ),
427 (
428 "mae",
429 "mid",
430 &["mae", "mid", "mother", "momid", "progenitora", "mat"],
431 ),
432 ("sexo", "sex", &["sexo", "sex", "gender"]),
433 (
434 "fenotipo",
435 "aff",
436 &[
437 "fenotipo",
438 "aff",
439 "affected",
440 "afetado",
441 "status_clinico",
442 "phenotype",
443 ],
444 ),
445 (
446 "probando",
447 "proband",
448 &["probando", "probanda", "proband", "propositus", "proposito"],
449 ),
450 (
451 "falecido",
452 "status",
453 &["falecido", "deceased", "status", "obito"],
454 ),
455 (
456 "rotulo",
457 "label",
458 &["rotulo", "label", "rotulo_ped", "text", "nome"],
459 ),
460];
461
462pub fn find_pedigree_synonym_column(target: &str, headers: &[String]) -> Option<usize> {
463 if let Some(pos) = headers.iter().position(|h| h.eq_ignore_ascii_case(target)) {
464 return Some(pos);
465 }
466
467 let lower_target = target.to_lowercase();
468 for (_, _, synonyms) in PEDIGREE_SYNONYM_GROUPS {
469 if synonyms.iter().any(|&s| s == lower_target) {
470 for (idx, h) in headers.iter().enumerate() {
471 let lower_h = h.to_lowercase();
472 if synonyms.iter().any(|&s| s == lower_h) {
473 return Some(idx);
474 }
475 }
476 }
477 }
478
479 None
480}