use std::collections::BTreeMap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::plugin::{ProcessTransport, PLUGIN_PREFIX};
use crate::transport::{LocalTransport, MemoryTransport, NameError, Transport, TransportError};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema)]
#[schemars(transparent)]
pub struct TransportKind(String);
impl TransportKind {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for TransportKind {
type Err = NameError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let ok = text.starts_with(|ch: char| ch.is_ascii_lowercase())
&& text
.chars()
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-');
if ok {
Ok(Self(text.to_owned()))
} else {
Err(NameError {
what: "transport kind",
text: text.to_owned(),
why: "a kind is lowercase ASCII letters, digits and `-`, starting with a letter"
.to_owned(),
})
}
}
}
impl fmt::Display for TransportKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Serialize for TransportKind {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for TransportKind {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
String::deserialize(deserializer)?
.parse()
.map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct TransportConfig {
pub kind: TransportKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dir: Option<PathBuf>,
#[serde(flatten)]
pub options: Map<String, Value>,
}
impl TransportConfig {
#[must_use]
pub fn local(dir: impl Into<PathBuf>) -> Self {
Self {
kind: TransportKind(LOCAL.to_owned()),
dir: Some(dir.into()),
options: Map::new(),
}
}
fn refuse_options(&self) -> Result<(), TransportError> {
match self.options.keys().next() {
Some(key) => Err(TransportError::Config {
kind: self.kind.to_string(),
why: format!(
"transport.{key} is not a key the {} transport takes",
self.kind
),
}),
None => Ok(()),
}
}
}
pub const LOCAL: &str = "local";
pub const MEMORY: &str = "memory";
pub type TransportFactory =
Arc<dyn Fn(&TransportConfig) -> Result<Arc<dyn Transport>, TransportError> + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum KindOrigin {
Builtin,
Registered,
Plugin,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct KindEntry {
pub kind: TransportKind,
pub origin: KindOrigin,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
}
#[derive(Clone)]
pub struct TransportKinds {
registered: BTreeMap<String, (KindOrigin, TransportFactory)>,
search: Option<Vec<PathBuf>>,
}
impl std::fmt::Debug for TransportKinds {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TransportKinds")
.field("kinds", &self.registered.keys().collect::<Vec<_>>())
.finish_non_exhaustive()
}
}
impl Default for TransportKinds {
fn default() -> Self {
Self::builtin()
}
}
fn check_kind(kind: &str) -> Result<(), TransportError> {
let ok = kind.starts_with(|ch: char| ch.is_ascii_lowercase())
&& kind
.chars()
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-');
if ok {
Ok(())
} else {
Err(TransportError::Config {
kind: kind.to_owned(),
why: format!(
"{kind:?} is not a transport kind: a kind is lowercase ASCII letters, digits and `-`, starting with a letter"
),
})
}
}
impl TransportKinds {
#[must_use]
pub fn builtin() -> Self {
let mut registered: BTreeMap<String, (KindOrigin, TransportFactory)> = BTreeMap::new();
let local: TransportFactory = Arc::new(|config: &TransportConfig| {
config.refuse_options()?;
let dir = config.dir.clone().ok_or_else(|| TransportError::Config {
kind: config.kind.to_string(),
why:
"the local transport needs transport.dir, the directory it keeps its queues in"
.to_owned(),
})?;
Ok(Arc::new(LocalTransport::open(dir)?) as Arc<dyn Transport>)
});
let memory: TransportFactory = Arc::new(|config: &TransportConfig| {
config.refuse_options()?;
if config.dir.is_some() {
return Err(TransportError::Config {
kind: config.kind.to_string(),
why: "transport.dir is not a key the memory transport takes".to_owned(),
});
}
Ok(Arc::new(MemoryTransport::new()) as Arc<dyn Transport>)
});
registered.insert(LOCAL.to_owned(), (KindOrigin::Builtin, local));
registered.insert(MEMORY.to_owned(), (KindOrigin::Builtin, memory));
Self {
registered,
search: None,
}
}
pub fn register(
&mut self,
kind: &str,
factory: TransportFactory,
) -> Result<(), TransportError> {
check_kind(kind)?;
if self.registered.contains_key(kind) {
return Err(TransportError::Config {
kind: kind.to_owned(),
why: format!("the {kind} transport kind is already registered"),
});
}
self.registered
.insert(kind.to_owned(), (KindOrigin::Registered, factory));
Ok(())
}
#[must_use]
pub fn searching(mut self, dirs: Vec<PathBuf>) -> Self {
self.search = Some(dirs);
self
}
fn search_dirs(&self) -> Vec<PathBuf> {
match &self.search {
Some(dirs) => dirs.clone(),
None => std::env::var_os("PATH")
.map(|path| std::env::split_paths(&path).collect())
.unwrap_or_default(),
}
}
#[must_use]
pub fn plugin(&self, kind: &TransportKind) -> Option<PathBuf> {
self.search_dirs()
.into_iter()
.flat_map(|dir| {
plugin_names(kind.as_str())
.into_iter()
.map(move |name| dir.join(name))
})
.find(|candidate| is_executable(candidate))
}
pub fn open(&self, config: &TransportConfig) -> Result<Arc<dyn Transport>, TransportError> {
if let Some((_, factory)) = self.registered.get(config.kind.as_str()) {
return factory(config);
}
match self.plugin(&config.kind) {
Some(path) => Ok(Arc::new(ProcessTransport::spawn(&path, config)?)),
None => Err(TransportError::UnknownKind {
kind: config.kind.to_string(),
known: self
.kinds()
.into_iter()
.map(|entry| entry.kind.to_string())
.collect::<Vec<_>>()
.join(", "),
}),
}
}
#[must_use]
pub fn kinds(&self) -> Vec<KindEntry> {
let mut entries: Vec<KindEntry> = self
.registered
.iter()
.filter(|(_, (origin, _))| *origin == KindOrigin::Builtin)
.chain(
self.registered
.iter()
.filter(|(_, (origin, _))| *origin == KindOrigin::Registered),
)
.map(|(kind, (origin, _))| KindEntry {
kind: TransportKind(kind.clone()),
origin: origin.clone(),
path: None,
})
.collect();
let mut plugins: BTreeMap<String, PathBuf> = BTreeMap::new();
for dir in self.search_dirs() {
let Ok(listing) = std::fs::read_dir(&dir) else {
continue;
};
for entry in listing.filter_map(Result::ok) {
let name = entry.file_name().to_string_lossy().into_owned();
let Some(kind) = name
.strip_prefix(PLUGIN_PREFIX)
.map(|kind| kind.strip_suffix(".exe").unwrap_or(kind))
else {
continue;
};
if check_kind(kind).is_err()
|| self.registered.contains_key(kind)
|| plugins.contains_key(kind)
|| !is_executable(&entry.path())
{
continue;
}
plugins.insert(kind.to_owned(), entry.path());
}
}
entries.extend(plugins.into_iter().map(|(kind, path)| KindEntry {
kind: TransportKind(kind),
origin: KindOrigin::Plugin,
path: Some(path),
}));
entries
}
}
fn plugin_names(kind: &str) -> Vec<String> {
let bare = format!("{PLUGIN_PREFIX}{kind}");
if cfg!(windows) {
vec![format!("{bare}.exe"), bare]
} else {
vec![bare]
}
}
#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt as _;
std::fs::metadata(path)
.is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
}
#[cfg(not(unix))]
fn is_executable(path: &Path) -> bool {
std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file())
}