Skip to content

Commit 6c7813e

Browse files
bjgillwing328
authored andcommitted
[rust-server] asynchronous support via hyper v0.11 (#7896)
* End use of deprecated openssl method * Enhance rust-server to use hyper 0.11 to support handling operations asynchronously The changes are complete and working (at least for microservices tested within Metaswitch). This isn't completely compatible with the (previous/current) synchronous swagger-codegen. Specifically, * `Client` is no longer `Send + Sync` * Api implementations used by Server are no longer expected to be `Send + Sync` (which is good, because it's quite hard if `Client` isn't) * the code to create `Client`s and `Server`s, and hook them into `hyper` or `tokio` is different. Importantly, though, the business logic itself should be unchanged. * Re-adds the `basePath` element to all server endpoints. This mean clients and servers can talk to each other again. * Fix multipart formdata codegen * Fix up handling of multipart messages * Fix server -> client multipart message response * Correct handling of optional file types * Add authorization header to requests with basic auth * Add client support for `application/x-www-form-urlencoded` * Import uuid library if headers use UUID type * Add BASE_PATH to the server module. * Wrap client connector * Support both query and body parameters on the same operation
1 parent c8650d0 commit 6c7813e

File tree

31 files changed

+7543
-5801
lines changed

31 files changed

+7543
-5801
lines changed

modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/RustServerCodegen.java

100755100644
Lines changed: 150 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class RustServerCodegen extends DefaultCodegen implements CodegenConfig {
3434
protected String packageName;
3535
protected String packageVersion;
3636
protected String externCrateName;
37+
protected Map<String, Map<String, String>> pathSetMap = new HashMap<String, Map<String, String>>();
3738

3839
public RustServerCodegen() {
3940
super();
@@ -155,8 +156,9 @@ public RustServerCodegen() {
155156
supportingFiles.add(new SupportingFile("gitignore", "", ".gitignore"));
156157
supportingFiles.add(new SupportingFile("lib.mustache", "src", "lib.rs"));
157158
supportingFiles.add(new SupportingFile("models.mustache", "src", "models.rs"));
158-
supportingFiles.add(new SupportingFile("server.mustache", "src", "server.rs"));
159-
supportingFiles.add(new SupportingFile("client.mustache", "src", "client.rs"));
159+
supportingFiles.add(new SupportingFile("server-mod.mustache", "src/server", "mod.rs"));
160+
supportingFiles.add(new SupportingFile("server-auth.mustache", "src/server", "auth.rs"));
161+
supportingFiles.add(new SupportingFile("client-mod.mustache", "src/client", "mod.rs"));
160162
supportingFiles.add(new SupportingFile("mimetypes.mustache", "src", "mimetypes.rs"));
161163
supportingFiles.add(new SupportingFile("example-server.mustache", "examples", "server.rs"));
162164
supportingFiles.add(new SupportingFile("example-client.mustache", "examples", "client.rs"));
@@ -448,75 +450,58 @@ boolean isMimetypePlainText(String mimetype) {
448450
return mimetype.toLowerCase().startsWith("text/plain");
449451
}
450452

453+
boolean isMimetypeWwwFormUrlEncoded(String mimetype) {
454+
return mimetype.toLowerCase().startsWith("application/x-www-form-urlencoded");
455+
}
456+
451457
@Override
452458
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, Map<String, Model> definitions, Swagger swagger) {
453459
CodegenOperation op = super.fromOperation(path, httpMethod, operation, definitions, swagger);
460+
461+
// The Rust code will need to contain a series of regular expressions.
462+
// For performance, we'll construct these at start-of-day and re-use
463+
// them. That means we need labels for them.
464+
//
465+
// Construct a Rust constant (uppercase) token name, and ensure it's
466+
// unique using a numeric tie-breaker if required.
467+
String basePathId = sanitizeName(op.path.replace("/", "_").replace("{", "").replace("}", "").replaceAll("^_", "")).toUpperCase();
468+
String pathId = basePathId;
469+
int pathIdTiebreaker = 2;
470+
boolean found = false;
471+
while (pathSetMap.containsKey(pathId)) {
472+
Map<String, String> pathSetEntry = pathSetMap.get(pathId);
473+
if (pathSetEntry.get("path").equals(op.path)) {
474+
found = true;
475+
break;
476+
}
477+
pathId = basePathId + pathIdTiebreaker;
478+
pathIdTiebreaker++;
479+
}
480+
481+
// Save off the regular expression and path details in the
482+
// "pathSetMap", which we'll add to the source document that will be
483+
// processed by the templates.
484+
if (!found) {
485+
Map<String, String> pathSetEntry = new HashMap<String, String>();
486+
pathSetEntry.put("path", op.path);
487+
pathSetEntry.put("PATH_ID", pathId);
488+
if (!op.pathParams.isEmpty()) {
489+
pathSetEntry.put("hasPathParams", "true");
490+
}
491+
// Don't prefix with '^' so that the templates can put the
492+
// basePath on the front.
493+
pathSetEntry.put("pathRegEx", op.path.replace("{", "(?P<").replace("}", ">[^/?#]*)") + "$");
494+
pathSetMap.put(pathId, pathSetEntry);
495+
}
496+
454497
op.vendorExtensions.put("operation_id", underscore(op.operationId));
455498
op.vendorExtensions.put("uppercase_operation_id", underscore(op.operationId).toUpperCase());
456499
op.vendorExtensions.put("path", op.path.replace("{", ":").replace("}", ""));
500+
op.vendorExtensions.put("PATH_ID", pathId);
501+
op.vendorExtensions.put("hasPathParams", !op.pathParams.isEmpty());
457502
op.vendorExtensions.put("HttpMethod", Character.toUpperCase(op.httpMethod.charAt(0)) + op.httpMethod.substring(1).toLowerCase());
458-
op.vendorExtensions.put("httpmethod", op.httpMethod.toLowerCase());
459503
for (CodegenParameter param : op.allParams) {
460-
String example = null;
461-
462-
if (param.isString) {
463-
if (param.dataFormat != null && param.dataFormat.equals("byte")) {
464-
param.vendorExtensions.put("formatString", "\\\"{:?}\\\"");
465-
example = "swagger::ByteArray(\"" + ((param.example != null) ? param.example : "") + "\".to_string().into_bytes())";
466-
} else {
467-
param.vendorExtensions.put("formatString", "\\\"{}\\\"");
468-
example = "\"" + ((param.example != null) ? param.example : "") + "\".to_string()";
469-
}
470-
} else if (param.isPrimitiveType) {
471-
if ((param.isByteArray) ||
472-
(param.isBinary)) {
473-
// Binary primitive types don't implement `Display`.
474-
param.vendorExtensions.put("formatString", "{:?}");
475-
example = "swagger::ByteArray(Vec::from(\"" + ((param.example != null) ? param.example : "") + "\"))";
476-
} else {
477-
param.vendorExtensions.put("formatString", "{}");
478-
example = (param.example != null) ? param.example : "";
479-
}
480-
} else if (param.isListContainer) {
481-
param.vendorExtensions.put("formatString", "{:?}");
482-
example = (param.example != null) ? param.example : "&Vec::new()";
483-
} else if (param.isFile) {
484-
param.vendorExtensions.put("formatString", "{:?}");
485-
op.vendorExtensions.put("hasFile", true);
486-
additionalProperties.put("apiHasFile", true);
487-
example = "Box::new(stream::once(Ok(b\"hello\".to_vec()))) as Box<Stream<Item=_, Error=_> + Send>";
488-
} else {
489-
param.vendorExtensions.put("formatString", "{:?}");
490-
if (param.example != null) {
491-
example = "serde_json::from_str::<" + param.dataType + ">(\"" + param.example + "\").expect(\"Failed to parse JSON example\")";
492-
}
493-
}
494-
495-
if (param.required) {
496-
if (example != null) {
497-
param.vendorExtensions.put("example", example);
498-
} else if (param.isListContainer) {
499-
// Use the empty list if we don't have an example
500-
param.vendorExtensions.put("example", "&Vec::new()");
501-
}
502-
else {
503-
// If we don't have an example that we can provide, we need to disable the client example, as it won't build.
504-
param.vendorExtensions.put("example", "???");
505-
op.vendorExtensions.put("noClientExample", Boolean.TRUE);
506-
}
507-
} else if ((param.dataFormat != null)&&((param.dataFormat.equals("date-time")) || (param.dataFormat.equals("date")))) {
508-
param.vendorExtensions.put("formatString", "{:?}");
509-
param.vendorExtensions.put("example", "None");
510-
} else {
511-
// Not required, so override the format string and example
512-
param.vendorExtensions.put("formatString", "{:?}");
513-
if (param.isFile) {
514-
// Optional file types are wrapped in a future
515-
param.vendorExtensions.put("example", (example != null) ? "Box::new(future::ok(Some(" + example + "))) as Box<Future<Item=_, Error=_> + Send>" : "None");
516-
} else {
517-
param.vendorExtensions.put("example", (example != null) ? "Some(" + example + ")" : "None");
518-
}
519-
}
504+
processParam(param, op);
520505
}
521506

522507
List<String> consumes = new ArrayList<String>();
@@ -544,6 +529,8 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
544529
consumesXml = true;
545530
} else if (isMimetypePlainText(mimeType)) {
546531
consumesPlainText = true;
532+
} else if (isMimetypeWwwFormUrlEncoded(mimeType)) {
533+
additionalProperties.put("usesUrlEncodedForm", true);
547534
}
548535

549536
mediaType.put("mediaType", mimeType);
@@ -587,7 +574,6 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
587574
}
588575

589576
if (op.bodyParam != null) {
590-
591577
if (paramHasXmlNamespace(op.bodyParam, definitions)){
592578
op.bodyParam.vendorExtensions.put("has_namespace", "true");
593579
}
@@ -607,6 +593,7 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
607593

608594
}
609595
for (CodegenParameter param : op.bodyParams) {
596+
processParam(param, op);
610597

611598
if (paramHasXmlNamespace(param, definitions)){
612599
param.vendorExtensions.put("has_namespace", "true");
@@ -624,9 +611,18 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
624611
}
625612
}
626613
for (CodegenParameter param : op.headerParams) {
614+
// If a header uses UUIDs, we need to import the UUID package.
615+
if (param.dataType.equals("uuid::Uuid")) {
616+
additionalProperties.put("apiUsesUuid", true);
617+
}
618+
processParam(param, op);
619+
627620
// Give header params a name in camel case. CodegenParameters don't have a nameInCamelCase property.
628621
param.vendorExtensions.put("typeName", toModelName(param.baseName));
629622
}
623+
for (CodegenParameter param : op.formParams) {
624+
processParam(param, op);
625+
}
630626
for (CodegenResponse rsp : op.responses) {
631627
String[] words = rsp.message.split("[^A-Za-z ]");
632628
String responseId;
@@ -673,10 +669,16 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
673669
}
674670
}
675671
for (CodegenProperty header : rsp.headers) {
672+
if (header.datatype.equals("uuid::Uuid")) {
673+
additionalProperties.put("apiUsesUuid", true);
674+
}
676675
header.nameInCamelCase = toModelName(header.baseName);
677676
}
678677
}
679678
for (CodegenProperty header : op.responseHeaders) {
679+
if (header.datatype.equals("uuid::Uuid")) {
680+
additionalProperties.put("apiUsesUuid", true);
681+
}
680682
header.nameInCamelCase = toModelName(header.baseName);
681683
}
682684

@@ -860,6 +862,27 @@ public Map<String, Object> postProcessSupportingFileData(Map<String, Object> obj
860862
LOGGER.error(e.getMessage(), e);
861863
}
862864
}
865+
866+
// We previously built a mapping from path to path ID and regular
867+
// expression - see fromOperation for details. Sort it and add an
868+
// index, and then add it to the objects that we're about to pass to
869+
// the templates to process.
870+
List<Map.Entry<String, Map<String, String>>> pathSetEntryList = new ArrayList(pathSetMap.entrySet());
871+
Collections.sort(pathSetEntryList, new Comparator<Map.Entry<String, Map<String, String>>>() {
872+
public int compare(Map.Entry<String, Map<String, String>> a, Map.Entry<String, Map<String, String>> b) {
873+
return a.getValue().get("path").compareTo(b.getValue().get("path"));
874+
}
875+
});
876+
List pathSet = new ArrayList<Map<String, String>>();
877+
int index = 0;
878+
for (Map.Entry<String, Map<String, String>> pathSetEntry : pathSetEntryList) {
879+
Map<String, String> pathSetEntryValue = pathSetEntry.getValue();
880+
pathSetEntryValue.put("index", Integer.toString(index));
881+
index++;
882+
pathSet.add(pathSetEntryValue);
883+
}
884+
objs.put("pathSet", pathSet);
885+
863886
return super.postProcessSupportingFileData(objs);
864887
}
865888

@@ -1029,4 +1052,67 @@ private boolean paramHasXmlNamespace(CodegenParameter param, Map<String, Model>
10291052
}
10301053
return false;
10311054
}
1055+
1056+
private void processParam(CodegenParameter param, CodegenOperation op) {
1057+
String example = null;
1058+
1059+
if (param.isString) {
1060+
if (param.dataFormat != null && param.dataFormat.equals("byte")) {
1061+
param.vendorExtensions.put("formatString", "\\\"{:?}\\\"");
1062+
example = "swagger::ByteArray(\"" + ((param.example != null) ? param.example : "") + "\".to_string().into_bytes())";
1063+
} else {
1064+
param.vendorExtensions.put("formatString", "\\\"{}\\\"");
1065+
example = "\"" + ((param.example != null) ? param.example : "") + "\".to_string()";
1066+
}
1067+
} else if (param.isPrimitiveType) {
1068+
if ((param.isByteArray) ||
1069+
(param.isBinary)) {
1070+
// Binary primitive types don't implement `Display`.
1071+
param.vendorExtensions.put("formatString", "{:?}");
1072+
example = "swagger::ByteArray(Vec::from(\"" + ((param.example != null) ? param.example : "") + "\"))";
1073+
} else {
1074+
param.vendorExtensions.put("formatString", "{}");
1075+
example = (param.example != null) ? param.example : "";
1076+
}
1077+
} else if (param.isListContainer) {
1078+
param.vendorExtensions.put("formatString", "{:?}");
1079+
example = (param.example != null) ? param.example : "&Vec::new()";
1080+
} else if (param.isFile) {
1081+
param.vendorExtensions.put("formatString", "{:?}");
1082+
op.vendorExtensions.put("hasFile", true);
1083+
additionalProperties.put("apiHasFile", true);
1084+
example = "Box::new(stream::once(Ok(b\"hello\".to_vec()))) as Box<Stream<Item=_, Error=_> + Send>";
1085+
} else {
1086+
param.vendorExtensions.put("formatString", "{:?}");
1087+
if (param.example != null) {
1088+
example = "serde_json::from_str::<" + param.dataType + ">(\"" + param.example + "\").expect(\"Failed to parse JSON example\")";
1089+
}
1090+
}
1091+
1092+
if (param.required) {
1093+
if (example != null) {
1094+
param.vendorExtensions.put("example", example);
1095+
} else if (param.isListContainer) {
1096+
// Use the empty list if we don't have an example
1097+
param.vendorExtensions.put("example", "&Vec::new()");
1098+
}
1099+
else {
1100+
// If we don't have an example that we can provide, we need to disable the client example, as it won't build.
1101+
param.vendorExtensions.put("example", "???");
1102+
op.vendorExtensions.put("noClientExample", Boolean.TRUE);
1103+
}
1104+
} else if ((param.dataFormat != null)&&((param.dataFormat.equals("date-time")) || (param.dataFormat.equals("date")))) {
1105+
param.vendorExtensions.put("formatString", "{:?}");
1106+
param.vendorExtensions.put("example", "None");
1107+
} else {
1108+
// Not required, so override the format string and example
1109+
param.vendorExtensions.put("formatString", "{:?}");
1110+
if (param.isFile) {
1111+
// Optional file types are wrapped in a future
1112+
param.vendorExtensions.put("example", (example != null) ? "Box::new(future::ok(Some(" + example + "))) as Box<Future<Item=_, Error=_> + Send>" : "None");
1113+
} else {
1114+
param.vendorExtensions.put("example", (example != null) ? "Some(" + example + ")" : "None");
1115+
}
1116+
}
1117+
}
10321118
}

modules/swagger-codegen/src/main/resources/rust-server/Cargo.mustache

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,37 @@ license = "Unlicense"
99

1010
[features]
1111
default = ["client", "server"]
12-
client = ["serde_json", {{#usesXml}}"serde-xml-rs", {{/usesXml}}"serde_ignored", "hyper", "hyper-openssl", "uuid"{{#apiHasFile}}, "multipart"{{/apiHasFile}}]
13-
server = ["serde_json", {{#usesXml}}"serde-xml-rs", {{/usesXml}}"serde_ignored", "hyper", "iron", "router", "bodyparser", "urlencoded", "uuid"{{#apiHasFile}}, "multipart"{{/apiHasFile}}]
12+
client = ["serde_json", {{#usesUrlEncodedForm}}"serde_urlencoded", {{/usesUrlEncodedForm}} {{#usesXml}}"serde-xml-rs", {{/usesXml}}"serde_ignored", "hyper", "hyper-tls", "native-tls", "openssl", "tokio-core", "url", "uuid"{{#apiHasFile}}, "multipart"{{/apiHasFile}}]
13+
server = ["serde_json", {{#usesXml}}"serde-xml-rs", {{/usesXml}}"serde_ignored", "hyper", "hyper-tls", "native-tls", "openssl", "tokio-core", "tokio-proto", "tokio-tls", "regex", "percent-encoding", "url", "uuid"{{#apiHasFile}}, "multipart"{{/apiHasFile}}]
1414

1515
[dependencies]
1616
# Required by example server.
1717
#
1818
chrono = { version = "0.4", features = ["serde"] }
1919
futures = "0.1"
20-
hyper = {version = "0.10", optional = true}
21-
hyper-openssl = {version = "0.2", optional = true }
22-
iron = {version = "0.5", optional = true}
23-
swagger = "0.7"
20+
hyper = {version = "0.11", optional = true}
21+
hyper-tls = {version = "0.1.2", optional = true}
22+
swagger = "0.9"
2423

2524
# Not required by example server.
2625
#
27-
bodyparser = {version = "0.7", optional = true}
28-
url = "1.5"
2926
lazy_static = "0.2"
3027
log = "0.3.0"
31-
multipart = {version = "0.13", optional = true}
32-
router = {version = "0.5", optional = true}
28+
mime = "0.3.3"
29+
multipart = {version = "0.13.3", optional = true}
30+
native-tls = {version = "0.1.4", optional = true}
31+
openssl = {version = "0.9.14", optional = true}
32+
percent-encoding = {version = "1.0.0", optional = true}
33+
regex = {version = "0.2", optional = true}
3334
serde = "1.0"
3435
serde_derive = "1.0"
3536
serde_ignored = {version = "0.0.4", optional = true}
3637
serde_json = {version = "1.0", optional = true}
37-
urlencoded = {version = "0.5", optional = true}
38+
serde_urlencoded = {version = "0.5.1", optional = true}
39+
tokio-core = {version = "0.1.6", optional = true}
40+
tokio-proto = {version = "0.1.1", optional = true}
41+
tokio-tls = {version = "0.1.3", optional = true, features = ["tokio-proto"]}
42+
url = {version = "1.5", optional = true}
3843
uuid = {version = "0.5", optional = true, features = ["serde", "v4"]}
3944
# ToDo: this should be updated to point at the official crate once
4045
# https://github.com/RReverser/serde-xml-rs/pull/45 is accepted upstream

0 commit comments

Comments
 (0)