1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
mod types;

use anyhow::{anyhow, Result};
use hyper::StatusCode;
use reqwest::header::HeaderMap;
use reqwest::Client as AsyncClient;

use self::types::{Base, ErrorResponse};
use crate::config::VERSION;

const HOP_API_BASE_URL: &str = "https://api.hop.io/v1";

#[derive(Debug, Clone)]
pub struct HttpClient {
    pub client: AsyncClient,
    pub base_url: String,
    pub headers: HeaderMap,
    pub ua: String,
}

impl HttpClient {
    pub fn new(token: Option<String>, api_url: Option<String>) -> Self {
        let mut headers = HeaderMap::new();

        headers.insert("accept", "application/json".parse().unwrap());

        if let Some(token) = token {
            headers.insert("authorization", token.parse().unwrap());
        }

        let ua = format!(
            "hop_cli/{} on {}",
            VERSION,
            sys_info::os_type().unwrap_or_else(|_| "unknown".to_string())
        );

        let base_url = match api_url {
            Some(url) => url,
            None => HOP_API_BASE_URL.to_string(),
        };

        Self {
            client: AsyncClient::builder()
                .user_agent(ua.clone())
                .default_headers(headers.clone())
                .build()
                .unwrap(),
            base_url,
            headers,
            ua,
        }
    }

    pub async fn handle_response<T>(&self, response: reqwest::Response) -> Result<Option<T>>
    where
        T: serde::de::DeserializeOwned,
    {
        let response = match response.status() {
            StatusCode::CREATED => return Ok(None),
            StatusCode::NO_CONTENT => return Ok(None),
            status => {
                if !status.clone().is_success() {
                    return self.handle_error(response, status).await;
                }

                response
            }
        };

        match response.json::<Base<T>>().await {
            Ok(base) => Ok(Some(base.data)),
            Err(e) => Err(anyhow!(e)),
        }
    }

    async fn handle_error<T>(
        &self,
        response: reqwest::Response,
        status: StatusCode,
    ) -> Result<Option<T>> {
        let body = response.json::<ErrorResponse>().await;

        match body {
            Ok(body) => Err(anyhow!("{}", body.error.message)),
            Err(err) => {
                log::debug!("Error deserialize message: {:#?}", err);

                Err(anyhow!("Error: HTTP {:#?}", status))
            }
        }
    }

    pub async fn request<T>(
        &self,
        method: &str,
        path: &str,
        data: Option<(reqwest::Body, &str)>,
    ) -> Result<Option<T>>
    where
        T: serde::de::DeserializeOwned,
    {
        let mut request = self.client.request(
            method.parse().unwrap(),
            &format!("{}{}", self.base_url, path),
        );

        log::debug!("request: {} {} {:?}", method, path, data);

        if let Some((body, content_type)) = data {
            request = request.header("content-type", content_type);
            request = request.body(body);
        }

        let request = request.build().unwrap();

        #[cfg(debug_assertions)]
        let now = tokio::time::Instant::now();

        let response = self
            .client
            .execute(request)
            .await
            .map_err(|e| e.to_string())
            .expect("Failed to send the request");

        #[cfg(debug_assertions)]
        log::debug!("response in: {:#?}", now.elapsed());

        self.handle_response(response).await
    }
}