use std::collections::VecDeque;
use std::env;
use std::path::{Path, PathBuf};
use super::{constraint::Constraint, Runtime};
pub type Runtimes<'a> = Box<dyn Iterator<Item = Runtime> + 'a>;
pub trait StrategyLike: std::fmt::Debug + std::panic::RefUnwindSafe + 'static {
fn runtimes(&self) -> Runtimes<'_>;
fn select(&self, constraint: &Constraint) -> Option<Runtime> {
self.runtimes()
.filter(|runtime| constraint.matches(runtime))
.max_by(|ra, rb| ra.version.cmp(&rb.version))
}
fn fallback(&self) -> Option<Runtime> {
self.runtimes().max_by(|ra, rb| ra.version.cmp(&rb.version))
}
}
#[derive(Clone, Debug)]
pub struct RuntimesOnPath(PathBuf);
impl StrategyLike for RuntimesOnPath {
fn runtimes(&self) -> Runtimes<'_> {
Box::new(
env::split_paths(&self.0)
.filter(|bindir| bindir.join("pg_ctl").exists())
.filter_map(|bindir| Runtime::new(bindir).ok()),
)
}
}
#[derive(Clone, Debug)]
pub struct RuntimesOnPathEnv;
impl StrategyLike for RuntimesOnPathEnv {
fn runtimes(&self) -> Runtimes<'_> {
Box::new(
env::var_os("PATH")
.map(|path| {
env::split_paths(&path)
.filter(|bindir| bindir.join("pg_ctl").exists())
.filter_map(|bindir| Runtime::new(bindir).ok())
.collect::<Vec<_>>()
})
.unwrap_or_default()
.into_iter(),
)
}
}
#[derive(Clone, Debug)]
pub struct RuntimesOnPlatform;
impl RuntimesOnPlatform {
#[cfg(any(doc, target_os = "linux"))]
pub fn find() -> Vec<PathBuf> {
glob::glob("/usr/lib/postgresql/*/bin/pg_ctl")
.ok()
.map(|entries| {
entries
.filter_map(Result::ok)
.filter(|path| path.is_file())
.filter_map(|path| path.parent().map(Path::to_owned))
.collect()
})
.unwrap_or_default()
}
#[cfg(any(doc, target_os = "macos"))]
pub fn find() -> Vec<PathBuf> {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
std::process::Command::new("brew")
.arg("--prefix")
.output()
.ok()
.and_then(|output| {
if output.status.success() {
Some(OsString::from_vec(output.stdout))
} else {
None
}
})
.and_then(|brew_prefix| {
glob::glob(&format!(
"{}/Cellar/postgresql@*/*/bin/pg_ctl",
brew_prefix.to_string_lossy().trim_end()
))
.ok()
})
.map(|entries| {
entries
.filter_map(Result::ok)
.filter(|path| path.is_file())
.filter_map(|path| path.parent().map(Path::to_owned))
.collect()
})
.unwrap_or_default()
}
}
impl StrategyLike for RuntimesOnPlatform {
fn runtimes(&self) -> Runtimes<'_> {
Box::new(
Self::find()
.into_iter()
.filter_map(|bindir| Runtime::new(bindir).ok()),
)
}
}
#[derive(Debug)]
pub enum Strategy {
Chain(VecDeque<Strategy>),
Delegated(Box<dyn StrategyLike + Send + Sync>),
Single(Runtime),
}
impl Strategy {
#[must_use]
pub fn push_front<S: Into<Strategy>>(mut self, strategy: S) -> Self {
match self {
Self::Chain(ref mut chain) => {
chain.push_front(strategy.into());
self
}
Self::Delegated(_) | Self::Single(_) => {
let mut chain: VecDeque<Strategy> = VecDeque::new();
chain.push_front(strategy.into());
chain.push_back(self);
Self::Chain(chain)
}
}
}
#[must_use]
pub fn push_back<S: Into<Strategy>>(mut self, strategy: S) -> Self {
match self {
Self::Chain(ref mut chain) => {
chain.push_back(strategy.into());
self
}
Self::Delegated(_) | Self::Single(_) => {
let mut chain: VecDeque<Strategy> = VecDeque::new();
chain.push_front(self);
chain.push_back(strategy.into());
Self::Chain(chain)
}
}
}
}
impl Default for Strategy {
fn default() -> Self {
Self::Chain(VecDeque::new())
.push_front(RuntimesOnPathEnv)
.push_back(RuntimesOnPlatform)
}
}
impl StrategyLike for Strategy {
fn runtimes(&self) -> Runtimes<'_> {
match self {
Self::Chain(chain) => {
let mut seen = std::collections::HashSet::new();
Box::new(
chain
.iter()
.flat_map(|strategy| strategy.runtimes())
.filter(move |runtime| seen.insert(runtime.version)),
)
}
Self::Delegated(strategy) => {
let mut seen = std::collections::HashSet::new();
Box::new(
strategy
.runtimes()
.filter(move |runtime| seen.insert(runtime.version)),
)
}
Self::Single(runtime) => Box::new(std::iter::once(runtime.clone())),
}
}
fn select(&self, constraint: &Constraint) -> Option<Runtime> {
match self {
Self::Chain(c) => c.iter().find_map(|strategy| strategy.select(constraint)),
Self::Delegated(strategy) => strategy.select(constraint),
Self::Single(runtime) if constraint.matches(runtime) => Some(runtime.clone()),
Self::Single(_) => None,
}
}
fn fallback(&self) -> Option<Runtime> {
match self {
Self::Chain(chain) => chain.iter().find_map(Strategy::fallback),
Self::Delegated(strategy) => strategy.fallback(),
Self::Single(runtime) => Some(runtime.clone()),
}
}
}
impl From<RuntimesOnPath> for Strategy {
fn from(strategy: RuntimesOnPath) -> Self {
Self::Delegated(Box::new(strategy))
}
}
impl From<RuntimesOnPathEnv> for Strategy {
fn from(strategy: RuntimesOnPathEnv) -> Self {
Self::Delegated(Box::new(strategy))
}
}
impl From<RuntimesOnPlatform> for Strategy {
fn from(strategy: RuntimesOnPlatform) -> Self {
Self::Delegated(Box::new(strategy))
}
}
impl From<Runtime> for Strategy {
fn from(runtime: Runtime) -> Self {
Self::Single(runtime)
}
}
#[cfg(test)]
mod tests {
use std::env;
use super::{RuntimesOnPath, RuntimesOnPathEnv, RuntimesOnPlatform, Strategy, StrategyLike};
#[test]
fn runtime_find_custom_path() {
let path = env::var_os("PATH").expect("PATH not set");
let strategy = RuntimesOnPath(path.into());
let runtimes = strategy.runtimes();
assert_ne!(0, runtimes.count());
}
#[test]
fn runtime_find_env_path() {
let runtimes = RuntimesOnPathEnv.runtimes();
assert_ne!(0, runtimes.count());
}
#[test]
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn runtime_find_on_platform() {
let runtimes = RuntimesOnPlatform.runtimes();
assert_ne!(0, runtimes.count());
}
#[test]
fn runtime_strategy_set_default() {
let strategy = Strategy::default();
let runtimes = strategy.runtimes();
assert_ne!(0, runtimes.count());
assert!(strategy.fallback().is_some());
}
}