tovuk 0.1.67

Deploy Rust workers, static frontends, and worker-static apps to Tovuk.
use super::{
    super::{
        command_policy::{
            command_name_from_token, command_tokens, is_noop_command,
            uses_javascript_backend_runtime,
        },
        constants::RUST_STRICT_CLIPPY_DENY_LINTS,
        frontend_checks::{
            has_frontend_install_command, has_frontend_script_run, uses_javascript_linter,
        },
        project::{is_dns_safe_name, is_safe_relative_directory, is_safe_relative_path},
        project_kind::ProjectKind,
        resource_config::validate_resource_config,
    },
    model::TovukConfig,
};

pub(crate) fn validate_config(config: &TovukConfig) -> std::result::Result<(), String> {
    let name = config.name.as_deref().unwrap_or_default();
    if !is_dns_safe_name(name) {
        return Err("name must be lowercase DNS-safe text up to 48 characters".to_owned());
    }
    if config.kind.is_worker_static() {
        validate_fullstack_config(config)?;
    } else {
        validate_build_config(config)?;
        if config.kind.is_static_frontend() {
            validate_output(config.build.output.as_deref(), "[build].output")?;
        } else {
            validate_rust_worker_config(config)?;
        }
    }
    Ok(())
}

fn validate_build_config(config: &TovukConfig) -> std::result::Result<(), String> {
    if config.build.command.trim().is_empty() {
        return Err("[build].command is required".to_owned());
    }
    if config.build.check.trim().is_empty() {
        return Err("[build].check is required".to_owned());
    }
    validate_check_command(config.kind, &config.build.check)?;
    if config.kind.is_rust_worker() {
        validate_rust_build_command(&config.build.command)?;
    }
    Ok(())
}

fn validate_rust_worker_config(config: &TovukConfig) -> std::result::Result<(), String> {
    if config.build.output.is_some() {
        return Err("[build].output is only valid for static_frontend".to_owned());
    }
    validate_rust_run_command(config.run.command.as_deref())?;
    validate_port(config.run.port, "[run].port")?;
    validate_health(&config.run.health, "[run].health")?;
    validate_resource_config(&config.resources)
}

fn validate_fullstack_config(config: &TovukConfig) -> std::result::Result<(), String> {
    let backend_root = validate_root(config.backend.root.as_deref(), "[worker].root")?;
    let frontend_root = validate_root(config.frontend.root.as_deref(), "[frontend].root")?;
    if backend_root == frontend_root {
        return Err("[worker].root and [frontend].root must be different directories".to_owned());
    }
    validate_rust_check_command(require_config_command(
        config.backend.check.as_deref(),
        "[worker].check",
    )?)?;
    validate_rust_build_command(require_config_command(
        config.backend.build.as_deref(),
        "[worker].build",
    )?)?;
    validate_rust_run_command(config.backend.command.as_deref())?;
    validate_port(config.backend.port.unwrap_or(0), "[worker].port")?;
    validate_health(
        config.backend.health.as_deref().unwrap_or_default(),
        "[worker].health",
    )?;
    validate_frontend_check_command(require_config_command(
        config.frontend.check.as_deref(),
        "[frontend].check",
    )?)?;
    require_config_command(config.frontend.build.as_deref(), "[frontend].build")?;
    validate_output(config.frontend.output.as_deref(), "[frontend].output")?;
    validate_resource_config(&config.resources)
}

fn require_config_command<'a>(
    value: Option<&'a str>,
    field: &str,
) -> std::result::Result<&'a str, String> {
    value
        .filter(|value| !value.trim().is_empty())
        .ok_or_else(|| format!("{field} is required"))
}

fn validate_root<'a>(value: Option<&'a str>, field: &str) -> std::result::Result<&'a str, String> {
    let value = value
        .ok_or_else(|| format!("{field} must be a safe relative directory such as api or web"))?;
    if is_safe_relative_path(value) {
        Ok(value)
    } else {
        Err(format!(
            "{field} must be a safe relative directory such as api or web"
        ))
    }
}

fn validate_port(value: u16, field: &str) -> std::result::Result<(), String> {
    if value == 0 {
        Err(format!("{field} must be between 1 and 65535"))
    } else {
        Ok(())
    }
}

fn validate_health(value: &str, field: &str) -> std::result::Result<(), String> {
    if value.starts_with('/') {
        Ok(())
    } else {
        Err(format!("{field} must be an absolute path"))
    }
}

fn validate_output(value: Option<&str>, field: &str) -> std::result::Result<(), String> {
    if value.is_some_and(is_safe_relative_directory) {
        Ok(())
    } else {
        Err(format!(
            "{field} must be a safe relative directory like dist or ."
        ))
    }
}

fn validate_check_command(kind: ProjectKind, command: &str) -> std::result::Result<(), String> {
    if kind.is_static_frontend() {
        validate_frontend_check_command(command)
    } else {
        validate_rust_check_command(command)
    }
}

fn validate_rust_check_command(command: &str) -> std::result::Result<(), String> {
    let required = [
        "cargo fmt --all --check",
        "cargo check --locked --release --all-targets --all-features",
        "cargo test --locked --release --all-targets --all-features",
        "cargo clippy --locked --release --all-targets --all-features",
        "-D warnings",
    ];
    let lint_ok = RUST_STRICT_CLIPPY_DENY_LINTS
        .iter()
        .all(|lint| command.contains(&format!("-D {lint}")));
    if required.iter().all(|fragment| command.contains(fragment)) && lint_ok {
        Ok(())
    } else {
        Err("[build].check must run rustfmt, locked release-mode cargo check, locked release-mode tests, and strict Clippy resource lints".to_owned())
    }
}

fn validate_rust_build_command(command: &str) -> std::result::Result<(), String> {
    if uses_javascript_backend_runtime(command) {
        return Err(
            "Rust worker build commands cannot invoke JavaScript or TypeScript runtimes; use cargo build --release"
                .to_owned(),
        );
    }
    let tokens = command_tokens(command);
    if tokens
        .iter()
        .any(|token| command_name_from_token(token) == "cargo")
        && tokens.iter().any(|token| token == "build")
        && tokens.iter().any(|token| token == "--release")
    {
        Ok(())
    } else {
        Err("Rust worker build commands must run cargo build --release".to_owned())
    }
}

fn validate_rust_run_command(command: Option<&str>) -> std::result::Result<(), String> {
    let value = command.unwrap_or_default();
    if uses_javascript_backend_runtime(value) {
        return Err(
            "Rust worker runtime commands cannot invoke JavaScript or TypeScript runtimes; run ./target/release/<binary> instead"
                .to_owned(),
        );
    }
    if command_tokens(value)
        .iter()
        .any(|token| token.contains("target/release/"))
    {
        Ok(())
    } else {
        Err("Rust worker runtime commands must start a binary under ./target/release/".to_owned())
    }
}

fn validate_frontend_check_command(command: &str) -> std::result::Result<(), String> {
    if is_noop_command(command) {
        return Ok(());
    }
    if uses_javascript_linter(command) {
        return Err("[build].check must not run JavaScript-based lint or format tooling; use oxlint, biome, or deno lint".to_owned());
    }
    let tokens = command_tokens(command);
    if has_frontend_install_command(&tokens)
        && has_frontend_script_run(&tokens, "typecheck")
        && has_frontend_script_run(&tokens, "lint")
    {
        Ok(())
    } else {
        Err("[build].check must install dependencies and run package scripts, for example `bun ci && bun run typecheck && bun run lint` or `npm ci --prefer-offline --no-audit --fund=false && npm run typecheck && npm run lint`".to_owned())
    }
}