ds_http_client/
lib.rs

1//! # HTTP client
2//!
3//! High-level interface to Hyper/reqwest HTTP client.
4//!
5//! This library is optimized to work with Nasqueron Datasources components.
6
7use std::collections::HashMap;
8use std::io::Error as IOError;
9use std::path::Path;
10
11use lazy_static::lazy_static;
12use reqwest::{Client as ReqwestClient, RequestBuilder};
13use reqwest::ClientBuilder;
14use reqwest::Error as ReqwestError;
15use reqwest::IntoUrl;
16use reqwest::Response;
17use reqwest::header::{HeaderMap, HeaderValue};
18use tokio::fs::File;
19use tokio::io::AsyncWriteExt;
20
21/*   -------------------------------------------------------------
22     User agent
23
24     The USER_AGENT variable is computed at build time.
25     - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
26
27lazy_static! {
28    pub static ref USER_AGENT: String = format!(
29        "{}/{}",
30        env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")
31    );
32}
33
34/// Gets the default user agent
35pub fn get_user_agent () -> &'static str {
36    &USER_AGENT
37}
38
39/*   -------------------------------------------------------------
40     HTTP client
41     - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
42
43/// HTTP client
44pub struct Client {
45    client: ReqwestClient,
46}
47
48impl Client {
49    pub fn new(headers: Option<HeaderMap>) -> Self {
50        let client = ClientBuilder::new()
51            .default_headers(build_default_headers(headers))
52            .gzip(true)
53            .deflate(true)
54            .build()
55            .expect("Can't build HTTP client");
56
57        Self {
58            client,
59        }
60    }
61
62    pub async fn get<T>(&self, url: T) -> Result<Response, Error>
63    where T: IntoUrl {
64        let request = self.client.get(url);
65        self.run(request).await
66    }
67
68    pub async fn get_with_headers<T>(&self, url: T, headers: HashMap<String, String>) -> Result<Response, Error>
69    where T: IntoUrl {
70        let headers = parse_headers(headers);
71
72        let request = self.client
73            .get(url)
74            .headers(headers);
75
76        self.run(request).await
77    }
78
79    pub async fn run(&self, request: RequestBuilder) -> Result<Response, Error> {
80        request
81            .send()
82            .await
83            .map_err(|error| Error::Reqwest(error))
84    }
85
86    pub async fn download<P, T>(&self, url: T, target_path: P) -> Result<usize, Error>
87    where T: IntoUrl, P: AsRef<Path> {
88        let mut file =  File::create(target_path)
89            .await
90            .map_err(|error| Error::IO(error))?;
91
92        let mut target_content = self.get(url).await?;
93        let mut bytes_read = 0;
94        while let Some(chunk) = target_content
95            .chunk()
96            .await
97            .map_err(|error| Error::Reqwest(error))?
98        {
99            bytes_read += file.write(chunk.as_ref())
100                .await
101                .map_err(|error| Error::IO(error))?;
102        }
103
104        Ok(bytes_read)
105    }
106}
107
108/*   -------------------------------------------------------------
109     HTTP client utilities
110     - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
111
112pub fn parse_headers(headers: HashMap<String, String>) -> HeaderMap {
113    headers
114        .iter()
115        .map(|(name, value)| (
116            name.parse().expect("Can't parse header name"),
117            value.parse().expect("Can't parse header value")
118        ))
119        .collect()
120}
121
122fn build_default_headers(headers: Option<HeaderMap>) -> HeaderMap {
123    let mut headers = headers
124        .unwrap_or(HeaderMap::new());
125
126    // RFC 7231 states User-Agent header SHOULD be sent.
127    if !headers.contains_key("User-Agent") {
128        headers.append("User-Agent", HeaderValue::from_static(get_user_agent()));
129    }
130
131    headers
132}
133
134/*   -------------------------------------------------------------
135     HTTP client error
136     - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
137
138/// HTTP client error
139#[derive(Debug)]
140pub enum Error {
141    /// Represents an underlying error from Reqwest HTTP client when processing a request.
142    Reqwest(ReqwestError),
143
144    /// Represents an IO error when doing file operations.
145    IO(IOError),
146}