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