use std::any::Any;
use std::sync::{Mutex, MutexGuard};
use crate::core::io::{ExtensionWithMime, Importable};
pub struct TabularSource {
bytes: Vec<u8>,
format: String,
opened: Mutex<Option<Box<dyn Any + Send>>>,
}
impl std::fmt::Debug for TabularSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TabularSource")
.field("format", &self.format)
.field("bytes", &self.bytes.len())
.finish()
}
}
impl TabularSource {
#[must_use]
pub fn new(bytes: Vec<u8>, format: impl Into<String>) -> Self {
Self {
bytes,
format: format.into().to_lowercase(),
opened: Mutex::new(None),
}
}
pub fn reader<T, E, F>(&self, open: F) -> Result<TabularReader<'_, T>, String>
where
T: Any + Send,
E: std::fmt::Display,
F: FnOnce(&[u8]) -> Result<T, E>,
{
let mut guard = self.opened.try_lock().map_err(|e| match e {
std::sync::TryLockError::WouldBlock => {
"data source is already open for reading elsewhere".to_string()
}
std::sync::TryLockError::Poisoned(_) => "data source lock poisoned".to_string(),
})?;
if guard.is_none() {
*guard = Some(Box::new(open(&self.bytes).map_err(|e| e.to_string())?));
}
if guard.as_ref().is_none_or(|b| !b.is::<T>()) {
return Err("data source was already opened as a different reader".to_string());
}
Ok(TabularReader {
guard,
_marker: std::marker::PhantomData,
})
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn format(&self) -> &str {
&self.format
}
}
#[allow(missing_debug_implementations)]
pub struct TabularReader<'a, T> {
guard: MutexGuard<'a, Option<Box<dyn Any + Send>>>,
_marker: std::marker::PhantomData<fn() -> T>,
}
impl<T: Any + Send> TabularReader<'_, T> {
#[must_use]
pub fn get(&self) -> &T {
self.guard
.as_ref()
.and_then(|b| b.downcast_ref::<T>())
.expect("reader was checked when the guard was taken")
}
}
#[derive(Debug)]
pub enum TabularSourceError {
Io(std::io::Error),
}
impl std::fmt::Display for TabularSourceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for TabularSourceError {}
impl From<std::io::Error> for TabularSourceError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
impl Importable for TabularSource {
type Error = TabularSourceError;
type ImportOptions = ();
fn import_from_reader_with_options<R: std::io::Read>(
mut reader: R,
data_format: &str,
_options: Self::ImportOptions,
) -> Result<Self, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes)?;
Ok(Self::new(bytes, data_format))
}
fn known_import_formats() -> Vec<ExtensionWithMime> {
vec![
ExtensionWithMime::new("sqlite", "application/vnd.sqlite3"),
ExtensionWithMime::new("sqlite3", "application/vnd.sqlite3"),
ExtensionWithMime::new("db", "application/vnd.sqlite3"),
#[cfg(feature = "extraction-dbcon")]
ExtensionWithMime::new("csv", "text/csv"),
#[cfg(feature = "extraction-dbcon")]
ExtensionWithMime::new("tsv", "text/tab-separated-values"),
#[cfg(feature = "extraction-dbcon")]
ExtensionWithMime::new("parquet", "application/vnd.apache.parquet"),
#[cfg(feature = "extraction-dbcon")]
ExtensionWithMime::new(
"xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_format_is_lowercased_however_it_was_spelled() {
for spelling in ["SQLite", "SQLITE", "sqlite"] {
assert_eq!(TabularSource::new(Vec::new(), spelling).format(), "sqlite");
}
assert_eq!(
TabularSource::import_from_bytes(b"a,b\n1,2\n", "CSV")
.expect("reading from a slice cannot fail")
.format(),
"csv"
);
}
#[test]
fn debug_reports_the_size_rather_than_the_bytes() {
let rendered = format!("{:?}", TabularSource::new(vec![0xAB; 4096], "sqlite"));
assert!(
rendered.contains("4096") && !rendered.contains("171, 171"),
"a source should render as a summary: {rendered}"
);
}
}