harn-cli 0.7.25

CLI for the Harn programming language — run, test, REPL, format, and lint
use std::env;
use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::process::ExitCode;
use std::time::Duration;

const DEFAULT_HEALTHCHECK_URL: &str = "http://127.0.0.1:8080/health";

fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("{error}");
            ExitCode::FAILURE
        }
    }
}

fn run() -> Result<(), String> {
    let url = env::var("HARN_CONTAINER_HEALTHCHECK_URL").unwrap_or_else(|_| {
        derive_healthcheck_url().unwrap_or_else(|| DEFAULT_HEALTHCHECK_URL.to_string())
    });
    let Some((authority, request_path)) = parse_http_url(&url) else {
        return Err(format!(
            "unsupported healthcheck URL '{url}'; expected http://host:port/path"
        ));
    };
    let addr = authority
        .to_socket_addrs()
        .map_err(|error| format!("failed to resolve {authority}: {error}"))?
        .next()
        .ok_or_else(|| format!("no socket addresses resolved for {authority}"))?;

    let mut stream = TcpStream::connect_timeout(&addr, Duration::from_secs(2))
        .map_err(|error| format!("failed to connect to {addr}: {error}"))?;
    stream
        .set_read_timeout(Some(Duration::from_secs(2)))
        .map_err(|error| format!("failed to set read timeout: {error}"))?;
    stream
        .set_write_timeout(Some(Duration::from_secs(2)))
        .map_err(|error| format!("failed to set write timeout: {error}"))?;

    let request =
        format!("GET {request_path} HTTP/1.1\r\nHost: {authority}\r\nConnection: close\r\n\r\n");
    stream
        .write_all(request.as_bytes())
        .map_err(|error| format!("failed to write request: {error}"))?;

    let mut response = String::new();
    stream
        .read_to_string(&mut response)
        .map_err(|error| format!("failed to read response: {error}"))?;

    if response.starts_with("HTTP/1.1 200") || response.starts_with("HTTP/1.0 200") {
        Ok(())
    } else {
        let status_line = response.lines().next().unwrap_or("<empty response>");
        Err(format!("healthcheck failed: {status_line}"))
    }
}

fn derive_healthcheck_url() -> Option<String> {
    let bind = env::var("HARN_ORCHESTRATOR_LISTEN").ok()?;
    let bind = bind.trim();
    if bind.is_empty() {
        return None;
    }
    let port = bind
        .rsplit(':')
        .next()
        .filter(|segment| !segment.is_empty())?;
    Some(format!("http://127.0.0.1:{port}/health"))
}

fn parse_http_url(url: &str) -> Option<(&str, &str)> {
    let remainder = url.strip_prefix("http://")?;
    let (authority, path) = match remainder.find('/') {
        Some(index) => (&remainder[..index], &remainder[index..]),
        None => (remainder, "/"),
    };
    if authority.is_empty() {
        return None;
    }
    Some((authority, path))
}

#[cfg(test)]
mod tests {
    use super::parse_http_url;

    #[test]
    fn parses_http_url_with_default_path() {
        assert_eq!(
            parse_http_url("http://127.0.0.1:8080"),
            Some(("127.0.0.1:8080", "/"))
        );
    }

    #[test]
    fn parses_http_url_with_explicit_path() {
        assert_eq!(
            parse_http_url("http://localhost:9000/health"),
            Some(("localhost:9000", "/health"))
        );
    }
}