tauri_api/
http.rs

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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
use attohttpc::{Method, RequestBuilder};
use http::header::HeaderName;
use serde::Deserialize;
use serde_json::Value;
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::collections::HashMap;
use std::fs::File;
use std::time::Duration;

#[derive(Serialize_repr, Deserialize_repr, Clone, Debug)]
#[repr(u16)]
/// The request's body type
pub enum BodyType {
  /// Send request body as application/x-www-form-urlencoded
  Form = 1,
  /// Send request body (which is a path to a file) as application/octet-stream
  File,
  /// Detects the body type automatically
  /// - if the body is a byte array, send is as bytes (application/octet-stream)
  /// - if the body is an object or array, send it as JSON (application/json with UTF-8 charset)
  /// - if the body is a string, send it as text (text/plain with UTF-8 charset)
  Auto,
}

#[derive(Serialize_repr, Deserialize_repr, Clone, Debug)]
#[repr(u16)]
/// The request's response type
pub enum ResponseType {
  /// Read the response as JSON
  Json = 1,
  /// Read the response as text
  Text,
  /// Read the response as binary
  Binary,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
/// The configuration object of an HTTP request
pub struct HttpRequestOptions {
  /// The request method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT or TRACE)
  pub method: String,
  /// The request URL
  pub url: String,
  /// The request query params
  pub params: Option<HashMap<String, String>>,
  /// The request headers
  pub headers: Option<HashMap<String, String>>,
  /// The request body
  pub body: Option<Value>,
  /// Whether to follow redirects or not
  pub follow_redirects: Option<bool>,
  /// Max number of redirections to follow
  pub max_redirections: Option<u32>,
  /// Connect timeout for the request
  pub connect_timeout: Option<u64>,
  /// Read timeout for the request
  pub read_timeout: Option<u64>,
  /// Timeout for the whole request
  pub timeout: Option<u64>,
  /// Whether the request will announce that it accepts compression
  pub allow_compression: Option<bool>,
  /// The body type (defaults to Auto)
  pub body_type: Option<BodyType>,
  /// The response type (defaults to Json)
  pub response_type: Option<ResponseType>,
}

/// The builder for HttpRequestOptions.
///
/// # Examples
/// ```
/// # use tauri_api::http::{ HttpRequestBuilder, HttpRequestOptions, make_request, ResponseType };
/// let mut builder = HttpRequestBuilder::new("GET", "http://example.com");
/// let option = builder.response_type(ResponseType::Text)
///                     .follow_redirects(false)
///                     .build();
///
/// if let Ok(response) = make_request(option) {
///   println!("Response: {}", response);
/// } else {
///   println!("Something Happened!");
/// }
/// ```
pub struct HttpRequestBuilder {
  /// The request method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT or TRACE)
  pub method: String,
  /// The request URL
  pub url: String,
  /// The request query params
  pub params: Option<HashMap<String, String>>,
  /// The request headers
  pub headers: Option<HashMap<String, String>>,
  /// The request body
  pub body: Option<Value>,
  /// Whether to follow redirects or not
  pub follow_redirects: Option<bool>,
  /// Max number of redirections to follow
  pub max_redirections: Option<u32>,
  /// Connect timeout for the request
  pub connect_timeout: Option<u64>,
  /// Read timeout for the request
  pub read_timeout: Option<u64>,
  /// Timeout for the whole request
  pub timeout: Option<u64>,
  /// Whether the request will announce that it accepts compression
  pub allow_compression: Option<bool>,
  /// The body type (defaults to Auto)
  pub body_type: Option<BodyType>,
  /// The response type (defaults to Json)
  pub response_type: Option<ResponseType>,
}

impl HttpRequestBuilder {
  /// Initializes a new instance of the HttpRequestBuilder.
  pub fn new(method: impl Into<String>, url: impl Into<String>) -> Self {
    Self {
      method: method.into(),
      url: url.into(),
      params: None,
      headers: None,
      body: None,
      follow_redirects: None,
      max_redirections: None,
      connect_timeout: None,
      read_timeout: None,
      timeout: None,
      allow_compression: None,
      body_type: None,
      response_type: None,
    }
  }

  /// Sets the request params.
  pub fn params(mut self, params: HashMap<String, String>) -> Self {
    self.params = Some(params);
    self
  }

  /// Sets the request headers.
  pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
    self.headers = Some(headers);
    self
  }

  /// Sets the request body.
  pub fn body(mut self, body: Value) -> Self {
    self.body = Some(body);
    self
  }

  /// Sets whether the request should follow redirects or not.
  pub fn follow_redirects(mut self, follow_redirects: bool) -> Self {
    self.follow_redirects = Some(follow_redirects);
    self
  }

  /// Sets the maximum number of redirections.
  pub fn max_redirections(mut self, max_redirections: u32) -> Self {
    self.max_redirections = Some(max_redirections);
    self
  }

  /// Sets the connection timeout.
  pub fn connect_timeout(mut self, connect_timeout: u64) -> Self {
    self.connect_timeout = Some(connect_timeout);
    self
  }

  /// Sets the read timeout.
  pub fn read_timeout(mut self, read_timeout: u64) -> Self {
    self.read_timeout = Some(read_timeout);
    self
  }

  /// Sets the general request timeout.
  pub fn timeout(mut self, timeout: u64) -> Self {
    self.timeout = Some(timeout);
    self
  }

  /// Sets whether the request allows compressed responses or not.
  pub fn allow_compression(mut self, allow_compression: bool) -> Self {
    self.allow_compression = Some(allow_compression);
    self
  }

  /// Sets the type of the request body.
  pub fn body_type(mut self, body_type: BodyType) -> Self {
    self.body_type = Some(body_type);
    self
  }

  /// Sets the type of the response. Interferes with the way we read the response.
  pub fn response_type(mut self, response_type: ResponseType) -> Self {
    self.response_type = Some(response_type);
    self
  }

  /// Builds the HttpRequestOptions.
  pub fn build(self) -> HttpRequestOptions {
    HttpRequestOptions {
      method: self.method,
      url: self.url,
      params: self.params,
      headers: self.headers,
      body: self.body,
      follow_redirects: self.follow_redirects,
      max_redirections: self.max_redirections,
      connect_timeout: self.connect_timeout,
      read_timeout: self.read_timeout,
      timeout: self.timeout,
      allow_compression: self.allow_compression,
      body_type: self.body_type,
      response_type: self.response_type,
    }
  }
}

/// Executes an HTTP request
///
/// The response will be transformed to String,
/// If reading the response as binary, the byte array will be serialized using serde_json
pub fn make_request(options: HttpRequestOptions) -> crate::Result<Value> {
  let method = Method::from_bytes(options.method.to_uppercase().as_bytes())?;
  let mut builder = RequestBuilder::new(method, options.url);
  if let Some(params) = options.params {
    for (param, param_value) in params.iter() {
      builder = builder.param(param, param_value);
    }
  }

  if let Some(headers) = options.headers {
    for (header, header_value) in headers.iter() {
      builder = builder.header(HeaderName::from_bytes(header.as_bytes())?, header_value);
    }
  }

  if let Some(follow_redirects) = options.follow_redirects {
    builder = builder.follow_redirects(follow_redirects);
  }
  if let Some(max_redirections) = options.max_redirections {
    builder = builder.max_redirections(max_redirections);
  }
  if let Some(connect_timeout) = options.connect_timeout {
    builder = builder.connect_timeout(Duration::from_secs(connect_timeout));
  }
  if let Some(read_timeout) = options.read_timeout {
    builder = builder.read_timeout(Duration::from_secs(read_timeout));
  }
  if let Some(timeout) = options.timeout {
    builder = builder.timeout(Duration::from_secs(timeout));
  }
  if let Some(allow_compression) = options.allow_compression {
    builder = builder.allow_compression(allow_compression);
  }
  builder = builder
    .danger_accept_invalid_certs(true)
    .danger_accept_invalid_hostnames(true);

  let response = if let Some(body) = options.body {
    match options.body_type.unwrap_or(BodyType::Auto) {
      BodyType::Form => builder.form(&body)?.send(),
      BodyType::File => {
        if let Some(path) = body.as_str() {
          builder.file(File::open(path)?).send()
        } else {
          return Err(crate::Error::Path("Body must be the path to the file".into()).into());
        }
      }
      BodyType::Auto => {
        if body.is_object() {
          builder.json(&body)?.send()
        } else if let Some(text) = body.as_str() {
          builder.text(&text).send()
        } else if body.is_array() {
          let u: Result<Vec<u8>, _> = serde_json::from_value(body.clone());
          match u {
            Ok(vec) => builder.bytes(&vec).send(),
            Err(_) => builder.json(&body)?.send(),
          }
        } else {
          builder.send()
        }
      }
    }
  } else {
    builder.send()
  };

  let response = response?;
  if response.is_success() {
    let response_data = match options.response_type.unwrap_or(ResponseType::Json) {
      ResponseType::Json => response.json::<Value>()?,
      ResponseType::Text => Value::String(response.text()?),
      ResponseType::Binary => Value::String(serde_json::to_string(&response.bytes()?)?),
    };
    Ok(response_data)
  } else {
    Err(crate::Error::Network(response.status()).into())
  }
}