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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
use std::collections::HashMap;

use crate::Driver;

use super::resource::Resource;

#[derive(Debug)]
enum Method {
    Get,
    Post,
}

impl Method {
    fn get_str(&self) -> &str {
        match self {
            Method::Get => "get",
            &Method::Post => "post",
        }
    }
}

/// Ensures that this type is serializable and deserializable so can be used for defining a resource.
pub trait SingleRequestTrait: Sized {
    fn into_string(self) -> Result<String, String>;
    fn from_string(data: &str) -> Result<Self, String>;
}

/// Ensures that vector of objects of this type is serializable and deserializable so can be used for defining a resource.
pub trait ListRequestTrait: Sized {
    fn list_into_string(vec: Vec<Self>) -> Result<String, String>;
    fn list_from_string(data: &str) -> Result<Vec<Self>, String>;
}

/// Builder for typed requests (more complex version of [FetchBuilder](struct.FetchBuilder.html)).
///
/// Unlike in the FetchBuilder, here request and response data is a type implementing [SingleRequestTrait] or [ListRequestTrait].
pub enum RequestBuilder {
    ErrorInput(String),
    Data {
        driver: Driver,
        url: String,
        headers: Option<HashMap<String, String>>,
        body: Option<String>,
    },
}

impl RequestBuilder {
    pub fn new(driver: &Driver, url: impl Into<String>) -> RequestBuilder {
        RequestBuilder::Data {
            driver: driver.clone(),
            url: url.into(),
            headers: None,
            body: None,
        }
    }

    #[must_use]
    pub fn body(self, body: String) -> RequestBuilder {
        match self {
            RequestBuilder::ErrorInput(message) => RequestBuilder::ErrorInput(message),
            RequestBuilder::Data { driver, url, headers, .. } =>
                RequestBuilder::Data {
                    driver,
                    url,
                    headers,
                    body: Some(body),
                },
        }
    }

    #[must_use]
    pub fn bearer_auth(self, token: impl Into<String>) -> RequestBuilder {
        let token: String = token.into();
        self.set_header("Authorization", format!("Bearer {}", token))
    }

    #[must_use]
    pub fn set_header(self, name: impl Into<String>, value: impl Into<String>) -> RequestBuilder {
        let name: String = name.into();
        let value: String = value.into();

        if let RequestBuilder::Data { headers, driver, url, body} = self {
            if let Some(mut headers) = headers {
                headers.insert(name, value);
                return RequestBuilder::Data { headers: Some(headers), driver, url, body };
            }

            let mut new_headers = HashMap::new();
            new_headers.insert(name, value);
            return RequestBuilder::Data { headers: Some(new_headers), driver, url, body };
        }

        self
    }

    #[must_use]
    pub fn body_json(self, body: impl SingleRequestTrait) -> RequestBuilder {
        let body_string: Result<String, String> = body.into_string();

        match body_string {
            Ok(body) => self.body(body).set_header("Content-Type", "application/json"),
            Err(message) => RequestBuilder::ErrorInput(message),
        }
    }

    #[must_use]
    pub fn headers(self, headers: HashMap<String, String>) -> RequestBuilder {
        match self {
            RequestBuilder::ErrorInput(message) => RequestBuilder::ErrorInput(message),
            RequestBuilder::Data { driver, url, body, .. } => RequestBuilder::Data {
                driver,
                url,
                headers: Some(headers),
                body,
            },
        }
    }

    async fn call(self, method: Method) -> RequestResponse {
        let (driver, url, headers, body) = match self {
            RequestBuilder::ErrorInput(message) => return RequestResponse::new(None, Err(message)),
            RequestBuilder::Data { driver, url, headers, body } => (driver, url, headers, body),
        };

        let builder = driver.fetch(url.clone());

        let builder = match body {
            None => builder,
            Some(body) => builder.set_body(body),
        };

        let builder = match headers {
            Some(headers) => builder.set_headres(headers),
            None => builder,
        };

        let result = match method {
            Method::Get => builder.get().await,
            Method::Post => builder.post().await,
        };

        RequestResponse::new(Some((method, url)), result)
    }

    pub async fn get(self) -> RequestResponse {
        self.call(Method::Get).await
    }

    pub async fn post(self) -> RequestResponse {
        self.call(Method::Post).await
    }
}

#[derive(Debug)]
pub struct RequestResponseBody {
    body: String,
}

impl RequestResponseBody {
    fn new(body: String) -> RequestResponseBody {
        RequestResponseBody { body }
    }

    pub fn into<T: PartialEq + SingleRequestTrait>(self) -> Resource<T> {
        match T::from_string(self.body.as_str()) {
            Ok(data) => Resource::Ready(data),
            Err(err) => Resource::Error(err),
        }
    }

    pub fn into_vec<T: PartialEq + ListRequestTrait>(self) -> Resource<Vec<T>> {
        match T::list_from_string(self.body.as_str()) {
            Ok(data) => Resource::Ready(data),
            Err(err) => Resource::Error(err),
        }
    }
}

/// Result from request made using [RequestBuilder].
#[derive(Debug)]
pub struct RequestResponse {
    request_details: Option<(Method, String)>,
    data: Result<(u32, String), String>,
}

impl RequestResponse {
    fn new(request_details: Option<(Method, String)>, data: Result<(u32, String), String>) -> RequestResponse {
        RequestResponse { request_details, data }
    }

    pub fn status(&self) -> Option<u32> {
        if let Ok((status, _)) = self.data {
            return Some(status);
        }

        None
    }

    pub fn into<T: PartialEq>(self, convert: impl Fn(u32, RequestResponseBody) -> Option<Resource<T>>) -> Resource<T> {
        let result = match self.data {
            Ok((status, body)) => {
                let body = RequestResponseBody::new(body);
                match convert(status, body) {
                    Some(result) => result,
                    None => Resource::Error(format!("Unhandled response code {}", status)),
                }
            }
            Err(err) => Resource::Error(err),
        };

        if let Resource::Error(err) = &result {
            if let Some((method, url)) = self.request_details {
                log::error!("Error fetching {} {}: {}", method.get_str(), url, err);
            } else {
                log::error!("Error fetching {}", err);
            }
        }

        result
    }

    pub fn into_data<T: PartialEq + SingleRequestTrait>(self) -> Resource<T> {
        self.into(|_, response_body| {
            Some(response_body.into::<T>())
        })
    }

    pub fn into_error_message<T: PartialEq>(self) -> Resource<T> {
        let body = match self.data {
            Ok((code, body)) => format!("API error {}: {}", code, body),
            Err(body) => format!("Network error: {}", body),
        };

        Resource::Error(body)
    }
}