use std::borrow::Cow;
use std::path::Path;
use crate::bootstrap::{DaemonEndpoint, read_daemon_endpoint, read_daemon_endpoint_at};
pub fn daemon_url() -> String {
env_override(
std::env::var("GOBBY_DAEMON_URL").ok().as_deref(),
std::env::var("GOBBY_PORT").ok().as_deref(),
)
.unwrap_or_else(|| endpoint_to_url(&read_daemon_endpoint()))
}
pub fn daemon_url_at(path: &Path) -> String {
endpoint_to_url(&read_daemon_endpoint_at(path))
}
fn env_override(url: Option<&str>, port: Option<&str>) -> Option<String> {
if let Some(url) = url {
let url = url.trim();
if !url.is_empty() {
return Some(url.trim_end_matches('/').to_string());
}
}
port?
.trim()
.parse::<u16>()
.ok()
.map(|port| format!("http://127.0.0.1:{port}"))
}
fn endpoint_to_url(endpoint: &DaemonEndpoint) -> String {
let host = dial_host(&endpoint.host);
format!("http://{host}:{}", endpoint.port)
}
fn dial_host(host: &str) -> Cow<'_, str> {
match host.trim() {
"" | "0.0.0.0" | "::" | "::0" | "[::]" => Cow::Borrowed("127.0.0.1"),
host if host.contains(':') && !host.starts_with('[') => Cow::Owned(format!("[{host}]")),
host => Cow::Borrowed(host),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn write_bootstrap(contents: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempdir().unwrap();
let path = dir.path().join("bootstrap.yaml");
fs::write(&path, contents).unwrap();
(dir, path)
}
#[test]
fn default_url_when_file_missing() {
let dir = tempdir().unwrap();
let path = dir.path().join("nope.yaml");
assert_eq!(daemon_url_at(&path), "http://127.0.0.1:60887");
}
#[test]
fn wildcard_ipv4_normalizes_to_loopback() {
let (_dir, path) = write_bootstrap("daemon_port: 60887\nbind_host: 0.0.0.0\n");
assert_eq!(daemon_url_at(&path), "http://127.0.0.1:60887");
}
#[test]
fn wildcard_ipv6_normalizes_to_loopback() {
let (_dir, path) = write_bootstrap(
r#"daemon_port: 60887
bind_host: "::"
"#,
);
assert_eq!(daemon_url_at(&path), "http://127.0.0.1:60887");
}
#[test]
fn wildcard_ipv6_zero_normalizes_to_loopback() {
let (_dir, path) = write_bootstrap(
r#"daemon_port: 60887
bind_host: "::0"
"#,
);
assert_eq!(daemon_url_at(&path), "http://127.0.0.1:60887");
}
#[test]
fn localhost_passes_through() {
let (_dir, path) = write_bootstrap("daemon_port: 60887\nbind_host: localhost\n");
assert_eq!(daemon_url_at(&path), "http://localhost:60887");
}
#[test]
fn custom_port_and_host_compose() {
let (_dir, path) = write_bootstrap("daemon_port: 61234\nbind_host: 10.0.0.5\n");
assert_eq!(daemon_url_at(&path), "http://10.0.0.5:61234");
}
#[test]
fn bare_ipv6_literal_is_bracketed() {
let (_dir, path) = write_bootstrap(
r#"daemon_port: 61234
bind_host: "::1"
"#,
);
assert_eq!(daemon_url_at(&path), "http://[::1]:61234");
}
#[test]
fn bracketed_ipv6_literal_passes_through() {
let (_dir, path) = write_bootstrap(
r#"daemon_port: 61234
bind_host: "[::1]"
"#,
);
assert_eq!(daemon_url_at(&path), "http://[::1]:61234");
}
#[test]
fn env_override_url_beats_port() {
assert_eq!(
env_override(Some("http://override.invalid:1234"), Some("61999")),
Some("http://override.invalid:1234".to_string())
);
}
#[test]
fn env_override_url_trims_trailing_slashes() {
assert_eq!(
env_override(Some("http://override.invalid:1234/"), None),
Some("http://override.invalid:1234".to_string())
);
}
#[test]
fn env_override_empty_url_falls_back_to_port() {
assert_eq!(
env_override(Some(""), Some("61999")),
Some("http://127.0.0.1:61999".to_string())
);
}
#[test]
fn env_override_ignores_unparseable_or_empty_port() {
assert_eq!(env_override(None, Some("not-a-port")), None);
assert_eq!(env_override(None, Some("")), None);
assert_eq!(env_override(None, Some("70000")), None);
}
#[test]
fn env_override_absent_returns_none() {
assert_eq!(env_override(None, None), None);
}
#[test]
fn daemon_url_honors_env_contract_over_bootstrap() {
let _lock = crate::config::TEST_ENV_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let saved: Vec<(&str, Option<std::ffi::OsString>)> =
["GOBBY_DAEMON_URL", "GOBBY_PORT", "GOBBY_HOME"]
.map(|name| (name, std::env::var_os(name)))
.into();
let dir = tempdir().unwrap();
fs::write(dir.path().join("bootstrap.yaml"), "daemon_port: 61111\n").unwrap();
unsafe {
std::env::set_var("GOBBY_HOME", dir.path());
std::env::set_var("GOBBY_DAEMON_URL", "http://override.invalid:1234/");
std::env::set_var("GOBBY_PORT", "61999");
}
let with_url = daemon_url();
unsafe { std::env::remove_var("GOBBY_DAEMON_URL") };
let with_port = daemon_url();
unsafe { std::env::remove_var("GOBBY_PORT") };
let from_bootstrap = daemon_url();
unsafe {
for (name, value) in saved {
match value {
Some(value) => std::env::set_var(name, value),
None => std::env::remove_var(name),
}
}
}
assert_eq!(with_url, "http://override.invalid:1234");
assert_eq!(with_port, "http://127.0.0.1:61999");
assert_eq!(from_bootstrap, "http://127.0.0.1:61111");
}
}