rok-cli 0.6.1

Developer CLI for rok-based Axum applications
//! `rok api:test` — interactive API client (M7.4).

pub fn run(
    method: Option<&str>,
    url: Option<&str>,
    data: Option<&str>,
    req_headers: Vec<String>,
    json_mode: bool,
) -> anyhow::Result<()> {
    let method = method.unwrap_or("GET").to_uppercase();
    let Some(url) = url else {
        print_usage();
        return Ok(());
    };

    println!("rok api:test — {method} {url}");
    println!("{}", "".repeat(60));

    let (status, body) = send_request(&method, url, data, &req_headers)?;

    println!("Status: {status}");
    println!();

    if json_mode {
        println!("{body}");
    } else {
        match serde_json::from_str::<serde_json::Value>(&body) {
            Ok(v) => println!("{}", serde_json::to_string_pretty(&v)?),
            Err(_) => println!("{body}"),
        }
    }

    Ok(())
}

fn apply_headers(
    mut req: ureq::RequestBuilder<ureq::typestate::WithoutBody>,
    ua: &str,
    headers: &[String],
) -> ureq::RequestBuilder<ureq::typestate::WithoutBody> {
    req = req.header("User-Agent", ua).header("Accept", "application/json");
    for h in headers {
        if let Some((k, v)) = h.split_once(':') {
            req = req.header(k.trim(), v.trim());
        }
    }
    req
}

fn send_request(
    method: &str,
    url: &str,
    data: Option<&str>,
    headers: &[String],
) -> anyhow::Result<(u16, String)> {
    let ua = format!("rok-cli/{}", env!("CARGO_PKG_VERSION"));

    // All methods: GET/DELETE without body, POST/PUT/PATCH with JSON body.
    match method {
        "GET" | "HEAD" | "DELETE" => {
            let req_base = if method == "DELETE" { ureq::delete(url) } else { ureq::get(url) };
            let req = apply_headers(req_base, &ua, headers);
            let mut resp = req.call()?;
            let status = resp.status().as_u16();
            let body = resp.body_mut().read_to_string()?;
            Ok((status, body))
        }
        _ => {
            let json_val: serde_json::Value =
                data.and_then(|s| serde_json::from_str(s).ok()).unwrap_or(serde_json::Value::Null);

            let req_base = if method == "PUT" {
                ureq::put(url)
            } else if method == "PATCH" {
                ureq::patch(url)
            } else {
                ureq::post(url)
            };

            // Add headers to a get-style builder first to reuse helper, then rebuild
            let mut req = req_base
                .header("User-Agent", &ua)
                .header("Accept", "application/json")
                .header("Content-Type", "application/json");
            for h in headers {
                if let Some((k, v)) = h.split_once(':') {
                    req = req.header(k.trim(), v.trim());
                }
            }
            let mut resp = req.send_json(&json_val)?;
            let status = resp.status().as_u16();
            let body = resp.body_mut().read_to_string()?;
            Ok((status, body))
        }
    }
}

fn print_usage() {
    println!("rok api:test — interactive API client");
    println!();
    println!("Usage:");
    println!("  rok api:test <URL> [options]");
    println!();
    println!("Options:");
    println!("  --method <METHOD>    HTTP method (default: GET)");
    println!("  --data <JSON>        Request body (JSON string)");
    println!("  --header <K:V>       Add a request header (repeatable)");
    println!();
    println!("Examples:");
    println!("  rok api:test http://localhost:3000/users");
    println!("  rok api:test http://localhost:3000/users --method POST --data '{{\"name\":\"Alice\"}}'");
    println!("  rok api:test http://localhost:3000/me --header 'Authorization: Bearer TOKEN'");
}