tovuk 0.1.81

Deploy Rust workers, static frontends, and full-stack services to Tovuk.
use super::{
    super::{
        constants::DEFAULT_RUST_CHECK_COMMAND,
        frontend_checks::{frontend_build_command, frontend_check_command},
        project_kind::ProjectKind,
        project_layout::{default_build_command, default_check_command},
        resource_config::parse_resource_config,
        toml_values::{get_bool, get_section, get_string, get_u16, reject_unknown_section_keys},
    },
    model::{
        BackendConfig, BuildConfig, CapabilitiesConfig, CapabilityToggle, FrontendConfig,
        RunConfig, TovukConfig,
    },
};
use std::path::Path;

pub(crate) fn parse_tovuk_toml(
    source: &str,
    project_dir: &Path,
) -> std::result::Result<TovukConfig, String> {
    let table = source
        .parse::<toml::Table>()
        .map_err(|error| error.to_string())?;
    reject_unknown_root_keys(&table)?;
    let kind = parse_project_kind(&table)?;
    let capabilities_table = get_required_section(&table, "capabilities")?;
    let build_table = get_section(&table, "build")?;
    let run_table = get_section(&table, "run")?;
    let frontend_table = get_section(&table, "frontend")?;
    let backend_table = get_section(&table, "worker")?;
    let resources_table = get_section(&table, "resources")?;
    reject_unknown_config_sections(
        &capabilities_table,
        &build_table,
        &run_table,
        &frontend_table,
        &backend_table,
        &resources_table,
    )?;

    Ok(TovukConfig {
        name: get_string(&table, "name")?,
        capabilities: parse_capabilities_config(&capabilities_table)?,
        build: parse_build_config(&build_table, kind, project_dir)?,
        run: parse_run_config(&run_table)?,
        frontend: parse_frontend_config(&frontend_table, kind, project_dir)?,
        backend: parse_backend_config(&backend_table, kind)?,
        resources: parse_resource_config(&resources_table)?,
        kind,
    })
}

fn get_required_section(
    table: &toml::Table,
    key: &str,
) -> std::result::Result<toml::map::Map<String, toml::Value>, String> {
    if table.contains_key(key) {
        get_section(table, key)
    } else {
        Err(format!(
            "[{key}] is required and must explicitly set every Tovuk capability"
        ))
    }
}

fn parse_project_kind(table: &toml::Table) -> std::result::Result<ProjectKind, String> {
    let kind = get_string(table, "kind")?.unwrap_or_else(|| "rust_worker".to_owned());
    ProjectKind::parse(&kind)
}

fn reject_unknown_config_sections(
    capabilities: &toml::Table,
    build: &toml::Table,
    run: &toml::Table,
    frontend: &toml::Table,
    backend: &toml::Table,
    resources: &toml::Table,
) -> std::result::Result<(), String> {
    reject_unknown_section_keys(capabilities, "capabilities", &CapabilitiesConfig::KEYS)?;
    reject_unknown_section_keys(build, "build", &["command", "check", "output"])?;
    reject_unknown_section_keys(run, "run", &["command", "port", "health"])?;
    reject_unknown_section_keys(frontend, "frontend", &["root", "check", "build", "output"])?;
    reject_unknown_section_keys(
        backend,
        "worker",
        &["root", "check", "build", "command", "port", "health"],
    )?;
    reject_unknown_section_keys(
        resources,
        "resources",
        &["memory", "cpu", "idle_timeout_minutes"],
    )
}

fn parse_capabilities_config(
    table: &toml::Table,
) -> std::result::Result<CapabilitiesConfig, String> {
    Ok(CapabilitiesConfig {
        static_frontend: parse_toggle(table, "static_frontend")?,
        worker: parse_toggle(table, "worker")?,
        sqlite: parse_toggle(table, "sqlite")?,
        object_storage: parse_toggle(table, "object_storage")?,
        kv: parse_toggle(table, "kv")?,
        state: parse_toggle(table, "state")?,
        queue: parse_toggle(table, "queue")?,
        cron: parse_toggle(table, "cron")?,
        service_bindings: parse_toggle(table, "service_bindings")?,
        secrets: parse_toggle(table, "secrets")?,
        custom_domains: parse_toggle(table, "custom_domains")?,
        logs: parse_toggle(table, "logs")?,
        builds: parse_toggle(table, "builds")?,
        usage_caps: parse_toggle(table, "usage_caps")?,
        billing: parse_toggle(table, "billing")?,
        support: parse_toggle(table, "support")?,
        abuse: parse_toggle(table, "abuse")?,
    })
}

fn parse_toggle(table: &toml::Table, key: &str) -> std::result::Result<CapabilityToggle, String> {
    required_bool(table, key).map(CapabilityToggle::from_bool)
}

fn required_bool(table: &toml::Table, key: &str) -> std::result::Result<bool, String> {
    get_bool(table, key)?.ok_or_else(|| format!("[capabilities].{key} must be true or false"))
}

fn parse_build_config(
    table: &toml::Table,
    kind: ProjectKind,
    project_dir: &Path,
) -> std::result::Result<BuildConfig, String> {
    Ok(BuildConfig {
        check: get_string(table, "check")?
            .unwrap_or_else(|| default_check_command(kind, project_dir)),
        command: get_string(table, "command")?
            .unwrap_or_else(|| default_build_command(kind, project_dir)),
        output: if kind.is_static_frontend() {
            Some(get_string(table, "output")?.unwrap_or_else(|| "dist".to_owned()))
        } else {
            get_string(table, "output")?
        },
    })
}

fn parse_run_config(table: &toml::Table) -> std::result::Result<RunConfig, String> {
    Ok(RunConfig {
        command: get_string(table, "command")?,
        port: get_u16(table, "port")?.unwrap_or(3000),
        health: get_string(table, "health")?.unwrap_or_else(|| "/healthz".to_owned()),
    })
}

fn parse_frontend_config(
    table: &toml::Table,
    kind: ProjectKind,
    project_dir: &Path,
) -> std::result::Result<FrontendConfig, String> {
    if !kind.is_fullstack() {
        return Ok(FrontendConfig::default());
    }
    let root = get_string(table, "root")?;
    let frontend_dir = root
        .as_deref()
        .map_or_else(|| project_dir.to_path_buf(), |root| project_dir.join(root));
    Ok(FrontendConfig {
        root,
        check: Some(
            get_string(table, "check")?.unwrap_or_else(|| frontend_check_command(&frontend_dir)),
        ),
        build: Some(
            get_string(table, "build")?.unwrap_or_else(|| frontend_build_command(&frontend_dir)),
        ),
        output: Some(get_string(table, "output")?.unwrap_or_else(|| "dist".to_owned())),
    })
}

fn parse_backend_config(
    table: &toml::Table,
    kind: ProjectKind,
) -> std::result::Result<BackendConfig, String> {
    if !kind.is_fullstack() {
        return Ok(BackendConfig::default());
    }
    Ok(BackendConfig {
        root: get_string(table, "root")?,
        check: Some(
            get_string(table, "check")?.unwrap_or_else(|| DEFAULT_RUST_CHECK_COMMAND.to_owned()),
        ),
        build: Some(
            get_string(table, "build")?.unwrap_or_else(|| "cargo build --release".to_owned()),
        ),
        command: get_string(table, "command")?,
        port: Some(get_u16(table, "port")?.unwrap_or(3000)),
        health: Some(get_string(table, "health")?.unwrap_or_else(|| "/api/healthz".to_owned())),
    })
}

fn reject_unknown_root_keys(
    table: &toml::map::Map<String, toml::Value>,
) -> std::result::Result<(), String> {
    let allowed = [
        "name",
        "kind",
        "capabilities",
        "build",
        "run",
        "frontend",
        "worker",
        "resources",
    ];
    for key in table.keys() {
        if !allowed.contains(&key.as_str()) {
            return Err(format!("unsupported root key {key}"));
        }
    }
    Ok(())
}