chio-proof-room 0.1.2

Focused Proof Room quickstart server
use std::{env, ffi::OsString, path::PathBuf, process::ExitCode};

use chio_proof_room::{
    parse_listen_addr, serve_proof_room, verify_proof_room_quickstart, ProofRoomServeConfig,
};

#[tokio::main]
async fn main() -> ExitCode {
    match parse_config(env::args_os().skip(1)) {
        Ok(ProofRoomCommand::Help) => {
            println!("{}", usage());
            ExitCode::SUCCESS
        }
        Ok(ProofRoomCommand::Serve(config)) => match serve_proof_room(config).await {
            Ok(()) => ExitCode::SUCCESS,
            Err(error) => {
                eprintln!("{error}");
                ExitCode::FAILURE
            }
        },
        Ok(ProofRoomCommand::VerifyOnly {
            bundle,
            doctor_report,
        }) => match verify_proof_room_quickstart(&bundle, doctor_report.as_deref()) {
            Ok(()) => ExitCode::SUCCESS,
            Err(error) => {
                eprintln!("{error}");
                ExitCode::FAILURE
            }
        },
        Err(error) => {
            eprintln!("{error}");
            ExitCode::from(2)
        }
    }
}

enum ProofRoomCommand {
    Help,
    Serve(ProofRoomServeConfig),
    VerifyOnly {
        bundle: PathBuf,
        doctor_report: Option<PathBuf>,
    },
}

fn parse_config<I>(args: I) -> Result<ProofRoomCommand, String>
where
    I: IntoIterator<Item = OsString>,
{
    let mut bundle = None;
    let mut ui_dir = env::var_os("CHIO_PROOF_ROOM_UI_DIR").map(PathBuf::from);
    let mut fixture_root = env::var_os("CHIO_PROOF_FIXTURE_ROOT").map(PathBuf::from);
    let mut listen = "0.0.0.0:7391".to_string();
    let mut doctor_report = None;
    let mut verify_only = false;
    let mut args = args.into_iter();

    while let Some(arg) = args.next() {
        match arg.to_string_lossy().as_ref() {
            "--bundle" => {
                bundle = Some(next_path_arg(&mut args, "--bundle")?);
            }
            "--ui-dir" => {
                ui_dir = Some(next_path_arg(&mut args, "--ui-dir")?);
            }
            "--fixture-root" => {
                fixture_root = Some(next_path_arg(&mut args, "--fixture-root")?);
            }
            "--listen" => {
                listen = next_string_arg(&mut args, "--listen")?;
            }
            "--doctor-report" => {
                doctor_report = Some(next_path_arg(&mut args, "--doctor-report")?);
            }
            "--verify-only" => {
                verify_only = true;
            }
            "--help" | "-h" => {
                return Ok(ProofRoomCommand::Help);
            }
            other => {
                return Err(format!("unknown argument: {other}\n{}", usage()));
            }
        }
    }

    let bundle = bundle.ok_or_else(|| format!("missing required --bundle\n{}", usage()))?;
    if verify_only {
        return Ok(ProofRoomCommand::VerifyOnly {
            bundle,
            doctor_report,
        });
    }

    let ui_dir = ui_dir.ok_or_else(|| format!("missing required --ui-dir\n{}", usage()))?;
    let listen = parse_listen_addr(&listen).map_err(|error| error.to_string())?;

    Ok(ProofRoomCommand::Serve(ProofRoomServeConfig {
        bundle,
        ui_dir,
        listen,
        doctor_report,
        fixture_root,
    }))
}

fn next_path_arg<I>(args: &mut I, flag: &str) -> Result<PathBuf, String>
where
    I: Iterator<Item = OsString>,
{
    args.next()
        .map(PathBuf::from)
        .ok_or_else(|| format!("{flag} requires a path"))
}

fn next_string_arg<I>(args: &mut I, flag: &str) -> Result<String, String>
where
    I: Iterator<Item = OsString>,
{
    let value = args
        .next()
        .ok_or_else(|| format!("{flag} requires a value"))?;
    value
        .into_string()
        .map_err(|_| format!("{flag} must be valid UTF-8"))
}

fn usage() -> &'static str {
    "usage: chio-proof-room --bundle <proof-room-bundle> --ui-dir <dashboard-dist> [--fixture-root <fixtures/proof-room>] [--listen <addr>] [--doctor-report <path>] [--verify-only]"
}