1 /**
2  * Utilities needed to gather information for and execute an HTTP request.
3  */
4 module openapi_client.apirequest;
5 
6 import vibe.core.log : logDebug, logError;
7 import vibe.data.json : Json, deserializeJson, serializeToJson;
8 import vibe.http.client : requestHTTP, HTTPClientRequest, HTTPClientResponse;
9 import vibe.http.common : HTTPMethod, httpMethodFromString;
10 import vibe.http.status : isSuccessCode;
11 import vibe.inet.url : URL;
12 import vibe.inet.webform : FormFields;
13 import vibe.textfilter.urlencode : urlEncode;
14 
15 import std.algorithm : map;
16 import std.array : join;
17 import std.conv : to;
18 import std.typecons : Nullable;
19 
20 import openapi_client.util : resolveTemplate, serializeDeepObject;
21 import openapi_client.handler : ResponseHandler;
22 
23 /**
24  * Utility class to create HTTPClientRequests to access the REST API.
25  */
26 class ApiRequest {
27   /**
28    * The HTTP Method to use for the request, e.g. "GET", "POST", "PUT", etc.
29    */
30   HTTPMethod method;
31 
32   /**
33    * The base-part of the URL including the schema, host, and base path.
34    */
35   string serverUrl;
36 
37   /**
38    * The path for the specific endpoint. It may include named parameters contained within curley
39    * braces, e.g. "{param}".
40    */
41   string pathUrl;
42 
43   /**
44    * A mapping from path-parameter names to their values.
45    */
46   string[string] pathParams;
47 
48   /**
49    * A mapping from query-string parameter names to their values.
50    */
51   string[string] queryParams;
52 
53   /**
54    * A mapping from header parameter names to their values.
55    */
56   string[string] headerParams;
57 
58   /**
59    * Constructs a new ApiRequest.
60    *
61    * Params:
62    *   method = The HTTP method of the request.
63    *   serverUrl = The base-URL of the server that offers the API.
64    *   pathUrl = The endpoint-specific path to add to the request.
65    */
66   this(HTTPMethod method, string serverUrl, string pathUrl) {
67     this.method = method;
68     this.serverUrl = serverUrl;
69     this.pathUrl = pathUrl;
70 
71     logDebug("Creating ApiRequest: method=%s, serverUrl=%s, pathUrl=%s", method, serverUrl, pathUrl);
72   }
73 
74   /**
75    * Adds a header parameter and value to the request.
76    */
77   void setHeaderParam(string key, string value) {
78     // Headers can contain ASCII characters.
79     logDebug("setHeaderParam(string,string): key=%s, value=%s", key, value);
80     headerParams[key] = value;
81   }
82 
83   /// ditto
84   void setHeaderParam(T)(string key, Nullable!T value) {
85     setHeaderParam(key, value.get);
86   }
87 
88   /**
89    * URL-encode a value to add as a path parameter.
90    */
91   void setPathParam(string key, string value) {
92     // Path parameters must be URL encoded.
93     pathParams[key] = urlEncode(value);
94   }
95 
96   /// ditto
97   void setPathParam(T)(string key, Nullable!T value) {
98     setPathParam(key, value.get);
99   }
100 
101   /**
102    * URL-encode a value to add as a query-string parameter.
103    */
104   void setQueryParam(string key, string value) {
105     // Path parameters must be URL encoded.
106     queryParams[key] = urlEncode(value);
107   }
108 
109   /// ditto
110   void setQueryParam(string mode : "deepObject", T : Nullable!T)(string key, T value) {
111     setQueryParam!("deepObject")(key, value.get);
112   }
113 
114   // TODO: Add more encoding mechanisms.
115   /// ditto
116   void setQueryParam(string mode : "deepObject", T)(string keyPrefix, T obj) {
117     FormFields fields;
118     serializeDeepObject(serializeToJson(obj), keyPrefix, fields);
119     foreach (string key, string value; fields.byKeyValue()) {
120       setQueryParam(key, value);
121     }
122   }
123 
124   /**
125    * Return the URL of an API Request, resolving any path and query-string parameters.
126    */
127   string getUrl() {
128     URL url = URL(serverUrl);
129     url.path = url.path ~ resolveTemplate(pathUrl[1..$], pathParams);
130     url.queryString = queryParams.byKeyValue()
131         .map!(pair => pair.key ~ "=" ~ pair.value)
132         .join("&");
133     return url.toString();
134   }
135 
136   /**
137    * Asynchronously perform the network request for an API Request, resolving cookie and header
138    * parameters, and transmitting the request body.
139    */
140   void makeRequest(RequestT)(RequestT reqBody, ResponseHandler handler) {
141     string url = getUrl();
142     logDebug("makeRequest 0: url=%s", url);
143     requestHTTP(
144         url,
145         (scope HTTPClientRequest req) {
146           req.method = method;
147           foreach (pair; headerParams.byKeyValue()) {
148             logDebug("Adding header: %s: %s", pair.key, pair.value);
149             req.headers[pair.key] = pair.value;
150           }
151           if (reqBody !is null) {
152             // TODO: Support additional content-types.
153             if (req.contentType == "application/x-www-form-urlencoded") {
154               // TODO: Only perform deepObject encoding if the OpenAPI Spec calls for it.
155               auto formFields = serializeDeepObject(reqBody);
156               logDebug("Writing Form Body: %s", formFields.toString);
157               req.writeFormBody(formFields.byKeyValue());
158             } else if (req.contentType == "application/json") {
159               req.writeJsonBody(reqBody);
160             } else {
161               logError("Unsupported request body format: %s", req.contentType);
162             }
163           }
164         },
165         (scope HTTPClientResponse res) {
166           logDebug("makeRequest 1: handler=%s", handler);
167           if (handler !is null)
168             handler.handleResponse(res);
169         });
170   }
171 
172 }