searchfox-lib 0.10.7

Library for searchfox.org API access
Documentation
use crate::types::{RequestLog, ResponseLog};
use anyhow::Result;
use log::debug;
use reqwest::{Client, Url};
use std::time::{Duration, Instant};

pub struct SearchfoxClient {
    client: Client,
    pub repo: String,
    pub log_requests: bool,
    request_counter: std::sync::atomic::AtomicUsize,
}

impl SearchfoxClient {
    pub fn new(repo: String, log_requests: bool) -> Result<Self> {
        let client = Self::create_tls13_client()?;
        Ok(Self {
            client,
            repo,
            log_requests,
            request_counter: std::sync::atomic::AtomicUsize::new(0),
        })
    }

    fn create_tls13_client() -> Result<Client> {
        Client::builder()
            .user_agent(Self::get_user_agent())
            .use_rustls_tls()
            .min_tls_version(reqwest::tls::Version::TLS_1_2)
            .max_tls_version(reqwest::tls::Version::TLS_1_3)
            .timeout(Duration::from_secs(30))
            .build()
            .map_err(|e| anyhow::anyhow!("Failed to build TLS client with rustls: {}", e))
    }

    fn get_user_agent() -> String {
        let magic_word = std::env::var("SEARCHFOX_MAGIC_WORD")
            .unwrap_or_else(|_| "sésame ouvre toi".to_string());
        format!("searchfox-cli/{} ({})", crate::VERSION, magic_word)
    }

    pub fn log_request_start(&self, method: &str, url: &str) -> Option<RequestLog> {
        if !self.log_requests {
            return None;
        }

        let request_id = self
            .request_counter
            .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
            + 1;

        let log = RequestLog {
            url: url.to_string(),
            method: method.to_string(),
            start_time: Instant::now(),
            request_id,
        };

        eprintln!(
            "[REQ-{}] {} {} - START",
            log.request_id, log.method, log.url
        );
        Some(log)
    }

    pub fn log_request_end(&self, request_log: RequestLog, status: u16, size_bytes: usize) {
        let duration = request_log.start_time.elapsed();

        let response_log = ResponseLog {
            request_id: request_log.request_id,
            status,
            size_bytes,
            duration,
        };

        eprintln!(
            "[REQ-{}] {} {} - END ({}ms, {} bytes, HTTP {})",
            response_log.request_id,
            request_log.method,
            request_log.url,
            duration.as_millis(),
            size_bytes,
            status
        );
    }

    pub async fn ping(&self) -> Result<Duration> {
        if !self.log_requests {
            return Ok(Duration::from_millis(0));
        }

        eprintln!(
            "[PING] Testing network latency to searchfox.org (ICMP ping disabled, using HTTP HEAD)..."
        );

        let ping_url = "https://searchfox.org/";
        let start = Instant::now();

        let response = self
            .client
            .head(ping_url)
            .timeout(Duration::from_secs(10))
            .send()
            .await?;

        let latency = start.elapsed();

        eprintln!(
            "[PING] HTTP HEAD latency: {}ms (HTTP {})",
            latency.as_millis(),
            response.status()
        );
        eprintln!(
            "[PING] Note: This includes minimal server processing time, not just network latency"
        );

        Ok(latency)
    }

    pub async fn get(&self, url: Url) -> Result<reqwest::Response> {
        let request_log = self.log_request_start("GET", url.as_ref());
        let response = self
            .client
            .get(url.clone())
            .header("Accept", "application/json")
            .send()
            .await?;

        if let Some(req_log) = request_log {
            self.log_request_end(req_log, response.status().as_u16(), 0);
        }

        Ok(response)
    }

    pub async fn get_raw(&self, url: &str) -> Result<String> {
        let request_log = self.log_request_start("GET", url);
        let response = self.client.get(url).send().await?;

        if !response.status().is_success() {
            if let Some(req_log) = request_log {
                self.log_request_end(req_log, response.status().as_u16(), 0);
            }
            anyhow::bail!("Request failed: {}", response.status());
        }

        let text = response.text().await?;
        let size = text.len();

        if let Some(req_log) = request_log {
            self.log_request_end(req_log, 200, size);
        }

        Ok(text)
    }

    pub async fn get_html(&self, url: &str) -> Result<String> {
        debug!("Fetching HTML from: {}", url);

        let response = self
            .client
            .get(url)
            .header("Accept", "text/html")
            .send()
            .await?;

        if !response.status().is_success() {
            anyhow::bail!("Request failed: {}", response.status());
        }

        Ok(response.text().await?)
    }

    pub fn client(&self) -> &Client {
        &self.client
    }
}