Skip to main content

abi_loader/fetcher/
http.rs

1//! HTTP/HTTPS Import Fetcher
2//!
3//! Fetches ABI files from HTTP/HTTPS URLs.
4
5use crate::fetcher::{FetchContext, FetchError, FetchResult, ImportFetcher};
6use crate::file::ImportSource;
7use std::time::Duration;
8
9/* HTTP/HTTPS URL fetcher */
10pub struct HttpFetcher {
11    client: reqwest::blocking::Client,
12}
13
14impl HttpFetcher {
15    /* Create a new HTTP fetcher with default configuration */
16    pub fn new() -> Result<Self, FetchError> {
17        Self::with_timeout(30)
18    }
19
20    /* Create with custom timeout */
21    pub fn with_timeout(timeout_seconds: u64) -> Result<Self, FetchError> {
22        let client = reqwest::blocking::Client::builder()
23            .timeout(Duration::from_secs(timeout_seconds))
24            .user_agent("thru-abi-loader/1.0")
25            .build()
26            .map_err(|e| FetchError::Http {
27                status: 0,
28                message: format!("Failed to create HTTP client: {}", e),
29            })?;
30
31        Ok(Self { client })
32    }
33}
34
35impl ImportFetcher for HttpFetcher {
36    fn handles(&self, source: &ImportSource) -> bool {
37        matches!(source, ImportSource::Http { .. })
38    }
39
40    fn fetch(&self, source: &ImportSource, _ctx: &FetchContext) -> Result<FetchResult, FetchError> {
41        let ImportSource::Http { url } = source else {
42            return Err(FetchError::UnsupportedSource(
43                "HttpFetcher only handles Http imports".to_string(),
44            ));
45        };
46
47        /* Perform the HTTP request */
48        let response = self
49            .client
50            .get(url)
51            .send()
52            .map_err(|e| FetchError::Http {
53                status: 0,
54                message: format!("Request failed: {}", e),
55            })?;
56
57        /* Check response status */
58        let status = response.status();
59        if !status.is_success() {
60            return Err(FetchError::Http {
61                status: status.as_u16(),
62                message: format!("HTTP {} for {}", status, url),
63            });
64        }
65
66        /* Read response body */
67        let content = response.text().map_err(|e| FetchError::Http {
68            status: 0,
69            message: format!("Failed to read response body: {}", e),
70        })?;
71
72        Ok(FetchResult {
73            content,
74            canonical_location: url.clone(),
75            is_remote: true,
76            resolved_path: None,
77        })
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_http_fetcher_handles() {
87        let fetcher = HttpFetcher::new().unwrap();
88
89        let http_import = ImportSource::Http {
90            url: "https://example.com/types.abi.yaml".to_string(),
91        };
92        let path_import = ImportSource::Path {
93            path: "local.abi.yaml".to_string(),
94        };
95        let git_import = ImportSource::Git {
96            url: "https://github.com/test/repo".to_string(),
97            git_ref: "main".to_string(),
98            path: "abi.yaml".to_string(),
99        };
100
101        assert!(fetcher.handles(&http_import));
102        assert!(!fetcher.handles(&path_import));
103        assert!(!fetcher.handles(&git_import));
104    }
105
106    /* Integration test - requires network access */
107    #[test]
108    #[ignore] /* Run with: cargo test -- --ignored */
109    fn test_http_fetcher_real_request() {
110        let fetcher = HttpFetcher::new().unwrap();
111        let source = ImportSource::Http {
112            url: "https://httpbin.org/get".to_string(),
113        };
114        let ctx = FetchContext {
115            base_path: None,
116            parent_is_remote: false,
117            include_dirs: vec![],
118        };
119
120        let result = fetcher.fetch(&source, &ctx);
121        assert!(result.is_ok());
122
123        let fetch_result = result.unwrap();
124        assert!(fetch_result.is_remote);
125        assert!(fetch_result.content.contains("httpbin"));
126    }
127}