pray-core 1.12.1

Core library for Prayfile, the package manager for the language placed before inference
Documentation
use crate::resource_limits::MAX_HTTP_RESPONSE_BYTES;
use crate::{PrayError, PrayResult};
use std::io::Read;
use std::sync::OnceLock;
use std::time::Duration;

pub struct HttpResponse {
    pub status: u16,
    pub body: Vec<u8>,
}

pub fn join_url(base: &str, path: &str) -> String {
    format!(
        "{}/{}",
        base.trim_end_matches('/'),
        path.trim_start_matches('/')
    )
}

fn http_client() -> PrayResult<&'static reqwest::blocking::Client> {
    static CLIENT: OnceLock<reqwest::blocking::Client> = OnceLock::new();
    if let Some(client) = CLIENT.get() {
        return Ok(client);
    }
    let client = reqwest::blocking::Client::builder()
        .timeout(Duration::from_secs(60))
        .redirect(reqwest::redirect::Policy::limited(5))
        .build()
        .map_err(|error| PrayError::Network(error.to_string()))?;
    let _ = CLIENT.set(client);
    CLIENT
        .get()
        .ok_or_else(|| PrayError::Network("HTTP client unavailable".to_string()))
}

fn ensure_http_url(url: &str) -> PrayResult<()> {
    if url.starts_with("http://") || url.starts_with("https://") {
        Ok(())
    } else {
        Err(PrayError::Unsupported(format!(
            "unsupported URL scheme: {url}"
        )))
    }
}

fn read_response_body(response: reqwest::blocking::Response) -> PrayResult<Vec<u8>> {
    let length = response.content_length();
    if length.is_some_and(|value| value > MAX_HTTP_RESPONSE_BYTES) {
        return Err(PrayError::Network(format!(
            "HTTP response exceeds {MAX_HTTP_RESPONSE_BYTES} bytes"
        )));
    }
    let mut body = Vec::new();
    let mut limited = response.take(MAX_HTTP_RESPONSE_BYTES.saturating_add(1));
    limited
        .read_to_end(&mut body)
        .map_err(|error| PrayError::Network(error.to_string()))?;
    if body.len() as u64 > MAX_HTTP_RESPONSE_BYTES {
        return Err(PrayError::Network(format!(
            "HTTP response exceeds {MAX_HTTP_RESPONSE_BYTES} bytes"
        )));
    }
    Ok(body)
}

pub fn http_get(url: &str) -> PrayResult<Vec<u8>> {
    let response = http_request("GET", url, None, None, &[])?;
    if response.status / 100 != 2 {
        return Err(PrayError::Resolution(format!(
            "GET {url} failed with HTTP {}",
            response.status
        )));
    }
    Ok(response.body)
}

pub fn http_get_with_headers(url: &str, headers: &[(&str, &str)]) -> PrayResult<(Vec<u8>, u16)> {
    let response = http_request("GET", url, None, None, headers)?;
    Ok((response.body, response.status))
}

pub fn http_post(url: &str, content_type: &str, body: &[u8]) -> PrayResult<HttpResponse> {
    http_request("POST", url, Some(content_type), Some(body), &[])
}

pub fn http_put(url: &str, content_type: &str, body: &[u8]) -> PrayResult<HttpResponse> {
    http_put_with_headers(url, content_type, body, &[])
}

pub fn http_put_with_headers(
    url: &str,
    content_type: &str,
    body: &[u8],
    headers: &[(&str, &str)],
) -> PrayResult<HttpResponse> {
    http_request("PUT", url, Some(content_type), Some(body), headers)
}

fn http_request(
    method: &str,
    url: &str,
    content_type: Option<&str>,
    body: Option<&[u8]>,
    headers: &[(&str, &str)],
) -> PrayResult<HttpResponse> {
    ensure_http_url(url)?;
    let client = http_client()?;
    let mut request = match method {
        "GET" => client.get(url),
        "POST" => client.post(url),
        "PUT" => client.put(url),
        other => {
            return Err(PrayError::Unsupported(format!(
                "unsupported HTTP method: {other}"
            )))
        }
    };
    for (name, value) in headers {
        request = request.header(*name, *value);
    }
    if let Some(content_type) = content_type {
        request = request.header(reqwest::header::CONTENT_TYPE, content_type);
    }
    if let Some(body) = body {
        request = request.body(body.to_vec());
    }
    let response = request
        .send()
        .map_err(|error| PrayError::Network(error.to_string()))?;
    let status = response.status().as_u16();
    let body = read_response_body(response)?;
    Ok(HttpResponse { status, body })
}