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.client.get(url).send().map_err(|e| FetchError::Http {
49            status: 0,
50            message: format!("Request failed: {}", e),
51        })?;
52
53        /* Check response status */
54        let status = response.status();
55        if !status.is_success() {
56            return Err(FetchError::Http {
57                status: status.as_u16(),
58                message: format!("HTTP {} for {}", status, url),
59            });
60        }
61
62        /* Read response body */
63        let content = response.text().map_err(|e| FetchError::Http {
64            status: 0,
65            message: format!("Failed to read response body: {}", e),
66        })?;
67
68        Ok(FetchResult {
69            content,
70            canonical_location: url.clone(),
71            is_remote: true,
72            resolved_path: None,
73        })
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_http_fetcher_handles() {
83        let fetcher = HttpFetcher::new().unwrap();
84
85        let http_import = ImportSource::Http {
86            url: "https://example.com/types.abi.yaml".to_string(),
87        };
88        let path_import = ImportSource::Path {
89            path: "local.abi.yaml".to_string(),
90        };
91        let git_import = ImportSource::Git {
92            url: "https://github.com/test/repo".to_string(),
93            git_ref: "main".to_string(),
94            path: "abi.yaml".to_string(),
95        };
96
97        assert!(fetcher.handles(&http_import));
98        assert!(!fetcher.handles(&path_import));
99        assert!(!fetcher.handles(&git_import));
100    }
101
102    /* Integration test - requires network access */
103    #[test]
104    #[ignore] /* Run with: cargo test -- --ignored */
105    fn test_http_fetcher_real_request() {
106        let fetcher = HttpFetcher::new().unwrap();
107        let source = ImportSource::Http {
108            url: "https://httpbin.org/get".to_string(),
109        };
110        let ctx = FetchContext {
111            base_path: None,
112            parent_is_remote: false,
113            include_dirs: vec![],
114        };
115
116        let result = fetcher.fetch(&source, &ctx);
117        assert!(result.is_ok());
118
119        let fetch_result = result.unwrap();
120        assert!(fetch_result.is_remote);
121        assert!(fetch_result.content.contains("httpbin"));
122    }
123}