use std::fs;
use anyhow::{Context, Result, anyhow, bail};
use slave_gateway::Config;
use slave_gateway::routes::HostPattern;
use crate::cli::{MakeAppArgs, Stack};
use crate::{apps, generator, project, templates};
const FIRST_PORT: u16 = 4000;
pub fn run(args: &MakeAppArgs) -> Result<()> {
let name = args.name.as_str();
slave_gateway::validate_app_name(name).map_err(|err| anyhow!(err))?;
let (host, path) = (args.domain.as_deref(), args.path.as_deref());
if host.is_none() && path.is_none() {
bail!("route the app with --domain <host>, --path </prefix>, or both");
}
if let Some(host) = host {
HostPattern::parse(host).map_err(|err| anyhow!("--domain: {err}"))?;
}
if let Some(path) = path {
slave_gateway::routes::validate_path(path).map_err(|err| anyhow!("--path: {err}"))?;
}
let route = slave_gateway::routes::label(host, path);
let root = project::find_root()?;
let package = project::package_name(&root)?;
if [package.as_str(), "gateway", "migrate"].contains(&name) {
bail!("`{name}` is already a binary of this project; choose another app name");
}
let config_path = root.join(apps::CONFIG);
let text = fs::read_to_string(&config_path).with_context(|| {
format!(
"{} not found; projects created before slave 0.4 need it (copy it from a new project)",
apps::CONFIG
)
})?;
let config = Config::parse(&text).with_context(|| format!("invalid {}", apps::CONFIG))?;
if let Some(domain) = config
.domains
.iter()
.find(|d| d.host.as_deref() == host && d.path.as_deref() == path)
{
bail!(
"`{route}` is already routed to `{}` in {}",
domain.app,
apps::CONFIG
);
}
if config.domains.iter().any(|d| d.app == name) {
bail!(
"app `{name}` already exists in {}; add another [[domain]] for it there",
apps::CONFIG
);
}
let app_dir = root.join("apps").join(name);
if app_dir.exists() {
bail!("apps/{name} already exists");
}
let port = match args.port {
Some(0) => bail!("--port must be between 1 and 65535"),
Some(port) => port,
None => next_port(&config),
};
if let Some(domain) = config.domains.iter().find(|d| d.port == Some(port)) {
bail!("port {port} is already used by app `{}`", domain.app);
}
let updated = apps::append_domain(&text, host, path, name, port);
Config::parse(&updated).with_context(|| format!("cannot route `{route}` to `{name}`"))?;
let port_text = port.to_string();
let title = title_case(name);
let vars = [("app", name), ("title", &title), ("port", &port_text)];
let files = match args.stack {
Stack::Axum => templates::AXUM_APP,
Stack::Dioxus => templates::DIOXUS_APP,
};
for file in files {
let path = app_dir.join(file.path);
generator::write_file(&path, &templates::render(file.contents, &vars), false)?;
println!(" created apps/{name}/{}", file.path);
}
fs::write(&config_path, updated)
.with_context(|| format!("failed to update {}", config_path.display()))?;
println!(
" registered {route} → apps/{name} (port {port}) in {}",
apps::CONFIG
);
println!("App {name} [apps/{name}] created successfully.");
if args.stack == Stack::Dioxus && !crate::dioxus::dx_installed() {
println!(
"note: Dioxus apps are built with `dx`; {}",
crate::dioxus::INSTALL_HINT
);
}
println!(
"Run `slave serve` and open {}",
apps::gateway_url(&config.gateway.listen, host, path)
);
Ok(())
}
fn next_port(config: &Config) -> u16 {
config
.domains
.iter()
.filter_map(|domain| domain.port)
.filter(|&port| port >= FIRST_PORT)
.max()
.map_or(FIRST_PORT, |port| port.saturating_add(1))
}
fn title_case(name: &str) -> String {
name.split(['-', '_'])
.filter(|word| !word.is_empty())
.map(|word| {
let mut chars = word.chars();
chars
.next()
.map(|first| first.to_ascii_uppercase().to_string() + chars.as_str())
.unwrap_or_default()
})
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn titles() {
assert_eq!(title_case("dashboard"), "Dashboard");
assert_eq!(title_case("admin-panel"), "Admin Panel");
assert_eq!(title_case("shop_front2"), "Shop Front2");
}
#[test]
fn ports_follow_the_highest_app_port() {
let config = |text: &str| Config::parse(text).unwrap();
assert_eq!(next_port(&config("")), 4000);
let two = "[[domain]]\nhost = \"a.localhost\"\napp = \"a\"\nport = 4000\n\
[[domain]]\nhost = \"b.localhost\"\napp = \"b\"\nport = 4007\n";
assert_eq!(next_port(&config(two)), 4008);
}
}