use std::io::Write;
use sqlx::Database;
use crate::manage_interactive;
use crate::tenancy::error::TenancyError;
use crate::tenancy::org_host::{self, HostError};
use crate::tenancy::pools::TenantPools;
use super::args::next_value;
fn explain(e: HostError) -> TenancyError {
match e {
HostError::Driver(d) => TenancyError::from(d),
other => TenancyError::Validation(other.to_string()),
}
}
fn positional_or_ask(
given: Option<&String>,
prompt: &str,
missing: &str,
) -> Result<String, TenancyError> {
match given {
Some(v) => Ok(v.clone()),
None => manage_interactive::ask(prompt)
.map_err(TenancyError::Io)?
.ok_or_else(|| TenancyError::Validation(missing.to_owned())),
}
}
pub(super) fn set_direction(
slot: &mut Option<bool>,
value: bool,
pair: (&str, &str),
) -> Result<(), TenancyError> {
match slot {
Some(prev) if *prev != value => Err(TenancyError::Validation(format!(
"{} and {} contradict each other — pass one",
pair.0, pair.1
))),
_ => {
*slot = Some(value);
Ok(())
}
}
}
fn parse_bool(raw: &str) -> Result<bool, TenancyError> {
match raw.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Ok(true),
"0" | "false" | "no" | "off" => Ok(false),
other => Err(TenancyError::Validation(format!(
"`{other}` is not a yes/no value — use --on or --off"
))),
}
}
fn slug_and_host(args: &[String], verb: &str) -> Result<(String, String), TenancyError> {
let mut positional = args.iter().filter(|a| !a.starts_with("--"));
let slug = positional_or_ask(
positional.next(),
"Tenant slug: ",
&format!("{verb} requires a tenant slug"),
)?;
let host = positional_or_ask(
positional.next(),
"Hostname: ",
&format!("{verb} requires a hostname"),
)?;
Ok((slug, host))
}
pub(super) async fn list_hosts<W: Write + Send, DB: Database>(
pools: &TenantPools<DB>,
args: &[String],
w: &mut W,
) -> Result<(), TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let slug = positional_or_ask(
args.iter().find(|a| !a.starts_with("--")),
"Tenant slug: ",
"list-hosts requires a tenant slug",
)?;
let hosts = org_host::list_for_org(&pools.registry_pool(), &slug)
.await
.map_err(explain)?;
if hosts.is_empty() {
writeln!(
w,
"(no hostnames — `{slug}` is reachable by subdomain only)"
)?;
return Ok(());
}
writeln!(w, "{:<48} {:<8} source", "hostname", "enabled")?;
writeln!(w, "{}", "-".repeat(70))?;
for h in &hosts {
writeln!(
w,
"{:<48} {:<8} {}",
h.hostname,
h.enabled,
if h.is_base { "base" } else { "extra" }
)?;
}
Ok(())
}
pub(super) async fn add_host<W: Write + Send, DB: Database>(
pools: &TenantPools<DB>,
args: &[String],
w: &mut W,
) -> Result<(), TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let (slug, host) = slug_and_host(args, "add-host")?;
let row = org_host::add_host(&pools.registry_pool(), &slug, &host)
.await
.map_err(explain)?;
writeln!(w, "bound {} -> {slug}", row.hostname)?;
if !row.enabled {
writeln!(w, " (parked — enable with `set-host-enabled`)")?;
}
Ok(())
}
pub(super) async fn remove_host<W: Write + Send, DB: Database>(
pools: &TenantPools<DB>,
args: &[String],
w: &mut W,
) -> Result<(), TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let (slug, host) = slug_and_host(args, "remove-host")?;
org_host::remove_host(&pools.registry_pool(), &slug, &host)
.await
.map_err(explain)?;
writeln!(w, "unbound {host} from {slug}")?;
Ok(())
}
pub(super) async fn set_host_enabled<W: Write + Send, DB: Database>(
pools: &TenantPools<DB>,
args: &[String],
w: &mut W,
) -> Result<(), TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let (slug, host) = slug_and_host(args, "set-host-enabled")?;
let mut enabled: Option<bool> = None;
let mut iter = args.iter();
while let Some(flag) = iter.next() {
match flag.as_str() {
"--on" => set_direction(&mut enabled, true, ("--on", "--off"))?,
"--off" => set_direction(&mut enabled, false, ("--on", "--off"))?,
"--enabled" => {
let raw = next_value(&mut iter, "--enabled")?;
set_direction(&mut enabled, parse_bool(&raw)?, ("--on", "--off"))?;
}
other if other.starts_with("--") => {
return Err(TenancyError::Validation(format!(
"unknown flag `{other}` — set-host-enabled takes --on or --off"
)))
}
_ => {}
}
}
let enabled = enabled.ok_or_else(|| {
TenancyError::Validation("set-host-enabled requires --on or --off".into())
})?;
org_host::set_host_enabled(&pools.registry_pool(), &slug, &host, enabled)
.await
.map_err(explain)?;
writeln!(
w,
"{host} is now {} for {slug}",
if enabled { "served" } else { "parked" }
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_missing_slug_or_host_is_named_in_the_error() {
let none: Vec<String> = Vec::new();
let err = slug_and_host(&none, "add-host").expect_err("no slug");
assert!(err.to_string().contains("slug"), "{err}");
let only_slug = vec!["acme".to_owned()];
let err = slug_and_host(&only_slug, "add-host").expect_err("no host");
assert!(err.to_string().contains("hostname"), "{err}");
let both = vec!["acme".to_owned(), "shop.test".to_owned()];
assert_eq!(
slug_and_host(&both, "add-host").expect("ok"),
("acme".to_owned(), "shop.test".to_owned())
);
}
#[test]
fn flags_are_not_mistaken_for_positionals() {
let args = vec![
"--off".to_owned(),
"acme".to_owned(),
"shop.test".to_owned(),
];
assert_eq!(
slug_and_host(&args, "set-host-enabled").expect("ok"),
("acme".to_owned(), "shop.test".to_owned())
);
}
}