use crate::error::DataError;
use flate2::read::GzDecoder;
use std::io::Read;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
CsvGz,
Csv,
Parquet,
}
impl Format {
pub fn detect(bytes: &[u8]) -> Format {
if bytes.starts_with(&[0x1f, 0x8b]) {
Format::CsvGz
} else if bytes.starts_with(b"PAR1") {
Format::Parquet
} else {
Format::Csv
}
}
}
#[cfg(feature = "parquet")]
pub(crate) const CANDIDATE_EXTS: &[&str] = &[".csv.gz", ".parquet", ".csv"];
#[cfg(not(feature = "parquet"))]
pub(crate) const CANDIDATE_EXTS: &[&str] = &[".csv.gz", ".csv"];
pub(crate) fn read_csv_text(bytes: &[u8]) -> Result<String, DataError> {
match Format::detect(bytes) {
Format::CsvGz => {
let mut text = String::new();
GzDecoder::new(bytes)
.read_to_string(&mut text)
.map_err(|e| DataError::Io(e.to_string()))?;
Ok(text)
}
Format::Csv => {
String::from_utf8(bytes.to_vec()).map_err(|e| DataError::Parse(e.to_string()))
}
Format::Parquet => Err(DataError::Parse(
"expected CSV bytes, got Parquet".to_string(),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use flate2::write::GzEncoder;
use flate2::Compression;
use std::io::Write;
fn gz(text: &str) -> Vec<u8> {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(text.as_bytes()).unwrap();
e.finish().unwrap()
}
#[test]
fn detects_each_format_from_magic_bytes() {
assert_eq!(Format::detect(&gz("day,close\n")), Format::CsvGz);
assert_eq!(Format::detect(b"day,close\n2024-01-02,10\n"), Format::Csv);
assert_eq!(Format::detect(b"PAR1\x00\x00PAR1"), Format::Parquet);
assert_eq!(Format::detect(b""), Format::Csv);
}
#[test]
fn read_csv_text_handles_gzip_and_plain() {
let plain = "day,close\n2024-01-02,10\n";
assert_eq!(read_csv_text(plain.as_bytes()).unwrap(), plain);
assert_eq!(read_csv_text(&gz(plain)).unwrap(), plain);
assert!(read_csv_text(b"PAR1junk").is_err());
}
}