Skip to main content

rust_releases_io/client/
remote_client.rs

1use crate::client::errors::{HttpError, IoError};
2use crate::{Document, ResourceFile, RetrievalLocation, RetrievedDocument, RustReleasesClient};
3use std::io::Read;
4use std::time::Duration;
5
6const DEFAULT_MEMORY_SIZE: usize = 4096;
7
8const DEFAULT_TIMEOUT: Duration = Duration::from_secs(150);
9
10/// A client to download rust releases data.
11///
12/// If a cached file is not present, or if a cached file is present, but the copy is outdated,
13/// the client will download a new copy of the given resource and store it to the `cache_folder`.
14/// If a cached file is present, and the copy is not outdated, the cached file will be returned
15/// instead.
16#[derive(Debug)]
17pub struct HttpClient {
18    agent: ureq::Agent,
19}
20
21impl HttpClient {
22    /// Create a new [`HttpClient`].
23    ///
24    /// ```
25    /// use std::time::Duration;
26    /// use rust_releases_io::HttpClient;
27    /// let timeout = Duration::from_secs(86_400);
28    ///
29    /// let _client = HttpClient::new(timeout);
30    /// ```
31    pub fn new(timeout: Duration) -> Self {
32        let config = ureq::Agent::config_builder()
33            .user_agent("rust-releases (github.com/foresterre/rust-releases/issues)")
34            .proxy(ureq::Proxy::try_from_env())
35            .timeout_global(Some(timeout))
36            .build();
37
38        let agent = config.new_agent();
39
40        Self { agent }
41    }
42
43    /// Fetch a response from the given `url`.
44    fn fetch_url(&self, url: &str) -> Result<Box<dyn Read + Send + Sync>, ClientError> {
45        let response = self.agent.get(url).call().map_err(|err| HttpError {
46            error: Box::new(err),
47        })?;
48
49        let reader = Box::new(response.into_body().into_reader());
50
51        Ok(reader)
52    }
53}
54
55impl Default for HttpClient {
56    /// Create a new [`HttpClient`].
57    ///
58    /// ```
59    /// use rust_releases_io::HttpClient;
60    ///
61    /// let _client = HttpClient::default();
62    /// ```
63    fn default() -> Self {
64        Self::new(DEFAULT_TIMEOUT)
65    }
66}
67
68impl RustReleasesClient for HttpClient {
69    type Error = ClientError;
70
71    fn fetch(&self, resource: ResourceFile) -> Result<RetrievedDocument, Self::Error> {
72        let mut reader = self.fetch_url(resource.url())?;
73
74        // write to memory
75        let document = write_document(&mut reader)?;
76
77        Ok(RetrievedDocument::new(
78            document,
79            RetrievalLocation::Url(resource.url.to_string()),
80        ))
81    }
82}
83
84fn write_document(reader: &mut Box<dyn Read + Send + Sync>) -> Result<Document, ClientError> {
85    let mut buffer = Vec::with_capacity(DEFAULT_MEMORY_SIZE);
86
87    let bytes_read = reader
88        .read_to_end(&mut buffer)
89        .map_err(IoError::auxiliary)?;
90
91    if bytes_read == 0 {
92        return Err(ClientError::Empty);
93    }
94
95    Ok(Document::new(buffer))
96}
97
98/// A list of errors which may be produced by [`HttpClient::fetch`].
99#[derive(Debug, thiserror::Error)]
100#[non_exhaustive]
101pub enum ClientError {
102    /// Returned if an empty document was fetched.
103    #[error("Received empty file")]
104    Empty,
105
106    /// Returned if the HTTP client could not fetch an item
107    #[error(transparent)]
108    Http(#[from] HttpError),
109
110    /// Returned in case of an `std::io::Error`.
111    #[error(transparent)]
112    Io(#[from] IoError),
113}