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