mod error;
pub use error::*;
pub(crate) mod layout;
mod io;
pub(crate) use io::*;
mod table_location;
pub use table_location::*;
pub(crate) use table_location::{
ensure_canonical_relative_storage_path, normalize_relative_storage_path,
};
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(StorageBackendError::from(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(StorageBackendError::from(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: StorageBackendError::Filesystem { source },
..
} => {
assert_eq!(source.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: StorageBackendError::Filesystem { source },
..
} => {
assert_eq!(source.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(())
}
}