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 }