arqen 0.17.0

Rust-first backend toolkit for HTTP services, agents, automation, jobs, and storage adapters
Documentation
use std::net::SocketAddr;

use super::exit;
use super::output::Output;

#[allow(clippy::too_many_arguments)]
pub fn serve_dev(
    file: Option<&str>,
    host: Option<&str>,
    port: Option<u16>,
    log: Option<&str>,
    storage: Option<&str>,
    log_format: Option<&crate::config::LogFormat>,
    watch: bool,
    output: &Output,
) -> i32 {
    if is_arqen_application() {
        return run_application(
            "development",
            file,
            host,
            port,
            log,
            storage,
            log_format,
            watch,
            output,
        );
    }
    if watch {
        return run_watch(output);
    }
    let cli = crate::config::CliOverrides {
        host: host.map(str::to_owned),
        port,
        log_level: log.map(str::to_owned),
        log_format: log_format.copied().or_else(|| {
            std::env::var_os("ARQEN_LOG_FORMAT")
                .is_none()
                .then_some(crate::config::LogFormat::Compact)
        }),
        storage_mode: storage.map(str::to_owned),
    };

    let path = file.map(std::path::PathBuf::from);
    let config = match crate::config::AppConfig::load_with_file(cli, path.as_deref()) {
        Ok(c) => c,
        Err(e) => {
            output.print_error(&e.to_string());
            return exit::CONFIG;
        }
    };

    let storage = config.storage.mode.as_str();
    let addr: SocketAddr = match config.address() {
        Ok(a) => a,
        Err(e) => {
            output.print_error(&e.to_string());
            return exit::CONFIG;
        }
    };

    crate::logging::init_logging_with_config(&config.logging);

    let state = match crate::AppState::builder()
        .with_config(config.clone())
        .with_storage_mode(storage)
        .with_tool_registry(crate::ToolRegistry::new(
            "arqen-app",
            env!("CARGO_PKG_VERSION"),
            "An Arqen application",
            storage,
        ))
        .build()
    {
        Ok(s) => s,
        Err(e) => {
            output.print_error(&e.to_string());
            return exit::RUNTIME;
        }
    };
    let router = crate::http::create_router_with_state(state);

    output.print_banner(&addr, storage, "development");

    let rt = match tokio::runtime::Runtime::new() {
        Ok(r) => r,
        Err(e) => {
            output.print_error(&e.to_string());
            return exit::RUNTIME;
        }
    };
    if let Err(e) = rt.block_on(crate::http::start_server(addr, router)) {
        output.print_error(&e.to_string());
        return exit::RUNTIME;
    }
    exit::SUCCESS
}

fn is_arqen_application() -> bool {
    let Ok(manifest) = std::fs::read_to_string("Cargo.toml") else {
        return false;
    };
    manifest.lines().any(|line| {
        let line = line.trim_start();
        line.starts_with("arqen =") || line.starts_with("arqen = {")
    })
}

#[allow(clippy::too_many_arguments)]
fn run_application(
    mode: &str,
    file: Option<&str>,
    host: Option<&str>,
    port: Option<u16>,
    log: Option<&str>,
    storage: Option<&str>,
    log_format: Option<&crate::config::LogFormat>,
    watch: bool,
    output: &Output,
) -> i32 {
    let mut command = std::process::Command::new("cargo");
    if watch {
        command.args(["watch", "--why", "-x", "run"]);
    } else {
        command.args(["run", "--quiet"]);
    }
    if let Some(file) = file {
        command.env("ARQEN_CONFIG_FILE", file);
    }
    if let Some(host) = host {
        command.env("ARQEN_HOST", host);
    }
    if let Some(port) = port {
        command.env("ARQEN_PORT", port.to_string());
    }
    if let Some(log) = log {
        command.env("ARQEN_LOG_LEVEL", log);
    }
    if let Some(storage) = storage {
        command.env("ARQEN_STORAGE_MODE", storage);
    }
    if let Some(format) = log_format {
        let format = match format {
            crate::config::LogFormat::Pretty => "pretty",
            crate::config::LogFormat::Compact => "compact",
            crate::config::LogFormat::Json => "json",
        };
        command.env("ARQEN_LOG_FORMAT", format);
    } else if std::env::var_os("ARQEN_LOG_FORMAT").is_none() {
        command.env(
            "ARQEN_LOG_FORMAT",
            if mode == "production" {
                "json"
            } else {
                "compact"
            },
        );
    }
    if mode == "production" {
        command.env("ARQEN_LAUNCH_MODE", "production");
    }
    output.print(&format!(
        "Starting application in {mode} mode{}",
        if watch { " with automatic reload" } else { "" }
    ));

    match command.status() {
        Ok(status) if status.success() => exit::SUCCESS,
        Ok(status) => {
            output.print_error(&format!(
                "application process exited with {}",
                status
                    .code()
                    .map_or_else(|| "a signal".to_string(), |code| format!("code {code}"))
            ));
            status.code().unwrap_or(exit::RUNTIME)
        }
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            output.print_error(
                "application launch requires Cargo to be installed and available on PATH",
            );
            exit::CONFIG
        }
        Err(error) => {
            output.print_error(&format!("failed to start application: {error}"));
            exit::RUNTIME
        }
    }
}

fn run_watch(output: &Output) -> i32 {
    let mut command = std::process::Command::new("cargo");
    command.args(["watch", "--why", "-x", "run"]);
    if output.is_verbose() && !output.is_quiet() {
        output.print_verbose("starting cargo watch -x run");
    }
    match command.status() {
        Ok(status) if status.success() => exit::SUCCESS,
        Ok(status) => status.code().unwrap_or(exit::RUNTIME),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            output.print_error(
                "automatic reload requires 'cargo-watch'; install it with `cargo install cargo-watch`",
            );
            exit::CONFIG
        }
        Err(error) => {
            output.print_error(&format!("failed to start automatic reload: {error}"));
            exit::RUNTIME
        }
    }
}

#[allow(clippy::too_many_arguments)]
pub fn serve_start(
    file: Option<&str>,
    host: Option<&str>,
    port: Option<u16>,
    log: Option<&str>,
    storage: Option<&str>,
    log_format: Option<&crate::config::LogFormat>,
    skip_schema_validation: bool,
    output: &Output,
) -> i32 {
    let cli = crate::config::CliOverrides {
        host: host.map(str::to_owned),
        port,
        log_level: log.map(str::to_owned),
        log_format: Some(
            log_format
                .copied()
                .unwrap_or(crate::config::LogFormat::Json),
        ),
        storage_mode: storage.map(str::to_owned),
    };

    let path = file.map(std::path::PathBuf::from);
    let config = match crate::config::AppConfig::load_with_file(cli, path.as_deref()) {
        Ok(c) => c,
        Err(e) => {
            output.print_error(&e.to_string());
            return exit::CONFIG;
        }
    };

    if let Err(error) = config.validate_production_with_schema_validation(!skip_schema_validation) {
        output.print_error(&format!("production configuration is unsafe: {error}"));
        return exit::CONFIG;
    }

    if is_arqen_application() {
        return run_application(
            "production",
            file,
            host,
            port,
            log,
            storage,
            log_format,
            false,
            output,
        );
    }

    let storage = config.storage.mode.as_str();
    let addr: SocketAddr = match config.address() {
        Ok(a) => a,
        Err(e) => {
            output.print_error(&e.to_string());
            return exit::CONFIG;
        }
    };

    crate::logging::init_logging_with_config(&config.logging);

    let state = match crate::AppState::builder()
        .with_config(config.clone())
        .with_storage_mode(storage)
        .with_tool_registry(crate::ToolRegistry::new(
            "arqen-app",
            env!("CARGO_PKG_VERSION"),
            "An Arqen application",
            storage,
        ))
        .build()
    {
        Ok(s) => s,
        Err(e) => {
            output.print_error(&e.to_string());
            return exit::RUNTIME;
        }
    };
    let router = crate::http::create_router_with_state(state);

    output.print_banner(&addr, storage, "production");

    let rt = match tokio::runtime::Runtime::new() {
        Ok(r) => r,
        Err(e) => {
            output.print_error(&e.to_string());
            return exit::RUNTIME;
        }
    };
    if let Err(e) = rt.block_on(crate::http::start_server(addr, router)) {
        output.print_error(&e.to_string());
        return exit::RUNTIME;
    }
    exit::SUCCESS
}