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/servicecomb-java-chassis.git
commit b1642095ccbc2d53a745db4640735a846efe7512 Author: liubao <[email protected]> AuthorDate: Mon Aug 21 09:46:24 2023 +0800 [SCB-2803]add proto-buffer schema codec --- .../converter/SwaggerToProtoGenerator.java | 30 +-- .../protobuf/schema/SchemaToProtoGenerator.java | 262 +++++++++++++++++++++ .../utils/ScopedProtobufSchemaManager.java | 55 +++++ .../codec/protobuf/schema/TestSchemaCodec.java | 87 +++++++ .../schema/TestSchemaToProtoGenerator.java | 191 +++++++++++++++ .../apache/servicecomb/swagger/SwaggerUtils.java | 83 +++++-- 6 files changed, 665 insertions(+), 43 deletions(-) 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 index a9333148e..6d00adeda 100644 --- 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 @@ -79,22 +79,19 @@ public class SwaggerToProtoGenerator { public Proto convert() { convertDefinitions(); convertOperations(); - for (; ; ) { + do { List<Runnable> oldPending = pending; pending = new ArrayList<>(); for (Runnable runnable : oldPending) { runnable.run(); } - if (pending.isEmpty()) { - break; - } - } + } while (!pending.isEmpty()); return createProto(); } public static String escapePackageName(String name) { - return name.replaceAll("[\\-\\:]", "_"); + return name.replaceAll("[\\-:]", "_"); } public static String escapeMessageName(String name) { @@ -102,10 +99,7 @@ public class SwaggerToProtoGenerator { } public static boolean isValidEnum(String name) { - if (name.contains(".") || name.contains("-")) { - return false; - } - return true; + return !name.contains(".") && !name.contains("-"); } private void convertDefinitions() { @@ -272,18 +266,14 @@ public class SwaggerToProtoGenerator { String key = swaggerType + ":" + swaggerFmt; return switch (key) { case "boolean:null" -> "bool"; - // there is no int8/int16 in protobuf - case "integer:null" -> "int64"; - case "integer:int8", "integer:int16", "integer:int32" -> "int32"; - case "integer:int64" -> "int64"; - case "number:null" -> "double"; + case "integer:null", "integer:int64" -> "int64"; + case "integer:int32" -> "int32"; + case "number:null", "number:double" -> "double"; case "number:float" -> "float"; - case "number:double" -> "double"; case "string:null" -> "string"; - case "string:byte" -> "bytes"; // LocalDate - case "string:date", "string:date-time" -> // Date - "int64"; - case "file:null" -> throw new IllegalStateException("not support swagger type: " + swaggerType); + case "string:byte" -> "bytes"; + case "string:date", "string:date-time" -> "int64"; + case "string:binary" -> throw new IllegalArgumentException("proto buffer not support file upload/download"); default -> null; }; } diff --git a/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/schema/SchemaToProtoGenerator.java b/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/schema/SchemaToProtoGenerator.java new file mode 100644 index 000000000..0e0c4bb6a --- /dev/null +++ b/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/schema/SchemaToProtoGenerator.java @@ -0,0 +1,262 @@ +/* + * 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.schema; + + +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.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.servicecomb.foundation.protobuf.internal.ProtoConst; +import org.apache.servicecomb.foundation.protobuf.internal.parser.ProtoParser; +import org.springframework.util.CollectionUtils; + +import com.google.common.hash.Hashing; + +import io.protostuff.compiler.model.Message; +import io.protostuff.compiler.model.Proto; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public class SchemaToProtoGenerator { + private final String protoPackage; + + private final OpenAPI openAPI; + + private final Schema<?> rootSchema; + + private final String rootName; + + private final Set<String> imports = new HashSet<>(); + + private final Set<String> messages = new HashSet<>(); + + private final StringBuilder msgStringBuilder = new StringBuilder(); + + private List<Runnable> pending = new ArrayList<>(); + + public SchemaToProtoGenerator(String protoPackage, OpenAPI openAPI, Schema<?> rootSchema, String rootName) { + this.protoPackage = protoPackage; + this.openAPI = openAPI; + this.rootSchema = rootSchema; + this.rootName = rootName; + } + + public Proto convert() { + convertSwaggerType(this.rootSchema); + + Map<String, Schema> wrap = new HashMap<>(1); + wrap.put("value", this.rootSchema); + createMessage(rootName, wrap, ProtoConst.ANNOTATION_WRAP_PROPERTY); + + do { + List<Runnable> oldPending = pending; + pending = new ArrayList<>(); + for (Runnable runnable : oldPending) { + runnable.run(); + } + } while (!pending.isEmpty()); + + return createProto(); + } + + 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); + ProtoParser protoParser = new ProtoParser(); + return protoParser.parseFromContent(sb.toString()); + } + + private String convertSwaggerType(Schema<?> swaggerType) { + @SuppressWarnings("unchecked") + String type = tryFindEnumType((List<String>) swaggerType.getEnum()); + if (type != null) { + return type; + } + + type = findBaseType(swaggerType.getType(), swaggerType.getFormat()); + if (type != null) { + return type; + } + + Schema<?> itemProperty = swaggerType.getItems(); + if (itemProperty != null) { + return "repeated " + convertArrayOrMapItem(itemProperty); + } + + itemProperty = swaggerType.getAdditionalItems(); + if (itemProperty != null) { + return String.format("map<string, %s>", convertArrayOrMapItem(itemProperty)); + } + + type = swaggerType.get$ref(); + if (type != null) { + Schema<?> refSchema = openAPI.getComponents().getSchemas().get( + type.substring(Components.COMPONENTS_SCHEMAS_REF.length())); + if (refSchema == null) { + throw new IllegalArgumentException("not found ref in components " + type); + } + return convertSwaggerType(refSchema); + } + + Map<String, Schema> properties = swaggerType.getProperties(); + if (CollectionUtils.isEmpty(properties)) { + addImports(ProtoConst.ANY_PROTO); + return ProtoConst.ANY.getCanonicalName(); + } + createMessage(swaggerType.getName(), properties); + return swaggerType.getName(); + } + + private void addImports(Proto proto) { + imports.add(proto.getFilename()); + for (Message message : proto.getMessages()) { + messages.add(message.getCanonicalName()); + } + } + + 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++) { + if (isValidEnum(enums.get(idx))) { + appendLine(msgStringBuilder, " %s =%d;", enums.get(idx), idx); + } else { + throw new IllegalStateException( + String.format("enum class [%s] name [%s] not supported by protobuffer.", enumName, enums.get(idx))); + } + } + appendLine(msgStringBuilder, "}"); + } + + public static boolean isValidEnum(String name) { + return !name.contains(".") && !name.contains("-"); + } + + 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); + pending.add(() -> createEnum(enumName, enums)); + return enumName; + } + return null; + } + + private String findBaseType(String swaggerType, String swaggerFmt) { + String key = swaggerType + ":" + swaggerFmt; + return switch (key) { + case "boolean:null" -> "bool"; + case "integer:null", "integer:int64" -> "int64"; + case "integer:int32" -> "int32"; + case "number:null", "number:double" -> "double"; + case "number:float" -> "float"; + case "string:null" -> "string"; + case "string:byte" -> "bytes"; + case "string:date", "string:date-time" -> "int64"; + case "string:binary" -> throw new IllegalArgumentException("proto buffer not support file upload/download"); + default -> null; + }; + } + + private String convertArrayOrMapItem(Schema<?> itemProperty) { + // List<List<>>, need to wrap + if (itemProperty.getItems() != null) { + String protoName = generateWrapPropertyName(List.class.getSimpleName(), itemProperty.getItems()); + pending.add(() -> wrapPropertyToMessage(protoName, itemProperty)); + return protoName; + } + + // List<Map<>>, need to wrap + if (itemProperty.getAdditionalItems() != null) { + String protoName = generateWrapPropertyName(Map.class.getSimpleName(), itemProperty.getAdditionalItems()); + pending.add(() -> wrapPropertyToMessage(protoName, itemProperty)); + return protoName; + } + + return convertSwaggerType(itemProperty); + } + + + private String generateWrapPropertyName(String prefix, Schema<?> property) { + // List<List<>>, need to wrap + if (property.getItems() != null) { + return generateWrapPropertyName(prefix + List.class.getSimpleName(), property.getItems()); + } + + // List<Map<>>, need to wrap + if (property.getAdditionalItems() != null) { + return generateWrapPropertyName(prefix + Map.class.getSimpleName(), property.getAdditionalItems()); + } + + // message name cannot have . (package separator) + return prefix + StringUtils.capitalize(escapeMessageName(convertSwaggerType(property))); + } + + public static String escapeMessageName(String name) { + return name.replaceAll("\\.", "_"); + } + + + private void wrapPropertyToMessage(String protoName, Schema<?> property) { + createMessage(protoName, Collections.singletonMap("value", property), ProtoConst.ANNOTATION_WRAP_PROPERTY); + } + + private void createMessage(String protoName, Map<String, Schema> properties, String... annotations) { + if (!messages.add(protoName)) { + // already created + return; + } + + for (String annotation : annotations) { + msgStringBuilder.append("//"); + appendLine(msgStringBuilder, annotation); + } + appendLine(msgStringBuilder, "message %s {", protoName); + int tag = 1; + for (Entry<String, Schema> entry : properties.entrySet()) { + Schema property = entry.getValue(); + String propertyType = convertSwaggerType(property); + + appendLine(msgStringBuilder, " %s %s = %d;", propertyType, entry.getKey(), tag); + tag++; + } + appendLine(msgStringBuilder, "}"); + } +} diff --git a/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/utils/ScopedProtobufSchemaManager.java b/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/utils/ScopedProtobufSchemaManager.java index 0df295d84..7cb40114e 100644 --- a/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/utils/ScopedProtobufSchemaManager.java +++ b/common/common-protobuf/src/main/java/org/apache/servicecomb/codec/protobuf/utils/ScopedProtobufSchemaManager.java @@ -19,14 +19,18 @@ package org.apache.servicecomb.codec.protobuf.utils; import java.util.Map; +import org.apache.commons.lang.StringUtils; import org.apache.servicecomb.codec.protobuf.internal.converter.SwaggerToProtoGenerator; +import org.apache.servicecomb.codec.protobuf.schema.SchemaToProtoGenerator; import org.apache.servicecomb.core.definition.SchemaMeta; import org.apache.servicecomb.foundation.common.concurrent.ConcurrentHashMapEx; import org.apache.servicecomb.foundation.protobuf.ProtoMapper; import org.apache.servicecomb.foundation.protobuf.ProtoMapperFactory; +import org.apache.servicecomb.swagger.SwaggerUtils; import io.protostuff.compiler.model.Proto; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; /** * Manage swagger -> protoBuffer mappings. @@ -35,9 +39,46 @@ import io.swagger.v3.oas.models.OpenAPI; * for each MicroserviceMeta. */ public class ScopedProtobufSchemaManager { + static class SchemaKey { + String schemaId; + + Schema<?> schema; + + int hashCode = -1; + + SchemaKey(String schemaId, Schema<?> schema) { + this.schemaId = schemaId; + this.schema = schema; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SchemaKey other = (SchemaKey) o; + return StringUtils.equals(schemaId, other.schemaId) + && SwaggerUtils.schemaEquals(schema, other.schema); + } + + @Override + public int hashCode() { + if (hashCode != -1) { + return hashCode; + } + hashCode = schemaId.hashCode() ^ SwaggerUtils.schemaHashCode(schema); + return hashCode; + } + } + // Because this class belongs to each SchemaMeta, the key is the schema id. private final Map<String, ProtoMapper> mapperCache = new ConcurrentHashMapEx<>(); + private final Map<SchemaKey, ProtoMapper> schemaMapperCache = new ConcurrentHashMapEx<>(); + public ScopedProtobufSchemaManager() { } @@ -55,4 +96,18 @@ public class ScopedProtobufSchemaManager { return protoMapperFactory.create(proto); }); } + + /** + * get the ProtoMapper from Schema + */ + public ProtoMapper getOrCreateProtoMapper(OpenAPI openAPI, String schemaId, String name, Schema<?> schema) { + SchemaKey schemaKey = new SchemaKey(schemaId, schema); + return schemaMapperCache.computeIfAbsent(schemaKey, key -> { + SchemaToProtoGenerator generator = new SchemaToProtoGenerator("scb.schema", openAPI, + key.schema, name); + Proto proto = generator.convert(); + ProtoMapperFactory protoMapperFactory = new ProtoMapperFactory(); + return protoMapperFactory.create(proto); + }); + } } diff --git a/common/common-protobuf/src/test/java/org/apache/servicecomb/codec/protobuf/schema/TestSchemaCodec.java b/common/common-protobuf/src/test/java/org/apache/servicecomb/codec/protobuf/schema/TestSchemaCodec.java new file mode 100644 index 000000000..a68ac1a1c --- /dev/null +++ b/common/common-protobuf/src/test/java/org/apache/servicecomb/codec/protobuf/schema/TestSchemaCodec.java @@ -0,0 +1,87 @@ +/* + * 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.schema; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.servicecomb.codec.protobuf.utils.ScopedProtobufSchemaManager; +import org.apache.servicecomb.foundation.protobuf.ProtoMapper; +import org.apache.servicecomb.foundation.protobuf.RootDeserializer; +import org.apache.servicecomb.foundation.protobuf.RootSerializer; +import org.apache.servicecomb.foundation.protobuf.internal.bean.PropertyWrapper; +import org.junit.jupiter.api.Test; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.ObjectSchema; +import io.swagger.v3.oas.models.media.StringSchema; + +public class TestSchemaCodec { + ScopedProtobufSchemaManager manager = new ScopedProtobufSchemaManager(); + + @Test + public void test_string_schema_codec() throws Exception { + OpenAPI openAPI = new OpenAPI(); + StringSchema schema = new StringSchema(); + ProtoMapper protoMapper = manager.getOrCreateProtoMapper(openAPI, "test", "input", schema); + RootSerializer serializer = protoMapper.getSerializerSchemaManager() + .createRootSerializer(protoMapper.getProto().getMessage("input"), + String.class); + Map<String, Object> arguments = new HashMap<>(); + arguments.put("value", "abcdefg"); + byte[] result = serializer.serialize(arguments); + RootDeserializer<PropertyWrapper<String>> deserializer = protoMapper.getDeserializerSchemaManager() + .createRootDeserializer(protoMapper.getProto().getMessage("input"), String.class); + PropertyWrapper<String> deserializedResult = deserializer.deserialize(result); + assertEquals("abcdefg", deserializedResult.getValue()); + } + + public static class User { + public String name; + } + + @Test + public void test_object_schema_codec() throws Exception { + OpenAPI openAPI = new OpenAPI(); + + ObjectSchema schema = new ObjectSchema(); + schema.setName("User"); + schema.addProperty("name", new StringSchema()); + openAPI.setComponents(new Components()); + openAPI.getComponents().addSchemas("User", schema); + + ObjectSchema ref = new ObjectSchema(); + ref.set$ref(Components.COMPONENTS_SCHEMAS_REF + "User"); + + ProtoMapper protoMapper = manager.getOrCreateProtoMapper(openAPI, "test", "input", ref); + RootSerializer serializer = protoMapper.getSerializerSchemaManager() + .createRootSerializer(protoMapper.getProto().getMessage("input"), + User.class); + Map<String, Object> arguments = new HashMap<>(); + User user = new User(); + user.name = "abcdefg"; + arguments.put("value", user); + byte[] result = serializer.serialize(arguments); + RootDeserializer<PropertyWrapper<User>> deserializer = protoMapper.getDeserializerSchemaManager() + .createRootDeserializer(protoMapper.getProto().getMessage("input"), User.class); + PropertyWrapper<User> deserializedResult = deserializer.deserialize(result); + assertEquals("abcdefg", deserializedResult.getValue().name); + } +} diff --git a/common/common-protobuf/src/test/java/org/apache/servicecomb/codec/protobuf/schema/TestSchemaToProtoGenerator.java b/common/common-protobuf/src/test/java/org/apache/servicecomb/codec/protobuf/schema/TestSchemaToProtoGenerator.java new file mode 100644 index 000000000..c1b5f029a --- /dev/null +++ b/common/common-protobuf/src/test/java/org/apache/servicecomb/codec/protobuf/schema/TestSchemaToProtoGenerator.java @@ -0,0 +1,191 @@ +/* + * 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.schema; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.servicecomb.codec.protobuf.internal.converter.ProtoToStringGenerator; +import org.apache.servicecomb.swagger.generator.springmvc.SpringmvcSwaggerGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import io.protostuff.compiler.model.Proto; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.ObjectSchema; +import io.swagger.v3.oas.models.media.StringSchema; +import jakarta.ws.rs.core.MediaType; + +@SuppressWarnings("unused") +public class TestSchemaToProtoGenerator { + @Test + public void test_string_schema_is_correct() { + OpenAPI openAPI = new OpenAPI(); + StringSchema schema = new StringSchema(); + SchemaToProtoGenerator generator = + new SchemaToProtoGenerator("test.string", openAPI, schema, "input"); + Proto proto = generator.convert(); + assertEquals(""" + syntax = "proto3"; + package test.string; + + //@WrapProperty + message input { + string value = 1; + } + + """, new ProtoToStringGenerator(proto).protoToString()); + } + + @Test + public void test_object_schema_is_correct() { + OpenAPI openAPI = new OpenAPI(); + + ObjectSchema schema = new ObjectSchema(); + schema.setName("User"); + schema.addProperty("name", new StringSchema()); + openAPI.setComponents(new Components()); + openAPI.getComponents().addSchemas("User", schema); + + ObjectSchema ref = new ObjectSchema(); + ref.set$ref(Components.COMPONENTS_SCHEMAS_REF + "User"); + + SchemaToProtoGenerator generator = + new SchemaToProtoGenerator("test.object", openAPI, ref, "input"); + Proto proto = generator.convert(); + assertEquals(""" + syntax = "proto3"; + package test.object; + + message User { + string name = 1; + } + + //@WrapProperty + message input { + User value = 1; + } + + """, new ProtoToStringGenerator(proto).protoToString()); + } + + static class Model { + public String name; + + public int age; + } + + interface SpringMvcSchema { + @PostMapping("/testInt") + int testInt(@RequestBody int param); + + @PostMapping("/testModel") + Model testModel(@RequestBody Model model); + } + + @Test + public void test_springmvc_int_schema_correct() { + SpringmvcSwaggerGenerator generator = new SpringmvcSwaggerGenerator(SpringMvcSchema.class); + OpenAPI openAPI = generator.generate(); + + SchemaToProtoGenerator protoGenerator = + new SchemaToProtoGenerator("test.int", openAPI, + openAPI.getPaths().get("/testInt").getPost() + .getRequestBody().getContent().get(MediaType.APPLICATION_JSON) + .getSchema(), "testIntRequest"); + Proto proto = protoGenerator.convert(); + assertEquals(""" + syntax = "proto3"; + package test.int; + + //@WrapProperty + message testIntRequest { + int32 value = 1; + } + + """, new ProtoToStringGenerator(proto).protoToString()); + + protoGenerator = + new SchemaToProtoGenerator("test.int", openAPI, + openAPI.getPaths().get("/testInt").getPost() + .getResponses().get("200").getContent().get(MediaType.APPLICATION_JSON) + .getSchema(), "testIntResponse"); + proto = protoGenerator.convert(); + assertEquals(""" + syntax = "proto3"; + package test.int; + + //@WrapProperty + message testIntResponse { + int32 value = 1; + } + + """, new ProtoToStringGenerator(proto).protoToString()); + } + + + @Test + public void test_springmvc_model_schema_correct() { + SpringmvcSwaggerGenerator generator = new SpringmvcSwaggerGenerator(SpringMvcSchema.class); + OpenAPI openAPI = generator.generate(); + + SchemaToProtoGenerator protoGenerator = + new SchemaToProtoGenerator("test.model", openAPI, + openAPI.getPaths().get("/testModel").getPost() + .getRequestBody().getContent().get(MediaType.APPLICATION_JSON) + .getSchema(), "testModelRequest"); + Proto proto = protoGenerator.convert(); + assertEquals(""" + syntax = "proto3"; + package test.model; + + message Model { + string name = 1; + int32 age = 2; + } + + //@WrapProperty + message testModelRequest { + Model value = 1; + } + + """, new ProtoToStringGenerator(proto).protoToString()); + + protoGenerator = + new SchemaToProtoGenerator("test.model", openAPI, + openAPI.getPaths().get("/testModel").getPost() + .getResponses().get("200").getContent().get(MediaType.APPLICATION_JSON) + .getSchema(), "testIntResponse"); + proto = protoGenerator.convert(); + assertEquals(""" + syntax = "proto3"; + package test.model; + + message Model { + string name = 1; + int32 age = 2; + } + + //@WrapProperty + message testIntResponse { + Model value = 1; + } + + """, new ProtoToStringGenerator(proto).protoToString()); + } +} diff --git a/swagger/swagger-generator/generator-core/src/main/java/org/apache/servicecomb/swagger/SwaggerUtils.java b/swagger/swagger-generator/generator-core/src/main/java/org/apache/servicecomb/swagger/SwaggerUtils.java index bf9aa56b8..b8da1ed61 100644 --- a/swagger/swagger-generator/generator-core/src/main/java/org/apache/servicecomb/swagger/SwaggerUtils.java +++ b/swagger/swagger-generator/generator-core/src/main/java/org/apache/servicecomb/swagger/SwaggerUtils.java @@ -239,7 +239,7 @@ public final class SwaggerUtils { if (!componentSchemas.containsKey(entry.getKey())) { componentSchemas.put(entry.getKey(), entry.getValue()); } else { - if (!entry.getValue().equals(componentSchemas.get(entry.getKey()))) { + if (!schemaEquals(entry.getValue(), componentSchemas.get(entry.getKey()))) { throw new IllegalArgumentException("duplicate param model: " + entry.getKey()); } } @@ -250,6 +250,65 @@ public final class SwaggerUtils { return resolvedSchema.schema; } + // swagger api equals method will compare Map address(extensions) + // and is not applicable for usage. + public static int schemaHashCode(Schema<?> schema) { + int result = schema.getType() != null ? schema.getType().hashCode() : 0; + result = result ^ (schema.getFormat() != null ? schema.getFormat().hashCode() : 0); + result = result ^ (schema.getName() != null ? schema.getName().hashCode() : 0); + result = result ^ (schema.get$ref() != null ? schema.get$ref().hashCode() : 0); + result = result ^ (schema.getItems() != null ? schemaHashCode(schema.getItems()) : 0); + result = result ^ (schema.getAdditionalItems() != null ? schemaHashCode(schema.getAdditionalItems()) : 0); + result = result ^ (schema.getProperties() != null ? propertiesHashCode(schema.getProperties()) : 0); + return result; + } + + private static int propertiesHashCode(Map<String, Schema> properties) { + int result = 0; + for (Entry<String, Schema> entry : properties.entrySet()) { + result = result ^ (entry.getKey().hashCode() ^ schemaHashCode(entry.getValue())); + } + return result; + } + + // swagger api equals method will compare Map address(extensions) + // and is not applicable for usage. + public static boolean schemaEquals(Schema<?> schema1, Schema<?> schema2) { + if (schema1 == null && schema2 == null) { + return true; + } + if (schema1 == null || schema2 == null) { + return false; + } + return StringUtils.equals(schema1.getType(), schema2.getType()) + && StringUtils.equals(schema1.getFormat(), schema2.getFormat()) + && StringUtils.equals(schema1.getName(), schema2.getName()) + && StringUtils.equals(schema1.get$ref(), schema2.get$ref()) + && schemaEquals(schema1.getItems(), schema2.getItems()) + && schemaEquals(schema1.getAdditionalItems(), schema2.getAdditionalItems()) + && propertiesEquals(schema1.getProperties(), schema2.getProperties()); + } + + public static boolean propertiesEquals(Map<String, Schema> properties1, Map<String, Schema> properties2) { + if (properties1 == null && properties2 == null) { + return true; + } + if (properties1 == null || properties2 == null) { + return false; + } + if (properties1.size() != properties2.size()) { + return false; + } + boolean result = true; + for (String key : properties1.keySet()) { + if (!schemaEquals(properties1.get(key), properties2.get(key))) { + result = false; + break; + } + } + return result; + } + public static Schema getSchema(OpenAPI swagger, String ref) { return swagger.getComponents().getSchemas().get(ref.substring(Components.COMPONENTS_SCHEMAS_REF.length())); } @@ -310,11 +369,6 @@ public final class SwaggerUtils { return (T) vendorExtensions.get(key); } - public static boolean isBean(RequestBody body) { - MediaType type = body.getContent().values().iterator().next(); - return type.getSchema().get$ref() != null; - } - public static boolean isBean(Type type) { if (type == null) { return false; @@ -362,23 +416,6 @@ public final class SwaggerUtils { } } - public static void updateConsumes(Operation operation, String[] consumes) { - if (consumes == null || consumes.length == 0) { - return; - } - if (operation.getRequestBody() == null) { - operation.setRequestBody(new RequestBody()); - } - if (operation.getRequestBody().getContent() == null) { - operation.getRequestBody().setContent(new Content()); - } - for (String consume : consumes) { - if (operation.getRequestBody().getContent().get(consume) == null) { - operation.getRequestBody().getContent().addMediaType(consume, new MediaType()); - } - } - } - public static boolean methodExists(PathItem pathItem, String httpMethod) { PathItem.HttpMethod method = PathItem.HttpMethod.valueOf(httpMethod); return switch (method) {
