mod error;
pub use error::*;
pub mod layout;
mod io;
pub use io::*;
mod table_location;
pub(crate) use table_location::normalize_relative_segment_path;
pub use table_location::*;
mod output;
pub use output::*;
use snafu::IntoError;
use std::path::PathBuf;
pub type StorageResult<T> = Result<T, StorageError>;
#[derive(Clone, Debug)]
pub enum StorageLocation {
Local(PathBuf),
}
impl StorageLocation {
pub fn local(root: impl Into<PathBuf>) -> Self {
StorageLocation::Local(root.into())
}
pub fn parse(spec: &str) -> StorageResult<Self> {
let trimmed = spec.trim();
if trimmed.is_empty() {
return Err(OtherIoSnafu {
path: "<empty table location>".to_string(),
}
.into_error(BackendError::Local(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"table location is empty",
))));
}
if trimmed.len() >= 2 {
let mut chars = trimmed.chars();
let first = chars.next();
let second = chars.next();
if let (Some(first), Some(second)) = (first, second)
&& first.is_ascii_alphabetic()
&& second == ':'
{
return Ok(StorageLocation::Local(PathBuf::from(trimmed)));
}
}
let scheme = trimmed.split_once("://").and_then(|(scheme, _)| {
if scheme.is_empty() {
None
} else {
Some(scheme)
}
});
if let Some(scheme) = scheme {
let scheme_ok = scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.');
if scheme_ok {
return Err(OtherIoSnafu {
path: trimmed.to_string(),
}
.into_error(BackendError::Local(std::io::Error::new(
std::io::ErrorKind::Unsupported,
format!("unsupported table location scheme: {scheme}"),
))));
}
}
Ok(StorageLocation::Local(PathBuf::from(trimmed)))
}
}
#[cfg(test)]
mod tests {
use std::io;
use super::*;
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn parse_rejects_empty_location() {
let err = StorageLocation::parse(" ").expect_err("expected error");
match err {
StorageError::OtherIo { source, .. } => match source {
BackendError::Local(inner) => {
assert_eq!(inner.kind(), io::ErrorKind::InvalidInput);
}
},
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn parse_rejects_unsupported_scheme() {
let err =
StorageLocation::parse("s3://bucket/path").expect_err("expected unsupported scheme");
match err {
StorageError::OtherIo { source, .. } => match source {
BackendError::Local(inner) => {
assert_eq!(inner.kind(), io::ErrorKind::Unsupported);
}
},
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn parse_accepts_local_path() -> TestResult {
let loc = StorageLocation::parse("/tmp/table")?;
match loc {
StorageLocation::Local(p) => {
assert_eq!(p, PathBuf::from("/tmp/table"));
}
}
Ok(())
}
}