1pub mod conformance;
17pub mod frame;
18pub mod host;
19pub mod msg;
20pub mod options;
21pub mod plugin;
22pub mod sessions;
23pub mod util;
24
25pub const CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
27
28pub const MIN_VERSION: u32 = 0;
30pub const MAX_VERSION: u32 = 0;
31
32pub const CAP_SESSIONS: &str = "sessions";
34pub const CAP_READ_ONLY: &str = "read_only";
36pub const CAP_CHECK: &str = "check";
38pub const CAP_MULTI_FILE: &str = "multi_file";
40pub const CAP_LOAD: &str = "load";
42pub const CAP_VALIDATE: &str = "validate";
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
46#[serde(rename_all = "lowercase")]
47pub enum Kind {
48 Source,
49 Format,
50 Destination,
51}
52
53impl Kind {
54 pub fn as_str(self) -> &'static str {
55 match self {
56 Kind::Source => "source",
57 Kind::Format => "format",
58 Kind::Destination => "destination",
59 }
60 }
61
62 pub fn parse(s: &str) -> Option<Kind> {
64 match s {
65 "source" | "sources" => Some(Kind::Source),
66 "format" | "formats" => Some(Kind::Format),
67 "destination" | "destinations" => Some(Kind::Destination),
68 _ => None,
69 }
70 }
71}
72
73impl std::fmt::Display for Kind {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.write_str(self.as_str())
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
82pub struct PluginId {
83 pub kind: Kind,
84 pub name: String,
85}
86
87impl PluginId {
88 pub fn new(kind: Kind, name: impl Into<String>) -> PluginId {
89 PluginId {
90 kind,
91 name: name.into(),
92 }
93 }
94}
95
96impl std::fmt::Display for PluginId {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 write!(f, "{}/{}", self.kind, self.name)
99 }
100}
101
102impl std::str::FromStr for PluginId {
103 type Err = String;
104
105 fn from_str(s: &str) -> Result<PluginId, String> {
106 let bad = || format!("`{s}` isn't `<kind>/<name>` (e.g. `destination/s3`)");
107 let (k, n) = s.split_once('/').ok_or_else(bad)?;
108 let kind = Kind::parse(k).ok_or_else(bad)?;
109 valid_name(n).then(|| PluginId::new(kind, n)).ok_or_else(bad)
110 }
111}
112
113impl serde::Serialize for PluginId {
114 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
115 s.collect_str(self)
116 }
117}
118
119impl<'de> serde::Deserialize<'de> for PluginId {
120 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
121 String::deserialize(d)?.parse().map_err(serde::de::Error::custom)
122 }
123}
124
125pub fn valid_name(name: &str) -> bool {
127 !name.is_empty()
128 && name
129 .bytes()
130 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
131}
132
133pub fn package_executable_name(package: &str) -> String {
135 format!("dre-plugin-{package}{}", std::env::consts::EXE_SUFFIX)
136}
137
138pub fn parse_package_executable_name(file: &str) -> Option<String> {
140 let stem = file.strip_suffix(std::env::consts::EXE_SUFFIX).unwrap_or(file);
141 if !std::env::consts::EXE_SUFFIX.is_empty() && stem == file {
142 return None;
143 }
144 let name = stem.strip_prefix("dre-plugin-")?;
145 valid_name(name).then(|| name.to_string())
146}
147
148pub fn executable_name(kind: Kind, name: &str) -> String {
150 format!("dre-{}-{name}{}", kind.as_str(), std::env::consts::EXE_SUFFIX)
151}
152
153pub fn parse_executable_name(file: &str) -> Option<(Kind, String)> {
155 let stem = file.strip_suffix(std::env::consts::EXE_SUFFIX).unwrap_or(file);
156 if !std::env::consts::EXE_SUFFIX.is_empty() && stem == file {
157 return None;
158 }
159 let rest = stem.strip_prefix("dre-")?;
160 let (kind, name) = rest.split_once('-')?;
161 let kind = Kind::parse(kind)?;
162 valid_name(name).then(|| (kind, name.to_string()))
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn parses_plugin_executable_names() {
171 let exe = |s: &str| format!("{s}{}", std::env::consts::EXE_SUFFIX);
172 assert_eq!(
173 parse_executable_name(&exe("dre-source-duckdb")),
174 Some((Kind::Source, "duckdb".into()))
175 );
176 assert_eq!(
177 parse_executable_name(&exe("dre-destination-azure_blob")),
178 Some((Kind::Destination, "azure_blob".into()))
179 );
180 assert_eq!(parse_executable_name(&exe("dre-format-csv.d")), None);
181 assert_eq!(parse_executable_name(&exe("dre-widget-x")), None);
182 assert_eq!(parse_executable_name(&exe("dre")), None);
183 assert_eq!(parse_executable_name(&exe("dre-plugin-csv")), None);
184 }
185
186 #[test]
187 fn parses_package_executable_names() {
188 let exe = |s: &str| format!("{s}{}", std::env::consts::EXE_SUFFIX);
189 assert_eq!(
190 parse_package_executable_name(&exe("dre-plugin-object_store")).as_deref(),
191 Some("object_store")
192 );
193 assert_eq!(parse_package_executable_name(&exe("dre-source-duckdb")), None);
194 assert_eq!(parse_package_executable_name(&exe("dre-plugin-")), None);
195 }
196
197 #[test]
198 fn plugin_ids_read_and_write_as_kind_slash_name() {
199 let id: PluginId = "destination/s3".parse().unwrap();
200 assert_eq!(id, PluginId::new(Kind::Destination, "s3"));
201 assert_eq!(id.to_string(), "destination/s3");
202 assert_eq!(serde_json::to_string(&id).unwrap(), "\"destination/s3\"");
203 assert!("s3".parse::<PluginId>().is_err());
204 assert!("widget/s3".parse::<PluginId>().is_err());
205 }
206}