use std::collections::HashMap;
use std::path::Path;
use anyhow::{Context, Result};
use slave_gateway::{Config, Routes};
pub const CONFIG: &str = "config/apps.toml";
pub struct Env {
dotenv: HashMap<String, String>,
}
impl Env {
pub fn load(root: &Path) -> Result<Self> {
let path = root.join(".env");
let dotenv = match dotenvy::from_path_iter(&path) {
Ok(iter) => iter
.collect::<Result<_, _>>()
.with_context(|| format!("failed to parse {}", path.display()))?,
Err(err) if err.not_found() => HashMap::new(),
Err(err) => {
return Err(err).with_context(|| format!("failed to read {}", path.display()));
}
};
Ok(Self { dotenv })
}
pub fn get(&self, key: &str) -> Option<String> {
std::env::var(key)
.ok()
.or_else(|| self.dotenv.get(key).cloned())
}
}
pub fn connect_address(host: &str, port: u16) -> String {
let host = match host {
"0.0.0.0" | "" => "127.0.0.1",
"::" | "[::]" => "[::1]",
host => host,
};
format!("{host}:{port}")
}
pub struct Apps {
pub config: Config,
pub routes: Routes,
pub listen: String,
}
impl Apps {
pub fn load(root: &Path, env: &Env, mcp: &str) -> Result<Option<Self>> {
let path = root.join(CONFIG);
if !path.is_file() {
return Ok(None);
}
let config = Config::load(&path)?;
let routes = Routes::new(&config, mcp);
let listen = env
.get("GATEWAY_LISTEN")
.unwrap_or_else(|| config.gateway.listen.clone());
Ok(Some(Self {
config,
routes,
listen,
}))
}
pub fn url(&self, host: Option<&str>, path: Option<&str>) -> String {
gateway_url(&self.listen, host, path)
}
}
pub fn gateway_url(listen: &str, host: Option<&str>, path: Option<&str>) -> String {
let port = listen.rsplit_once(':').map_or("", |(_, port)| port);
let host = host
.unwrap_or("localhost")
.replace('{', "<")
.replace('}', ">");
let path = path.unwrap_or("");
match port {
"80" | "" => format!("http://{host}{path}"),
port => format!("http://{host}:{port}{path}"),
}
}
pub fn append_domain(
text: &str,
host: Option<&str>,
path: Option<&str>,
app: &str,
port: u16,
) -> String {
let mut block = String::from("[[domain]]\n");
if let Some(host) = host {
block.push_str(&format!("host = \"{host}\"\n"));
}
if let Some(path) = path {
block.push_str(&format!("path = \"{path}\"\n"));
}
block.push_str(&format!("app = \"{app}\"\nport = {port}\n"));
format!("{}\n\n{block}", text.trim_end())
}
#[cfg(test)]
mod tests {
use super::*;
const BASE: &str = "# routes\n[gateway]\nlisten = \"127.0.0.1:8000\"\n\n[[domain]]\nhost = \"mcp.localhost\"\napp = \"mcp\"\n";
#[test]
fn appends_entries_keeping_comments() {
let text = append_domain(BASE, Some("localhost"), None, "site", 4000);
let text = append_domain(&text, None, Some("/admin"), "dashboard", 4001);
let text = append_domain(&text, Some("localhost"), Some("/panel"), "panel", 4002);
let config = Config::parse(&text).unwrap();
let labels: Vec<String> = config.domains.iter().map(|d| d.label()).collect();
assert_eq!(
labels,
["mcp.localhost", "localhost", "*/admin", "localhost/panel"]
);
assert!(text.starts_with("# routes\n"), "{text}");
}
#[test]
fn builds_gateway_urls() {
assert_eq!(
gateway_url("127.0.0.1:8000", Some("{toko}.localhost"), None),
"http://<toko>.localhost:8000"
);
assert_eq!(
gateway_url("127.0.0.1:8000", None, Some("/console")),
"http://localhost:8000/console"
);
assert_eq!(
gateway_url("0.0.0.0:80", Some("example.com"), None),
"http://example.com"
);
}
#[test]
fn connect_addresses_avoid_wildcards() {
assert_eq!(connect_address("0.0.0.0", 3000), "127.0.0.1:3000");
assert_eq!(connect_address("10.0.0.2", 3000), "10.0.0.2:3000");
}
}