use std::path::PathBuf;
use clap::Parser;
#[derive(Debug, Clone, Parser)]
#[command(
name = "malkuth",
version,
about = "Wrap any program with a supervised pod pool, file watching and a sticky reverse proxy."
)]
pub struct Args {
#[arg(long = "watch", value_name = "PATH")]
pub watch: Vec<PathBuf>,
#[arg(long = "proxy", value_name = "PUBLIC:LO-HI")]
pub proxy: Option<String>,
#[arg(long = "pod-count", default_value = "1")]
pub pod_count: usize,
#[arg(long = "host", default_value = "127.0.0.1")]
pub host: String,
#[arg(long = "port-env", default_value = "PORT")]
pub port_env: String,
#[arg(long = "sticky-ttl-secs", default_value = "300")]
pub sticky_ttl_secs: u64,
#[arg(long = "drain-secs", default_value = "8")]
pub drain_secs: u64,
#[arg(last = true, allow_hyphen_values = true)]
pub command: Vec<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct ProxySpec {
pub public_port: u16,
pub range_lo: u16,
pub range_hi: u16,
}
impl ProxySpec {
pub fn parse(s: &str) -> Result<Self, String> {
let (pub_s, range_s) = s
.split_once(':')
.ok_or_else(|| format!("--proxy must look like PUBLIC:LO-HI (got `{s}`)"))?;
let public_port: u16 = pub_s
.parse()
.map_err(|_| format!("invalid public port `{pub_s}`"))?;
let (lo_s, hi_s) = range_s
.split_once('-')
.ok_or_else(|| format!("backend range must look like LO-HI (got `{range_s}`)"))?;
let range_lo: u16 = lo_s
.parse()
.map_err(|_| format!("invalid range lo `{lo_s}`"))?;
let range_hi: u16 = hi_s
.parse()
.map_err(|_| format!("invalid range hi `{hi_s}`"))?;
if range_hi < range_lo {
return Err(format!("range hi {range_hi} < lo {range_lo}"));
}
Ok(Self {
public_port,
range_lo,
range_hi,
})
}
pub fn backend_ports(&self) -> impl Iterator<Item = u16> {
(self.range_lo..=self.range_hi).filter(move |p| *p != self.public_port)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_proxy_spec() {
let s = ProxySpec::parse("3000:3000-3999").unwrap();
assert_eq!(s.public_port, 3000);
assert_eq!(s.range_lo, 3000);
assert_eq!(s.range_hi, 3999);
let ports: Vec<u16> = s.backend_ports().take(5).collect();
assert_eq!(ports, vec![3001, 3002, 3003, 3004, 3005]);
}
#[test]
fn parse_rejects_bad() {
assert!(ProxySpec::parse("3000").is_err());
assert!(ProxySpec::parse("3000:4000-3999").is_err());
}
}