1#![warn(clippy::missing_panics_doc)]
22
23pub use log;
26
27pub mod conformance;
28pub mod connection;
29pub mod delivery;
30pub mod frame;
31#[cfg(target_os = "linux")]
32mod fuse_fds;
33pub mod host;
34pub mod markdown;
35pub mod msg;
36pub mod options;
37pub mod plugin;
38pub mod schema;
39pub mod sessions;
40pub mod style;
41pub mod util;
42
43pub const CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
45
46pub const MIN_VERSION: u32 = 0;
48pub const MAX_VERSION: u32 = 1;
49pub const CORE_MIN_VERSION: u32 = 1;
51
52pub const CAP_SESSIONS: &str = "sessions";
54pub const CAP_READ_ONLY: &str = "read_only";
56pub const CAP_CHECK: &str = "check";
58pub const CAP_MULTI_FILE: &str = "multi_file";
60pub const CAP_MESSAGE: &str = "message";
62pub const CAP_MESSAGE_ONLY: &str = "message_only";
64pub const CAP_LOAD: &str = "load";
66pub const CAP_CATALOG: &str = "catalog";
68pub const CAP_VALIDATE: &str = "validate";
70
71pub const CAPABILITIES: &[&str] = &[
73 CAP_SESSIONS,
74 CAP_READ_ONLY,
75 CAP_CHECK,
76 CAP_MULTI_FILE,
77 CAP_MESSAGE,
78 CAP_MESSAGE_ONLY,
79 CAP_LOAD,
80 CAP_CATALOG,
81 CAP_VALIDATE,
82];
83
84#[derive(
85 Debug,
86 Clone,
87 Copy,
88 PartialEq,
89 Eq,
90 PartialOrd,
91 Ord,
92 Hash,
93 serde::Serialize,
94 serde::Deserialize,
95 schemars::JsonSchema,
96)]
97#[serde(rename_all = "lowercase")]
98pub enum Kind {
99 Source,
100 Format,
101 Destination,
102}
103
104impl Kind {
105 pub fn as_str(self) -> &'static str {
106 match self {
107 Kind::Source => "source",
108 Kind::Format => "format",
109 Kind::Destination => "destination",
110 }
111 }
112
113 pub fn parse(s: &str) -> Option<Kind> {
115 match s {
116 "source" | "sources" => Some(Kind::Source),
117 "format" | "formats" => Some(Kind::Format),
118 "destination" | "destinations" => Some(Kind::Destination),
119 _ => None,
120 }
121 }
122}
123
124impl std::fmt::Display for Kind {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 f.write_str(self.as_str())
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
133pub struct PluginId {
134 pub kind: Kind,
135 pub name: String,
136}
137
138impl PluginId {
139 pub fn new(kind: Kind, name: impl Into<String>) -> PluginId {
140 PluginId {
141 kind,
142 name: name.into(),
143 }
144 }
145}
146
147impl std::fmt::Display for PluginId {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 write!(f, "{}/{}", self.kind, self.name)
150 }
151}
152
153impl std::str::FromStr for PluginId {
154 type Err = String;
155
156 fn from_str(s: &str) -> Result<PluginId, String> {
157 let bad = || format!("`{s}` isn't `<kind>/<name>` (e.g. `destination/s3`)");
158 let (k, n) = s.split_once('/').ok_or_else(bad)?;
159 let kind = Kind::parse(k).ok_or_else(bad)?;
160 valid_name(n).then(|| PluginId::new(kind, n)).ok_or_else(bad)
161 }
162}
163
164impl serde::Serialize for PluginId {
165 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
166 s.collect_str(self)
167 }
168}
169
170impl schemars::JsonSchema for PluginId {
171 fn schema_name() -> std::borrow::Cow<'static, str> {
172 "PluginId".into()
173 }
174
175 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
176 schemars::json_schema!({
177 "description": "One plugin, written `<kind>/<name>` (`destination/s3`).",
178 "type": "string",
179 "pattern": "^(source|format|destination)/[a-z0-9_]+$"
180 })
181 }
182}
183
184impl<'de> serde::Deserialize<'de> for PluginId {
185 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
186 String::deserialize(d)?.parse().map_err(serde::de::Error::custom)
187 }
188}
189
190pub fn valid_name(name: &str) -> bool {
192 !name.is_empty()
193 && name
194 .bytes()
195 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
196}
197
198pub fn package_executable_name(package: &str) -> String {
200 format!("dre-plugin-{package}{}", std::env::consts::EXE_SUFFIX)
201}
202
203pub fn parse_package_executable_name(file: &str) -> Option<String> {
205 let stem = file.strip_suffix(".exe").unwrap_or(file);
206 let name = stem.strip_prefix("dre-plugin-")?;
207 valid_name(name).then(|| name.to_string())
208}
209
210pub fn executable_name(kind: Kind, name: &str) -> String {
212 format!("dre-{}-{name}{}", kind.as_str(), std::env::consts::EXE_SUFFIX)
213}
214
215pub fn parse_executable_name(file: &str) -> Option<(Kind, String)> {
217 let stem = file.strip_suffix(".exe").unwrap_or(file);
218 let rest = stem.strip_prefix("dre-")?;
219 let (kind, name) = rest.split_once('-')?;
220 let kind = Kind::parse(kind)?;
221 valid_name(name).then(|| (kind, name.to_string()))
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn parses_plugin_executable_names() {
230 let exe = |s: &str| format!("{s}{}", std::env::consts::EXE_SUFFIX);
231 assert_eq!(
232 parse_executable_name(&exe("dre-source-duckdb")),
233 Some((Kind::Source, "duckdb".into()))
234 );
235 assert_eq!(
236 parse_executable_name(&exe("dre-destination-azure_blob")),
237 Some((Kind::Destination, "azure_blob".into()))
238 );
239 assert_eq!(parse_executable_name(&exe("dre-format-csv.d")), None);
240 assert_eq!(parse_executable_name("dre-source-fixture").unwrap().1, "fixture");
241 assert_eq!(
242 parse_executable_name("dre-source-fixture.exe").unwrap().1,
243 "fixture"
244 );
245 assert_eq!(parse_executable_name(&exe("dre-widget-x")), None);
246 assert_eq!(parse_executable_name(&exe("dre")), None);
247 assert_eq!(parse_executable_name(&exe("dre-plugin-csv")), None);
248 }
249
250 #[test]
251 fn parses_package_executable_names() {
252 let exe = |s: &str| format!("{s}{}", std::env::consts::EXE_SUFFIX);
253 assert_eq!(
254 parse_package_executable_name(&exe("dre-plugin-object_store")).as_deref(),
255 Some("object_store")
256 );
257 assert_eq!(parse_package_executable_name(&exe("dre-source-duckdb")), None);
258 assert_eq!(parse_package_executable_name(&exe("dre-plugin-")), None);
259 assert_eq!(
260 parse_package_executable_name("dre-plugin-fixture").as_deref(),
261 Some("fixture")
262 );
263 assert_eq!(
264 parse_package_executable_name("dre-plugin-fixture.exe").as_deref(),
265 Some("fixture")
266 );
267 }
268
269 #[test]
270 fn plugin_ids_read_and_write_as_kind_slash_name() {
271 let id: PluginId = "destination/s3".parse().unwrap();
272 assert_eq!(id, PluginId::new(Kind::Destination, "s3"));
273 assert_eq!(id.to_string(), "destination/s3");
274 assert_eq!(serde_json::to_string(&id).unwrap(), "\"destination/s3\"");
275 assert!("s3".parse::<PluginId>().is_err());
276 assert!("widget/s3".parse::<PluginId>().is_err());
277 }
278}