use crate::csv::row::CsvRow; use crate::csv::table::{CsvColumn, CsvTable}; use crate::csv::value::{ColumnType, CsvValue}; #[derive(Debug, thiserror::Error)] pub enum CsvError { #[error("{file}: the file has no column name line")] MissingColumnNames { file: String }, #[error("{file}: the file has no column type line")] MissingColumnTypes { file: String }, #[error("{file}: column {column} (`{name}`) has the unknown type `{value}`, expecting int/string/boolean")] UnknownColumnType { file: String, column: usize, name: String, value: String, }, #[error("{file}: the first data line does not open a row")] OrphanValues { file: String }, #[error("io: {0}")] Io(#[from] std::io::Error), } pub struct CsvReader; impl CsvReader { pub fn parse(file_name: impl Into, source: &str) -> Result { let file_name = file_name.into(); let source = source.strip_prefix('\u{feff}').unwrap_or(source); let mut lines = source .lines() .map(|line| line.strip_suffix('\r').unwrap_or(line)); let name_line = lines.next().ok_or_else(|| CsvError::MissingColumnNames { file: file_name.clone(), })?; let type_line = lines.next().ok_or_else(|| CsvError::MissingColumnTypes { file: file_name.clone(), })?; let names = split_line(name_line); let types = split_line(type_line); let mut columns = Vec::with_capacity(names.len()); for (index, name) in names.iter().enumerate() { let raw = types .get(index) .map(|value| value.trim()) .filter(|value| !value.is_empty()) .unwrap_or("string"); let column_type = ColumnType::parse(raw).ok_or_else(|| CsvError::UnknownColumnType { file: file_name.clone(), column: index, name: name.clone(), value: raw.to_owned(), })?; columns.push(CsvColumn { name: name.clone(), column_type, }); } let mut rows: Vec = Vec::new(); for line in lines { if line.trim().is_empty() { continue; } let cells = split_line(line); let opens_row = cells .first() .map(|cell| !cell.trim().is_empty()) .unwrap_or(false); if opens_row { rows.push(CsvRow::with_columns(columns.len())); } else if rows.is_empty() { return Err(CsvError::OrphanValues { file: file_name }); } let row = rows.last_mut().expect("row present"); for (index, column) in columns.iter().enumerate() { let raw = cells.get(index).map(String::as_str).unwrap_or(""); row.push(index, CsvValue::convert(raw, column.column_type)); } } Ok(CsvTable::new(file_name, columns, rows)) } pub fn parse_file(path: impl AsRef) -> Result { let path = path.as_ref(); let bytes = std::fs::read(path)?; let source = String::from_utf8_lossy(&bytes).into_owned(); let file_name = path .file_name() .map(|name| name.to_string_lossy().into_owned()) .unwrap_or_default(); Self::parse(file_name, &source) } } fn split_line(line: &str) -> Vec { let mut cells = Vec::new(); let mut current = String::new(); let mut quoted = false; let mut chars = line.chars().peekable(); while let Some(character) = chars.next() { match character { '"' if quoted && chars.peek() == Some(&'"') => { current.push('"'); chars.next(); } '"' => quoted = !quoted, ',' if !quoted => cells.push(std::mem::take(&mut current)), _ => current.push(character), } } cells.push(current); cells }