tovuk 0.1.67

Deploy Rust workers, static frontends, and worker-static apps 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_section, get_string, get_u16, reject_unknown_section_keys},
    },
    model::{BackendConfig, BuildConfig, 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 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(
        &build_table,
        &run_table,
        &frontend_table,
        &backend_table,
        &resources_table,
    )?;

    Ok(TovukConfig {
        name: get_string(&table, "name")?,
        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 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(
    build: &toml::Table,
    run: &toml::Table,
    frontend: &toml::Table,
    backend: &toml::Table,
    resources: &toml::Table,
) -> std::result::Result<(), String> {
    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_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_worker_static() {
        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_worker_static() {
        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",
        "build",
        "run",
        "frontend",
        "worker",
        "resources",
    ];
    for key in table.keys() {
        if !allowed.contains(&key.as_str()) {
            return Err(format!("unsupported root key {key}"));
        }
    }
    Ok(())
}