use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum RouteError {
#[error("invalid store protocol: '{protocol}'")]
InvalidProtocol {
protocol: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Adapter {
File,
S3,
B2,
Gcs,
External {
name: String,
},
}
impl Adapter {
#[must_use]
pub fn name(&self) -> &str {
match self {
Adapter::File => "file",
Adapter::S3 => "s3",
Adapter::B2 => "b2",
Adapter::Gcs => "gcs",
Adapter::External { name } => name,
}
}
#[must_use]
pub fn store_binary(&self) -> String {
format!("snapdir-{}-store", self.name())
}
#[must_use]
pub fn is_builtin(&self) -> bool {
!matches!(self, Adapter::External { .. })
}
}
pub fn store_protocol(store_url: &str) -> Result<&str, RouteError> {
let proto = store_url.split(':').next().unwrap_or("");
if proto.is_empty()
|| !proto
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
{
return Err(RouteError::InvalidProtocol {
protocol: proto.to_owned(),
});
}
Ok(proto)
}
pub fn resolve_adapter(store_url: &str) -> Result<Adapter, RouteError> {
let proto = store_protocol(store_url)?;
Ok(match proto {
"gs" => Adapter::Gcs,
"file" => Adapter::File,
"s3" => Adapter::S3,
"b2" => Adapter::B2,
other => Adapter::External {
name: other.to_owned(),
},
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shim_router_gs_is_special_cased_to_gcs() {
let a = resolve_adapter("gs://bucket/x").unwrap();
assert_eq!(a, Adapter::Gcs);
assert_eq!(a.name(), "gcs");
assert_eq!(a.store_binary(), format!("snapdir-{}-store", "gcs"));
assert!(a.is_builtin());
}
#[test]
fn shim_router_s3_resolves_to_s3_store() {
let a = resolve_adapter("s3://bucket/path/to/dir").unwrap();
assert_eq!(a, Adapter::S3);
assert_eq!(a.store_binary(), format!("snapdir-{}-store", "s3"));
assert!(a.is_builtin());
}
#[test]
fn shim_router_b2_resolves_to_b2_store() {
let a = resolve_adapter("b2://bucket/x").unwrap();
assert_eq!(a, Adapter::B2);
assert_eq!(a.store_binary(), format!("snapdir-{}-store", "b2"));
}
#[test]
fn shim_router_file_is_builtin() {
let a = resolve_adapter("file:///long/term/storage/").unwrap();
assert_eq!(a, Adapter::File);
assert_eq!(a.store_binary(), format!("snapdir-{}-store", "file"));
assert!(a.is_builtin());
}
#[test]
fn shim_router_unknown_protocol_is_external_binary() {
let a = resolve_adapter("mock://bucket/x").unwrap();
assert_eq!(
a,
Adapter::External {
name: "mock".to_owned()
}
);
assert_eq!(a.name(), "mock");
assert_eq!(a.store_binary(), "snapdir-mock-store");
assert!(!a.is_builtin());
}
#[test]
fn shim_router_numeric_protocols_are_allowed() {
assert_eq!(store_protocol("s3://b").unwrap(), "s3");
assert_eq!(store_protocol("b2://b").unwrap(), "b2");
assert_eq!(store_protocol("0a1://b").unwrap(), "0a1");
}
#[test]
fn shim_router_rejects_invalid_protocols() {
assert_eq!(
resolve_adapter("S3://b"),
Err(RouteError::InvalidProtocol {
protocol: "S3".to_owned()
})
);
assert!(matches!(
resolve_adapter("://b"),
Err(RouteError::InvalidProtocol { .. })
));
assert!(matches!(
resolve_adapter("/just/a/path"),
Err(RouteError::InvalidProtocol { .. })
));
}
#[test]
fn shim_router_protocol_is_text_before_first_colon() {
assert_eq!(store_protocol("gs://bucket/a:b:c").unwrap(), "gs");
}
}