pub mod conformance;
pub mod frame;
pub mod host;
pub mod msg;
pub mod options;
pub mod plugin;
pub mod sessions;
pub mod util;
pub const CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const MIN_VERSION: u32 = 0;
pub const MAX_VERSION: u32 = 0;
pub const CAP_SESSIONS: &str = "sessions";
pub const CAP_READ_ONLY: &str = "read_only";
pub const CAP_CHECK: &str = "check";
pub const CAP_MULTI_FILE: &str = "multi_file";
pub const CAP_LOAD: &str = "load";
pub const CAP_VALIDATE: &str = "validate";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Kind {
Source,
Format,
Destination,
}
impl Kind {
pub fn as_str(self) -> &'static str {
match self {
Kind::Source => "source",
Kind::Format => "format",
Kind::Destination => "destination",
}
}
pub fn parse(s: &str) -> Option<Kind> {
match s {
"source" | "sources" => Some(Kind::Source),
"format" | "formats" => Some(Kind::Format),
"destination" | "destinations" => Some(Kind::Destination),
_ => None,
}
}
}
impl std::fmt::Display for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PluginId {
pub kind: Kind,
pub name: String,
}
impl PluginId {
pub fn new(kind: Kind, name: impl Into<String>) -> PluginId {
PluginId {
kind,
name: name.into(),
}
}
}
impl std::fmt::Display for PluginId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}", self.kind, self.name)
}
}
impl std::str::FromStr for PluginId {
type Err = String;
fn from_str(s: &str) -> Result<PluginId, String> {
let bad = || format!("`{s}` isn't `<kind>/<name>` (e.g. `destination/s3`)");
let (k, n) = s.split_once('/').ok_or_else(bad)?;
let kind = Kind::parse(k).ok_or_else(bad)?;
valid_name(n).then(|| PluginId::new(kind, n)).ok_or_else(bad)
}
}
impl serde::Serialize for PluginId {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
impl<'de> serde::Deserialize<'de> for PluginId {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
String::deserialize(d)?.parse().map_err(serde::de::Error::custom)
}
}
pub fn valid_name(name: &str) -> bool {
!name.is_empty()
&& name
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
}
pub fn package_executable_name(package: &str) -> String {
format!("dre-plugin-{package}{}", std::env::consts::EXE_SUFFIX)
}
pub fn parse_package_executable_name(file: &str) -> Option<String> {
let stem = file.strip_suffix(std::env::consts::EXE_SUFFIX).unwrap_or(file);
if !std::env::consts::EXE_SUFFIX.is_empty() && stem == file {
return None;
}
let name = stem.strip_prefix("dre-plugin-")?;
valid_name(name).then(|| name.to_string())
}
pub fn executable_name(kind: Kind, name: &str) -> String {
format!("dre-{}-{name}{}", kind.as_str(), std::env::consts::EXE_SUFFIX)
}
pub fn parse_executable_name(file: &str) -> Option<(Kind, String)> {
let stem = file.strip_suffix(std::env::consts::EXE_SUFFIX).unwrap_or(file);
if !std::env::consts::EXE_SUFFIX.is_empty() && stem == file {
return None;
}
let rest = stem.strip_prefix("dre-")?;
let (kind, name) = rest.split_once('-')?;
let kind = Kind::parse(kind)?;
valid_name(name).then(|| (kind, name.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_plugin_executable_names() {
let exe = |s: &str| format!("{s}{}", std::env::consts::EXE_SUFFIX);
assert_eq!(
parse_executable_name(&exe("dre-source-duckdb")),
Some((Kind::Source, "duckdb".into()))
);
assert_eq!(
parse_executable_name(&exe("dre-destination-azure_blob")),
Some((Kind::Destination, "azure_blob".into()))
);
assert_eq!(parse_executable_name(&exe("dre-format-csv.d")), None);
assert_eq!(parse_executable_name(&exe("dre-widget-x")), None);
assert_eq!(parse_executable_name(&exe("dre")), None);
assert_eq!(parse_executable_name(&exe("dre-plugin-csv")), None);
}
#[test]
fn parses_package_executable_names() {
let exe = |s: &str| format!("{s}{}", std::env::consts::EXE_SUFFIX);
assert_eq!(
parse_package_executable_name(&exe("dre-plugin-object_store")).as_deref(),
Some("object_store")
);
assert_eq!(parse_package_executable_name(&exe("dre-source-duckdb")), None);
assert_eq!(parse_package_executable_name(&exe("dre-plugin-")), None);
}
#[test]
fn plugin_ids_read_and_write_as_kind_slash_name() {
let id: PluginId = "destination/s3".parse().unwrap();
assert_eq!(id, PluginId::new(Kind::Destination, "s3"));
assert_eq!(id.to_string(), "destination/s3");
assert_eq!(serde_json::to_string(&id).unwrap(), "\"destination/s3\"");
assert!("s3".parse::<PluginId>().is_err());
assert!("widget/s3".parse::<PluginId>().is_err());
}
}