earl 0.5.2

AI-safe CLI for AI agents
use std::collections::BTreeMap;
use std::time::Duration;

use anyhow::{Context, Result, bail};

use crate::config::ProxyProfile;
use crate::template::schema::TransportTemplate;

pub use earl_core::transport::ResolvedTransport;

const DEFAULT_MAX_RESPONSE_BYTES: usize = 8 * 1024 * 1024;

pub fn resolve_transport(
    input: Option<&TransportTemplate>,
    proxy_profiles: &BTreeMap<String, ProxyProfile>,
) -> Result<ResolvedTransport> {
    let timeout = input
        .and_then(|t| t.timeout_ms)
        .map(Duration::from_millis)
        .unwrap_or_else(|| Duration::from_secs(30));

    let (follow_redirects, max_redirect_hops) = input
        .and_then(|t| t.redirects.as_ref())
        .map(|r| (r.follow, r.max_hops))
        .unwrap_or((true, 5));

    let (retry_max_attempts, retry_backoff, retry_on_status) = input
        .and_then(|t| t.retry.as_ref())
        .map(|r| {
            (
                if r.max_attempts == 0 {
                    1
                } else {
                    r.max_attempts
                },
                Duration::from_millis(r.backoff_ms.max(1)),
                r.retry_on_status.clone(),
            )
        })
        .unwrap_or((1, Duration::from_millis(250), Vec::new()));

    let compression = input.and_then(|t| t.compression).unwrap_or(true);
    let tls_min_version = parse_tls_min_version(input)?;
    let proxy_url = resolve_proxy_url(input, proxy_profiles)?;
    let max_response_bytes = input
        .and_then(|t| t.max_response_bytes)
        .and_then(|v| usize::try_from(v).ok())
        .unwrap_or(DEFAULT_MAX_RESPONSE_BYTES)
        .clamp(1024, 128 * 1024 * 1024);

    Ok(ResolvedTransport {
        timeout,
        follow_redirects,
        max_redirect_hops,
        retry_max_attempts,
        retry_backoff,
        retry_on_status,
        compression,
        tls_min_version,
        proxy_url,
        max_response_bytes,
    })
}

fn parse_tls_min_version(
    input: Option<&TransportTemplate>,
) -> Result<Option<reqwest::tls::Version>> {
    let Some(raw) = input
        .and_then(|t| t.tls.as_ref())
        .and_then(|tls| tls.min_version.as_deref())
        .map(str::trim)
        .filter(|v| !v.is_empty())
    else {
        return Ok(None);
    };

    let version = match raw {
        "1.0" => reqwest::tls::Version::TLS_1_0,
        "1.1" => reqwest::tls::Version::TLS_1_1,
        "1.2" => reqwest::tls::Version::TLS_1_2,
        "1.3" => reqwest::tls::Version::TLS_1_3,
        _ => bail!("unsupported tls.min_version `{raw}`; expected one of 1.0, 1.1, 1.2, 1.3"),
    };

    Ok(Some(version))
}

fn resolve_proxy_url(
    input: Option<&TransportTemplate>,
    proxy_profiles: &BTreeMap<String, ProxyProfile>,
) -> Result<Option<String>> {
    let Some(profile_name) = input
        .and_then(|t| t.proxy_profile.as_deref())
        .map(str::trim)
        .filter(|name| !name.is_empty())
    else {
        return Ok(None);
    };

    let profile = proxy_profiles
        .get(profile_name)
        .with_context(|| format!("unknown transport proxy_profile `{profile_name}`"))?;

    let proxy_url = profile.url.trim();
    if proxy_url.is_empty() {
        bail!("proxy profile `{profile_name}` has empty url");
    }

    Ok(Some(proxy_url.to_string()))
}