This is an automated email from the ASF dual-hosted git repository. liubao pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-servicecomb-java-chassis.git
commit a5bbe149c76db49d0e1aebab056400b02724972b Author: wujimin <[email protected]> AuthorDate: Thu Oct 11 10:12:34 2018 +0800 [SCB-675] generate proto from swagger --- .../protobuf/internal/converter/ProtoMethod.java | 81 ++++++ .../protobuf/internal/converter/ProtoResponse.java | 25 +- .../converter/SwaggerToProtoGenerator.java | 317 +++++++++++++++++++++ .../foundation/protobuf/internal/ProtoConst.java | 38 ++- .../protobuf/internal/schema/SchemaManager.java | 3 +- 5 files changed, 449 insertions(+), 15 deletions(-) diff --git a/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/internal/converter/ProtoMethod.java b/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/internal/converter/ProtoMethod.java new file mode 100644 index 0000000..4ae87ae --- /dev/null +++ b/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/internal/converter/ProtoMethod.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.servicecomb.codec.protobuf.internal.converter; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.Response.Status; + +import org.apache.servicecomb.swagger.invocation.context.HttpStatus; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ProtoMethod { + private String argTypeName; + + private boolean argWrapped; + + @JsonProperty + // key is status + private Map<Integer, ProtoResponse> responses = new HashMap<>(); + + private ProtoResponse defaultResponse; + + public String getArgTypeName() { + return argTypeName; + } + + public void setArgTypeName(String argTypeName) { + this.argTypeName = argTypeName; + } + + public boolean isArgWrapped() { + return argWrapped; + } + + public void setArgWrapped(boolean argWrapped) { + this.argWrapped = argWrapped; + } + + public void addResponse(String status, ProtoResponse response) { + if (status.equals("default")) { + defaultResponse = response; + return; + } + + int statusCode = Integer.parseInt(status); + responses.put(statusCode, response); + + if (defaultResponse == null && statusCode == Status.OK.getStatusCode()) { + defaultResponse = response; + } + } + + public ProtoResponse findResponse(int statusCode) { + ProtoResponse response = responses.get(statusCode); + if (response != null) { + return response; + } + + if (HttpStatus.isSuccess(statusCode)) { + return responses.get(Status.OK.getStatusCode()); + } + + return defaultResponse; + } +} diff --git a/foundations/foundation-protobuf/src/main/java/org/apache/servicecomb/foundation/protobuf/internal/ProtoConst.java b/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/internal/converter/ProtoResponse.java similarity index 65% copy from foundations/foundation-protobuf/src/main/java/org/apache/servicecomb/foundation/protobuf/internal/ProtoConst.java copy to common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/internal/converter/ProtoResponse.java index c15dbae..feaa406 100644 --- a/foundations/foundation-protobuf/src/main/java/org/apache/servicecomb/foundation/protobuf/internal/ProtoConst.java +++ b/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/internal/converter/ProtoResponse.java @@ -14,19 +14,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.servicecomb.foundation.protobuf.internal; +package org.apache.servicecomb.codec.protobuf.internal.converter; -import java.util.LinkedHashMap; +public class ProtoResponse { + private String typeName; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.type.TypeFactory; + private boolean wrapped; -public interface ProtoConst { - String PACK_SCHEMA = "type.googleapis.com/"; + public String getTypeName() { + return typeName; + } - String JSON_SCHEMA = "json/"; + public void setTypeName(String typeName) { + this.typeName = typeName; + } - String JSON_ID_NAME = "@type"; + public boolean isWrapped() { + return wrapped; + } - JavaType MAP_TYPE = TypeFactory.defaultInstance().constructType(LinkedHashMap.class); + public void setWrapped(boolean wrapped) { + this.wrapped = wrapped; + } } diff --git a/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/internal/converter/SwaggerToProtoGenerator.java b/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/internal/converter/SwaggerToProtoGenerator.java new file mode 100644 index 0000000..cd8942a --- /dev/null +++ b/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/internal/converter/SwaggerToProtoGenerator.java @@ -0,0 +1,317 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.servicecomb.codec.protobuf.internal.converter; + +import static org.apache.servicecomb.foundation.common.utils.StringBuilderUtils.appendLine; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.ws.rs.core.Response.Status; + +import org.apache.commons.lang3.StringUtils; +import org.apache.servicecomb.foundation.protobuf.internal.ProtoConst; +import org.apache.servicecomb.foundation.protobuf.internal.parser.ProtoParser; + +import com.google.common.hash.Hashing; + +import io.protostuff.compiler.model.Message; +import io.protostuff.compiler.model.Proto; +import io.swagger.models.Model; +import io.swagger.models.ModelImpl; +import io.swagger.models.Operation; +import io.swagger.models.Path; +import io.swagger.models.Response; +import io.swagger.models.Swagger; +import io.swagger.models.parameters.Parameter; +import io.swagger.models.properties.Property; +import io.vertx.core.json.Json; + +public class SwaggerToProtoGenerator { + private final String protoPackage; + + private final Swagger swagger; + + private final StringBuilder msgStringBuilder = new StringBuilder(); + + private final StringBuilder serviceBuilder = new StringBuilder(); + + private final Set<String> imports = new HashSet<>(); + + private final Set<String> messages = new HashSet<>(); + + private final List<Runnable> pending = new ArrayList<>(); + + // not java package + // better to be: app_${app}.mid_{microservice}.sid_{schemaId} + public SwaggerToProtoGenerator(String protoPackage, Swagger swagger) { + this.protoPackage = protoPackage; + this.swagger = swagger; + } + + public Proto convert() { + convertDefinitions(); + convertOperations(); + for (Runnable runnable : pending) { + runnable.run(); + } + + return createProto(); + } + + private void convertDefinitions() { + if (swagger.getDefinitions() == null) { + return; + } + + for (Entry<String, Model> entry : swagger.getDefinitions().entrySet()) { + convertDefinition(entry.getKey(), (ModelImpl) entry.getValue()); + } + } + + private void convertDefinition(String modelName, ModelImpl model) { + Map<String, Property> properties = model.getProperties(); + if (properties == null) { + // it's a empty message + properties = Collections.emptyMap(); + } + + // complex + messages.add(modelName); + appendLine(msgStringBuilder, "message %s {", modelName); + int tag = 1; + for (Entry<String, Property> entry : properties.entrySet()) { + Property property = entry.getValue(); + String propertyType = convertSwaggerType(property); + + appendLine(msgStringBuilder, " %s %s = %d;", propertyType, entry.getKey(), tag); + tag++; + } + appendLine(msgStringBuilder, "}"); + } + + private void addImports(Proto proto) { + imports.add(proto.getFilename()); + for (Message message : proto.getMessages()) { + messages.add(message.getCanonicalName()); + } + } + + private String convertSwaggerType(Object swaggerType) { + if (swaggerType == null) { + // void + addImports(ProtoConst.EMPTY_PROTO); + return ProtoConst.EMPTY.getCanonicalName(); + } + + SwaggerTypeAdapter adapter = SwaggerTypeAdapter.create(swaggerType); + String type = tryFindEnumType(adapter.getEnum()); + if (type != null) { + return type; + } + + type = findBaseType(adapter.getType(), adapter.getFormat()); + if (type != null) { + return type; + } + + type = adapter.getRefType(); + if (type != null) { + return type; + } + + Property property = adapter.getArrayItem(); + if (property != null) { + return "repeated " + convertSwaggerType(property); + } + + property = adapter.getMapItem(); + if (property != null) { + return String.format("map<string, %s>", convertSwaggerType(property)); + } + + if (adapter.isObject()) { + addImports(ProtoConst.ANY_PROTO); + return ProtoConst.ANY.getCanonicalName(); + } + + throw new IllegalStateException(String + .format("not support swagger type, class=%s, content=%s.", swaggerType.getClass().getName(), + Json.encode(swaggerType))); + } + + private String tryFindEnumType(List<String> enums) { + if (enums != null && !enums.isEmpty()) { + String strEnums = enums.toString(); + String enumName = "Enum_" + Hashing.sha256().hashString(strEnums, StandardCharsets.UTF_8).toString(); + pending.add(() -> createEnum(enumName, enums)); + return enumName; + } + return null; + } + + private void createEnum(String enumName, List<String> enums) { + if (!messages.add(enumName)) { + // already created + return; + } + + appendLine(msgStringBuilder, "enum %s {", enumName); + for (int idx = 0; idx < enums.size(); idx++) { + appendLine(msgStringBuilder, " %s =%d;", enums.get(idx), idx); + } + appendLine(msgStringBuilder, "}"); + } + + private String findBaseType(String swaggerType, String swaggerFmt) { + String key = swaggerType + ":" + swaggerFmt; + switch (key) { + case "boolean:null": + return "bool"; + // there is no int8/int16 in protobuf + case "integer:null": + return "int64"; + case "integer:int8": + case "integer:int16": + case "integer:int32": + return "int32"; + case "integer:int64": + return "int64"; + case "number:null": + return "double"; + case "number:float": + return "float"; + case "number:double": + return "double"; + case "string:null": + return "string"; + case "string:byte": + return "bytes"; + case "string:date": // LocalDate + case "string:date-time": // Date + return "int64"; + case "file:null": + throw new IllegalStateException("not support swagger type: " + swaggerType); + } + return null; + } + + private void convertOperations() { + Map<String, Path> paths = swagger.getPaths(); + if (paths == null || paths.isEmpty()) { + return; + } + + appendLine(serviceBuilder, "service MainService {"); + for (Path path : paths.values()) { + for (Operation operation : path.getOperationMap().values()) { + convertOpeation(operation); + } + } + serviceBuilder.setLength(serviceBuilder.length() - 1); + appendLine(serviceBuilder, "}"); + } + + private void convertOpeation(Operation operation) { + ProtoMethod protoMethod = new ProtoMethod(); + fillRequestType(operation, protoMethod); + fillResponseType(operation, protoMethod); + + appendLine(serviceBuilder, " //%s%s", ProtoConst.OP_HINT, Json.encode(protoMethod)); + appendLine(serviceBuilder, " rpc %s (%s) returns (%s);\n", operation.getOperationId(), + protoMethod.getArgTypeName(), + protoMethod.findResponse(Status.OK.getStatusCode()).getTypeName()); + } + + private void fillRequestType(Operation operation, ProtoMethod protoMethod) { + List<Parameter> parameters = operation.getParameters(); + if (parameters.isEmpty()) { + addImports(ProtoConst.EMPTY_PROTO); + protoMethod.setArgTypeName(ProtoConst.EMPTY.getCanonicalName()); + return; + } + + if (parameters.size() == 1) { + String type = convertSwaggerType(parameters.get(0)); + if (messages.contains(type)) { + protoMethod.setArgTypeName(type); + return; + } + } + + String wrapName = operation.getOperationId() + "RequestWrap"; + createWrapArgs(wrapName, parameters); + + protoMethod.setArgWrapped(true); + protoMethod.setArgTypeName(wrapName); + } + + private void fillResponseType(Operation operation, ProtoMethod protoMethod) { + for (Entry<String, Response> entry : operation.getResponses().entrySet()) { + String type = convertSwaggerType(entry.getValue().getSchema()); + + ProtoResponse protoResponse = new ProtoResponse(); + protoResponse.setWrapped(!messages.contains(type)); + protoResponse.setTypeName(type); + + if (protoResponse.isWrapped()) { + String wrapName = operation.getOperationId() + "ResponseWrap" + entry.getKey(); + appendLine(msgStringBuilder, "message %s {", wrapName); + appendLine(msgStringBuilder, " %s response = 1;", type); + appendLine(msgStringBuilder, "}"); + + protoResponse.setTypeName(wrapName); + } + protoMethod.addResponse(entry.getKey(), protoResponse); + } + } + + private void createWrapArgs(String wrapName, List<Parameter> parameters) { + appendLine(msgStringBuilder, "message %s {", wrapName); + + int idx = 1; + for (Parameter parameter : parameters) { + String type = convertSwaggerType(parameter); + appendLine(msgStringBuilder, " %s %s = %d;", type, parameter.getName(), idx); + idx++; + } + + appendLine(msgStringBuilder, "}"); + } + + protected Proto createProto() { + StringBuilder sb = new StringBuilder(); + appendLine(sb, "syntax = \"proto3\";"); + for (String importMsg : imports) { + appendLine(sb, "import \"%s\";", importMsg); + } + if (StringUtils.isNotEmpty(protoPackage)) { + sb.append("package ").append(protoPackage).append(";\n"); + } + sb.append(msgStringBuilder); + sb.append(serviceBuilder); + + ProtoParser protoParser = new ProtoParser(); + return protoParser.parseFromContent(sb.toString()); + } +} diff --git a/foundations/foundation-protobuf/src/main/java/org/apache/servicecomb/foundation/protobuf/internal/ProtoConst.java b/foundations/foundation-protobuf/src/main/java/org/apache/servicecomb/foundation/protobuf/internal/ProtoConst.java index c15dbae..53ec0c2 100644 --- a/foundations/foundation-protobuf/src/main/java/org/apache/servicecomb/foundation/protobuf/internal/ProtoConst.java +++ b/foundations/foundation-protobuf/src/main/java/org/apache/servicecomb/foundation/protobuf/internal/ProtoConst.java @@ -18,15 +18,43 @@ package org.apache.servicecomb.foundation.protobuf.internal; import java.util.LinkedHashMap; +import org.apache.servicecomb.foundation.protobuf.internal.parser.ProtoParser; + import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.type.TypeFactory; -public interface ProtoConst { - String PACK_SCHEMA = "type.googleapis.com/"; +import io.protostuff.compiler.model.Message; +import io.protostuff.compiler.model.Proto; + +public final class ProtoConst { + private ProtoConst() { + } + + public static String OP_HINT = " scb:"; + + public static String PACK_SCHEMA = "type.googleapis.com/"; + + public static String JSON_SCHEMA = "json/"; + + public static String JSON_ID_NAME = "@type"; + + public static JavaType MAP_TYPE = TypeFactory.defaultInstance().constructType(LinkedHashMap.class); + + public static Proto ANY_PROTO; + + public static Message ANY; + + public static Proto EMPTY_PROTO; + + public static Message EMPTY; - String JSON_SCHEMA = "json/"; + static { + ProtoParser protoParser = new ProtoParser(); - String JSON_ID_NAME = "@type"; + ANY_PROTO = protoParser.parse("google/protobuf/any.proto"); + ANY = ANY_PROTO.getMessage("Any"); - JavaType MAP_TYPE = TypeFactory.defaultInstance().constructType(LinkedHashMap.class); + EMPTY_PROTO = protoParser.parse("google/protobuf/empty.proto"); + EMPTY = EMPTY_PROTO.getMessage("Empty"); + } } diff --git a/foundations/foundation-protobuf/src/main/java/org/apache/servicecomb/foundation/protobuf/internal/schema/SchemaManager.java b/foundations/foundation-protobuf/src/main/java/org/apache/servicecomb/foundation/protobuf/internal/schema/SchemaManager.java index 49e8c9a..5e7d507 100644 --- a/foundations/foundation-protobuf/src/main/java/org/apache/servicecomb/foundation/protobuf/internal/schema/SchemaManager.java +++ b/foundations/foundation-protobuf/src/main/java/org/apache/servicecomb/foundation/protobuf/internal/schema/SchemaManager.java @@ -17,6 +17,7 @@ package org.apache.servicecomb.foundation.protobuf.internal.schema; import org.apache.servicecomb.foundation.protobuf.ProtoMapper; +import org.apache.servicecomb.foundation.protobuf.internal.ProtoConst; import org.apache.servicecomb.foundation.protobuf.internal.schema.scalar.BoolSchema; import org.apache.servicecomb.foundation.protobuf.internal.schema.scalar.BytesSchema; import org.apache.servicecomb.foundation.protobuf.internal.schema.scalar.DoubleSchema; @@ -48,7 +49,7 @@ public class SchemaManager { } protected boolean isAnyField(Field protoField, boolean repeated) { - return !repeated && protoField.getType().getCanonicalName().equals("google.protobuf.Any"); + return !repeated && protoField.getType().getCanonicalName().equals(ProtoConst.ANY.getCanonicalName()); } protected FieldSchema createScalarField(Field protoField) {
