1 module hunt.httpclient.Request;
2 
3 import hunt.Functions;
4 import hunt.http.client;
5 import hunt.httpclient.Response : Response;
6 import hunt.logging.ConsoleLogger;
7 import hunt.util.MimeTypeUtils;
8 
9 import std.json;
10 import std.range;
11 import core.thread;
12 import core.time;
13 
14 private alias HuntHttpClient = hunt.http.client.HttpClient.HttpClient;
15 
16 private struct PendingFile {
17     string name;
18     // const(ubyte)[] contents;
19     string filename;
20     string[string] headers;
21 }
22 
23 private struct BodyFormat {
24     enum string JSON = "json";
25     enum string FormParams = "form_params";
26     enum string Multipart = "multipart";
27 }
28 
29 /**
30  * 
31  */
32 class Request {
33 
34     private string _contentType; // = MimeType.TEXT_PLAIN_VALUE;
35     private string[string] _headers;
36     private Duration _timeout;
37     private PendingFile[] _pendingFiles;
38     private string _bodyFormat;
39     private MimeTypeUtils _mimeUtil = new MimeTypeUtils();
40     private string[string] _formData;
41 
42     /**
43      * The number of times to try the request.
44      */
45     private int _tries = 1;
46 
47     /**
48      * The number of milliseconds to wait between retries.
49      */
50     private Duration _retryDelay = 100.msecs;
51 
52     this() {
53     }
54 
55     /**
56      * Indicate the request contains JSON.
57      */
58     Request asJson() {
59         _bodyFormat = BodyFormat.JSON;
60         contentType(MimeType.APPLICATION_JSON_VALUE);
61         return this;
62     }
63 
64     /**
65      * Indicate the request contains form parameters.
66      */
67     Request asForm() {
68         _bodyFormat = BodyFormat.FormParams;
69         contentType(MimeType.APPLICATION_X_WWW_FORM_VALUE);
70         return this;
71     }
72 
73     /**
74      * Specify the body format of the request.
75      */
76     Request bodyFormat(string value) {
77         _bodyFormat = value;
78         return this;
79     }
80 
81     /**
82      * Indicate the request is a multi-part form request.
83      */
84     Request asMultipart() {
85         _bodyFormat = BodyFormat.Multipart;
86         return this;
87     }
88 
89     Request attach(string name, string filename = null, string[string] headers = null) {
90         _pendingFiles ~= PendingFile(name, filename, headers);
91         asMultipart();
92         return this;
93     }
94 
95     Request formData(string[string] data) {
96         _formData = data;
97         asMultipart();
98         return this;
99     }
100 
101     /**
102      * Indicate the type of content that should be returned by the server.
103      */
104     Request accept(string contentType) {
105         _headers[HttpHeader.ACCEPT.toString()] = contentType;
106         return this;
107     }
108 
109     /**
110      * Indicate that JSON should be returned by the server.
111      */
112     Request acceptJson(string contentType) {
113         return accept(MimeType.APPLICATION_JSON_VALUE);
114     }
115 
116     /**
117      * Specify the request's content type.
118      */
119     Request contentType(string value) {
120         _contentType = value;
121         return this;
122     }
123 
124     Request withHeaders(string[string] headers) {
125         foreach (string key, string value; headers) {
126             _headers[key] = value;
127         }
128         return this;
129     }
130 
131     Request withCookies(Cookie[] cookies...) {
132         string cookie = HttpHeader.COOKIE.toString();
133         _headers[cookie] = generateCookies(cookies);
134         return this;
135     }
136 
137     /**
138      * Specify an authorization token for the request.
139      */
140     Request withToken(string value, string type = "Bearer") {
141         _headers[HttpHeader.AUTHORIZATION.toString()] = type ~ " " ~ value;
142         return this;
143     }
144 
145     Request timeout(Duration dur) {
146         _timeout = dur;
147         return this;
148     }
149 
150     Request retry(int timers, Duration delay) {
151         _tries = timers;
152         _retryDelay = delay;
153         return this;
154     }
155 
156     Response get(string url) {
157         Response res;
158         .retry(_tries, (int attempts) { res = doGet(url); }, _retryDelay);
159         return res;
160     }
161 
162     private Response doGet(string url) {
163         HttpClientOptions options = new HttpClientOptions();
164         if (_timeout > Duration.zero) {
165             options.getTcpConfiguration().setIdleTimeout(_timeout);
166             options.getTcpConfiguration().setConnectTimeout(_timeout);
167         }
168 
169         HuntHttpClient client = new HuntHttpClient(options);
170         scope (exit) {
171             client.close();
172         }
173 
174         RequestBuilder builder = new RequestBuilder();
175 
176         if (_headers !is null) {
177             foreach (string name, string value; _headers) {
178                 builder.addHeader(name, value);
179             }
180         }
181 
182         HttpClientRequest request = builder.url(url).build();
183         HttpClientResponse response = client.newCall(request).execute();
184 
185         return new Response(response);
186     }
187 
188     /* #region private methods  */
189 
190     private HttpBody buildBody() {
191         HttpBody httpBody;
192         if (_bodyFormat == BodyFormat.Multipart) {
193             MultipartBody.Builder builder = new MultipartBody.Builder();
194 
195             foreach (string key, string value; _formData) {
196                 builder.addFormDataPart(key, value, MimeType.TEXT_PLAIN_VALUE);
197             }
198 
199             foreach (ref PendingFile file; _pendingFiles) {
200                 string mimeType = _mimeUtil.getMimeByExtension(file.filename);
201                 if (mimeType.empty)
202                     mimeType = MimeType.TEXT_PLAIN_VALUE;
203 
204                 builder.addFormDataPart(file.name, file.filename,
205                         HttpBody.createFromFile(mimeType, file.filename));
206 
207                 // TODO: Tasks pending completion -@zhangxueping at 2020-05-15T16:41:19+08:00
208                 // Add headers
209             }
210 
211             httpBody = builder.build();
212         } else {
213             httpBody = HttpBody.create(_contentType, cast(const(ubyte)[]) null);
214         }
215 
216         return httpBody;
217     }
218 
219     private Response perform(string url, HttpBody httpBody, HttpMethod method = HttpMethod.POST) {
220         HttpClientOptions options = new HttpClientOptions();
221         if (_timeout > Duration.zero) {
222             options.getTcpConfiguration().setIdleTimeout(_timeout);
223             options.getTcpConfiguration().setConnectTimeout(_timeout);
224         }
225 
226         HuntHttpClient client = new HuntHttpClient(options);
227         scope (exit) {
228             client.close();
229         }
230 
231         RequestBuilder builder = new RequestBuilder();
232 
233         if (_headers !is null) {
234             foreach (string name, string value; _headers) {
235                 builder.addHeader(name, value);
236             }
237         }
238 
239         builder.url(url);
240 
241         if(method == HttpMethod.POST) 
242             builder.post(httpBody);
243         else if(method == HttpMethod.PUT) 
244             builder.put(httpBody);
245         else if(method == HttpMethod.DELETE) 
246             builder.del(httpBody);
247         else if(method == HttpMethod.PATCH) 
248             builder.patch(httpBody);
249         else
250             throw new Exception("Unsupported method: " ~ method.toString());
251 
252         HttpClientRequest request = builder.build();
253         HttpClientResponse response = client.newCall(request).execute();
254 
255         return new Response(response);
256     }
257 
258     /* #endregion */
259     
260     /* #region POST */
261 
262     Response post(string url) {
263         return post(url, buildBody());
264     }
265 
266     Response post(string url, string[string] data) {
267         assert(data !is null, "No data avaliable!");
268 
269         UrlEncoded encoder = new UrlEncoded(UrlEncodeStyle.HtmlForm);
270         foreach (string name, string value; data) {
271             encoder.put(name, value);
272         }
273         string content = encoder.encode();
274         if (_contentType.empty)
275             _contentType = MimeType.APPLICATION_X_WWW_FORM_VALUE;
276         return post(url, _contentType, cast(const(ubyte)[]) content);
277     }
278 
279     Response post(string url, JSONValue json) {
280         if (_contentType.empty)
281             _contentType = MimeType.APPLICATION_JSON_VALUE;
282         return post(url, _contentType, cast(const(ubyte)[]) json.toString());
283     }
284 
285     Response post(string url, string contentType, const(ubyte)[] content) {
286         Response res;
287         .retry(_tries, 
288             (int attempts) { 
289                 res = perform(url, HttpBody.create(contentType, content), HttpMethod.POST); 
290             }, 
291             _retryDelay);
292         return res;
293     }
294 
295     Response post(string url, HttpBody httpBody) {
296         Response res;
297         .retry(_tries, 
298             (int attempts) { 
299                 res = perform(url, httpBody, HttpMethod.POST); 
300             }, 
301             _retryDelay);
302         return res;
303     }
304 
305     /* #endregion */
306 
307 
308     /* #region PUT */
309 
310     Response put(string url) {
311         return put(url, buildBody());
312     }
313 
314     Response put(string url, string[string] data) {
315         assert(data !is null, "No data avaliable!");
316 
317         UrlEncoded encoder = new UrlEncoded(UrlEncodeStyle.HtmlForm);
318         foreach (string name, string value; data) {
319             encoder.put(name, value);
320         }
321         string content = encoder.encode();
322         if (_contentType.empty)
323             _contentType = MimeType.APPLICATION_X_WWW_FORM_VALUE;
324         return put(url, _contentType, cast(const(ubyte)[]) content);
325     }
326 
327     Response put(string url, JSONValue json) {
328         if (_contentType.empty)
329             _contentType = MimeType.APPLICATION_JSON_VALUE;
330         return put(url, _contentType, cast(const(ubyte)[]) json.toString());
331     }
332 
333     Response put(string url, string contentType, const(ubyte)[] content) {
334         Response res;
335         .retry(_tries, 
336             (int attempts) { 
337                 res = perform(url, HttpBody.create(contentType, content), HttpMethod.PUT); 
338             }, 
339             _retryDelay);
340         return res;
341     }
342 
343     Response put(string url, HttpBody httpBody) {
344         Response res;
345         .retry(_tries, 
346             (int attempts) { 
347                 res = perform(url, httpBody, HttpMethod.PUT); 
348             }, 
349             _retryDelay);
350         return res;
351     }
352 
353     /* #endregion */
354 
355 
356     /* #region DELETE */
357 
358     Response del(string url) {
359         return del(url, buildBody());
360     }
361 
362     Response del(string url, string[string] data) {
363         assert(data !is null, "No data avaliable!");
364 
365         UrlEncoded encoder = new UrlEncoded(UrlEncodeStyle.HtmlForm);
366         foreach (string name, string value; data) {
367             encoder.put(name, value);
368         }
369         string content = encoder.encode();
370         if (_contentType.empty)
371             _contentType = MimeType.APPLICATION_X_WWW_FORM_VALUE;
372         return del(url, _contentType, cast(const(ubyte)[]) content);
373     }
374 
375     Response del(string url, JSONValue json) {
376         if (_contentType.empty)
377             _contentType = MimeType.APPLICATION_JSON_VALUE;
378         return del(url, _contentType, cast(const(ubyte)[]) json.toString());
379     }
380 
381     Response del(string url, string contentType, const(ubyte)[] content) {
382         Response res;
383         .retry(_tries, 
384             (int attempts) { 
385                 res = perform(url, HttpBody.create(contentType, content), HttpMethod.DELETE); 
386             }, 
387             _retryDelay);
388         return res;
389     }
390 
391     Response del(string url, HttpBody httpBody) {
392         Response res;
393         .retry(_tries, 
394             (int attempts) { 
395                 res = perform(url, httpBody, HttpMethod.DELETE); 
396             }, 
397             _retryDelay);
398         return res;
399     }
400 
401     /* #endregion */
402     
403 
404     /* #region PATCH */
405 
406     Response patch(string url) {
407         return patch(url, buildBody());
408     }
409 
410     Response patch(string url, string[string] data) {
411         assert(data !is null, "No data avaliable!");
412 
413         UrlEncoded encoder = new UrlEncoded(UrlEncodeStyle.HtmlForm);
414         foreach (string name, string value; data) {
415             encoder.put(name, value);
416         }
417         string content = encoder.encode();
418         if (_contentType.empty)
419             _contentType = MimeType.APPLICATION_X_WWW_FORM_VALUE;
420         return patch(url, _contentType, cast(const(ubyte)[]) content);
421     }
422 
423     Response patch(string url, JSONValue json) {
424         if (_contentType.empty)
425             _contentType = MimeType.APPLICATION_JSON_VALUE;
426         return patch(url, _contentType, cast(const(ubyte)[]) json.toString());
427     }
428 
429     Response patch(string url, string contentType, const(ubyte)[] content) {
430         Response res;
431         .retry(_tries, 
432             (int attempts) { 
433                 res = perform(url, HttpBody.create(contentType, content), HttpMethod.PATCH); 
434             }, 
435             _retryDelay);
436         return res;
437     }
438 
439     Response patch(string url, HttpBody httpBody) {
440         Response res;
441         .retry(_tries, 
442             (int attempts) { 
443                 res = perform(url, httpBody, HttpMethod.PATCH); 
444             }, 
445             _retryDelay);
446         return res;
447     }
448 
449     /* #endregion */
450 }
451 
452 /**
453  * Retry an operation a given number of times.
454  */
455 private void retry(int times, Action1!int handler, Duration delay = Duration.zero,
456         Func1!(Exception, bool) precondition = null) {
457 
458     assert(handler !is null, "The handler can't be null");
459     int attempts = 0;
460 
461     while (true) {
462         attempts++;
463 
464         try {
465             handler(attempts);
466             break;
467         } catch (Exception ex) {
468             version (HUNT_HTTP_DEBUG)
469                 tracef("Retrying %d / %d", attempts, times);
470             if (attempts >= times || precondition !is null && !precondition(ex)) {
471                 throw ex;
472             }
473 
474             if (delay > Duration.zero) {
475                 Thread.sleep(delay);
476             }
477         }
478     }
479 }