Skip to content

Commit

Permalink
refs #4059 better base path handling fixes for REST and Swagger (#4067)
Browse files Browse the repository at this point in the history
* refs #4059 better base path handling fixes for REST and Swagger

* refs #4059 fixed tests

* refs #4059 fixed type error in last commit
  • Loading branch information
davidnich committed Oct 30, 2020
1 parent 5f25c7b commit f086a7e
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 38 deletions.
3 changes: 3 additions & 0 deletions doxygen/lang/900_release_notes.dox.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
- <a href="../../modules/WebSocketHandler/html/index.html">WebSocketHandler</a> module updates:
- fixed a bug handling the case when a connection object is deleted in a callback method
(<a href="https://github.com/qorelanguage/qore/issues/4063">issue 4063</a>)
- <a href="../../modules/RestClient/html/index.html">RestClient</a> module updates:
- fixed a bug where the base path in the Swagger schema was ignored in
(<a href="https://github.com/qorelanguage/qore/issues/4059">issue 4059</a>)

@section qore_096 Qore 0.9.6

Expand Down
2 changes: 2 additions & 0 deletions examples/test/qlib/Swagger/Swagger.qtest
Original file line number Diff line number Diff line change
Expand Up @@ -2616,6 +2616,8 @@ public class SwaggerTest inherits QUnit::Test {
"validator": so,
);

assertEq("/v2", so.getBasePath());

RestClient rc(opts);
#printf("url: %y\n", url);
hash h = rc.get("/user/login?username=user;password=pass");
Expand Down
10 changes: 5 additions & 5 deletions examples/test/qlib/ZeyosRestClient/ZeyosRestClient.qtest
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ class FakeZeyosRestHandler inherits AbstractHttpRequestHandler {
self.input = input;
}

hash handleRequest(hash cx, hash hdr, *data body) {
hash<auto> handleRequest(hash cx, hash hdr, *data body) {
#printf("cx: %N\nhdr: %N\nbody: %N\n", cx, hdr, body);
if (cx.raw_path =~ /^\/?auth\/v1\/login$/) {
if (cx.raw_path =~ /^\/?.+\/auth\/v1\/login$/) {
return makeResponse({"Content-Type": MimeTypeJson}, 200, "{\"token\": \"token\"}");
}
if (cx.raw_path =~ /^\/?auth\/v1\/logout$/) {
if (cx.raw_path =~ /^\/?.+\/auth\/v1\/logout$/) {
return makeResponse({"Content-Type": MimeTypeJson}, 200, "OK");
}
if (!hdr."authorization") {
Expand Down Expand Up @@ -88,7 +88,7 @@ class ZeyosTest inherits QUnit::Test {
port = m_http.addListener(<HttpListenerOptionInfo>{"service": 0}).port;

try {
zeyosRestClient = new ZeyosRestClient({"url": "http://test1:test2@localhost:" + port, "appsecret": "a"});
zeyosRestClient = new ZeyosRestClient({"url": "http://test1:test2@localhost:" + port, "appsecret": "a", "instance": "x"});
} catch (hash<ExceptionInfo> ex) {
if (ex.err == "REST-RESPONSE-ERROR") {
printf("no client support: %s: %s\n", ex.err, ex.desc);
Expand Down Expand Up @@ -132,7 +132,7 @@ class ZeyosTest inherits QUnit::Test {

connectionTest() {
string url = "http://user:pass@localhost:" + port;
ZeyosRestConnection zeyosrc("test", "test", url, NOTHING, {"appsecret": "a"});
ZeyosRestConnection zeyosrc("test", "test", url, NOTHING, {"appsecret": "a", "instance": "x"});
assertEq(True, zeyosrc instanceof ZeyosRestConnection);

%ifdef NoJson
Expand Down
13 changes: 11 additions & 2 deletions qlib/ConnectionProvider/AbstractConnection.qc
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ scheme://user:pass@hostname:port/path
if (scheme_info.required_options) {
list<string> req_list = scheme_info.required_options.split("|");
bool ok;
list<string> missing;
foreach string req in (req_list) {
list<string> req_opt = req.split(",");
# check which options are missing
Expand All @@ -418,10 +419,18 @@ scheme://user:pass@hostname:port/path
ok = True;
break;
}
if (req_list.size() == 1) {
missing = keys ((map {$1: True}, req_opt) - keys have);
}
}
if (!ok) {
throw "CONNECTION-OPTION-ERROR", sprintf("missing required options %y; options provided: %y",
scheme_info.required_options, options);
if (missing) {
throw "CONNECTION-OPTION-ERROR", sprintf("missing required options %y; options provided: %y",
missing, options);
} else {
throw "CONNECTION-OPTION-ERROR", sprintf("missing required options %y; options provided: %y",
req_list, options);
}
}
}
return options;
Expand Down
59 changes: 41 additions & 18 deletions qlib/RestClient.qm
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*/

# minimum qore version
%requires qore >= 0.9.6
%requires qore >= 0.9.7

# require type definitions everywhere
%require-types
Expand Down Expand Up @@ -54,7 +54,7 @@
%endtry

module RestClient {
version = "1.7.1";
version = "1.7.2";
desc = "user module for calling REST services";
author = "David Nichols <david@qore.org>";
url = "http://qore.org";
Expand Down Expand Up @@ -126,6 +126,10 @@ printf("%N\n", ans.body);

@section restclientrelnotes Release Notes

@subsection restclientv1_7_2 RestClient v1.7.2
- better fixes to REST URI path handling with schema validation
(<a href="https://github.com/qorelanguage/qore/issues/4059">issue 4059</a>)

@subsection restclientv1_7_1 RestClient v1.7.1
- added the \a swagger_base_path option to REST clients and connections to allow for Swagger schemas to have their
base path overridden
Expand Down Expand Up @@ -446,14 +450,6 @@ RestClient rest(("url": "http://localhost:8001/rest"));
validator = new NullRestSchemaValidator();
}
}
if (validator) {
# issue #4059: strip the URI path from the URL if it matches the prefix of the REST validator's base path
# otherwise we will run into compatibility problems with the fix
string base_path = validator.getBasePath();
if (base_path == getConnectionPath()) {
setConnectionPath();
}
}

if (!do_not_connect)
connect();
Expand Down Expand Up @@ -870,18 +866,28 @@ hash<auto> ans = rest.del("/orders/1");

#! sets up the path for the HTTP request URI
private nothing preparePath(reference<string> path) {
# prepare path
*string p = getConnectionPath();

# strip trailing "/" off the connection path
p =~ s/\/+$//;
# strip leading "/" off the given path
path =~ s/^\/+//;
if (p.val())

# prepare path = connection path + base path + path
*string p = getConnectionPath();
string base_path = validator.getBasePath();
if (base_path.val() && base_path != "/" && p != base_path) {
# strip trailing "/" off the base path
base_path =~ s/\/+$//;
path = base_path + (path.val() ? ("/" + path) : "");
}

if (p.val() && p != "/") {
# strip trailing "/" off the connection path
p =~ s/\/+$//;
path = p + (path.val() ? ("/" + path) : "");
}

# ensure path is absolute
if (path !~ /^\//)
if (path !~ /^\//) {
splice path, 0, 0, "/";
}

path = encode_uri_request(path);
}
Expand Down Expand Up @@ -963,6 +969,23 @@ hash<auto> ans = rest.doRequest("DELETE", "/orders/1");
return sendAndDecodeResponse(body, m, path, hdr, \info, decode_errors);
}

#! The same as doRequest() except no schema validation is performed on the request
/** @since RestClient 1.7.2
*/
hash<auto> doValidatedRequest(string m, string path, auto body, *reference<hash<auto>> info, softbool decode_errors = True, *hash<auto> hdr) {
# use {} + ... here to ensure that hdr stays "hash<auto>"
hdr = {} + headers + hdr;

on_exit if (exists body) {
info += {
"request-body": body,
"request-serialization": ds,
};
}

return sendAndDecodeResponse(body, m, path, hdr, \info, decode_errors);
}

#! sends the outgoing HTTP message and recodes the response to data
private hash<auto> sendAndDecodeResponse(*data body, string m, string path, hash<auto> hdr, *reference<hash<auto>> info, *softbool decode_errors) {
hash<auto> h;
Expand Down Expand Up @@ -1284,7 +1307,7 @@ public class RestConnection inherits ConnectionProvider::HttpBasedConnection {
*AbstractRestSchemaValidator validator = rest.getValidator();
if (validator) {
# if there's a validator, return the provider
return validator.getDataProvider(get());
return validator.getDataProvider(rest);
}
}
throw "DATA-PROVIDER-ERROR", sprintf("there is no validator object in the %s object to use to return a "
Expand Down
49 changes: 48 additions & 1 deletion qlib/Swagger.qm
Original file line number Diff line number Diff line change
Expand Up @@ -1451,7 +1451,13 @@ public class SwaggerSchema inherits ObjectBase, AbstractRestSchemaValidator {
/** @param basePath the new base path value; use an empty string here to clear the basePath
*/
private setBasePathImpl(string basePath) {
self.basePath = basePath;
# remove any trailing '/'s from path
basePath =~ s/\/+$//;
if (basePath.val()) {
self.basePath = basePath;
} else {
remove basePath;
}
}

#! returns example Qore code for the given request
Expand Down Expand Up @@ -2491,6 +2497,47 @@ public class OperationObject inherits ObjectBase {
}
}

#! Processes a generated request
*data getRequestBody(PathItemObject pio, auto body, reference<hash<auto>> headers) {
*AbstractParameterObject body_po = self.body ?? pio.body;
if (body_po) {
body_po.check(True, True, path, method, "body", \body);
} else if (formData) {
map $1.value.check(True, True, path, method, "formData." + $1.key, \body{$1.key}),
formData.pairIterator();
} else if (body) {
error("SCHEMA-VALIDATION-ERROR", "No message body is accepted; body keys passed: %y", keys body);
}

if (!exists body) {
return;
}

# get content type
string ct = consumes.firstKey();

if (ct == MimeTypeMultipartFormData) {
hash<string, hash<FormDataMessageInfo>> parts = cast<hash<string, hash<FormDataMessageInfo>>>(map {$1.key: cast<hash<FormDataMessageInfo>>((
"name": $1.key,
"filename": $1.key,
"hdr": ("Content-Type": MimeTypeText),
"body": $1.value,
))}, body.pairIterator());

hash<MessageInfo> mh = MultiPartFormDataMessage::makeMessage(parts).getMsgAndHeaders();
headers."Content-Type" = mh.hdr."Content-Type";
return mh.body;
}

if (MimeDataTypes{ct}) {
headers."Content-Type" = ct;
return MimeDataTypes{ct}.serialize(body);
}

throw "SERIALIZATION-ERROR", sprintf("%s %s: message body cannot be serialized; available MIME type(s): %y; "
"available serialization modules: %y", method.upr(), path, keys consumes, SerializationModules);
}

#! processes a REST API client-side request to the operation
/** @param serialize if request arguments should be processed for serialization (client-side) or not (server-side)
@param pio the PathItemObject corresponding to the URI path
Expand Down
22 changes: 17 additions & 5 deletions qlib/SwaggerDataProvider/SwaggerRequestDataProvider.qc
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ public class SwaggerRequestDataProvider inherits SwaggerDataProviderBase {

#! Returns the data provider name
string getName() {
return schema.info.title + getUriPath() + "/" + op.method.upr();
string base_path = schema.getBasePath();
string rv = schema.info.title;
if (base_path.val() && base_path != "/") {
rv += base_path;
}
return rv + getUriPath() + "/" + op.method.upr();
}

#! Returns data provider info
Expand Down Expand Up @@ -247,7 +252,17 @@ public class SwaggerRequestDataProvider inherits SwaggerDataProviderBase {
# make the call
hash<auto> info;
try {
hash<auto> resp = rest.doRequest(op.method, uri_path, req.body, \info, NOTHING, req.header + options.hdr);
*hash<auto> hdr = req.header + options.hdr;
*data body = op.getRequestBody(pio, req.body, \hdr);
# prepend base path to request, if any
string base_path = schema.getBasePath();
if (base_path.val() && base_path != "/") {
if (uri_path !~ /^\//) {
base_path += "/";
}
uri_path = base_path + uri_path;
}
hash<auto> resp = rest.doValidatedRequest(op.method, uri_path, body, \info, NOTHING, hdr);
return resp + {"info": info};
} catch (hash<ExceptionInfo> ex) {
#printf("XXX %s\n", get_exception_string(ex));
Expand All @@ -268,9 +283,6 @@ public class SwaggerRequestDataProvider inherits SwaggerDataProviderBase {
#! Returns the URI path to use in requests
private string getUriPath() {
string uri_path = self.uri_path;
if ((string base_path = schema.getBasePath()).val() && base_path != "/") {
uri_path = base_path + uri_path;
}
if (resolve_uri) {
uri_path = cb_resolve_value(uri_path);
}
Expand Down
Loading

0 comments on commit f086a7e

Please sign in to comment.