diff --git a/pom.xml b/pom.xml index be8c6d80e734..48d7ead0d802 100644 --- a/pom.xml +++ b/pom.xml @@ -728,7 +728,7 @@ /script/shenyu_checkstyle.xml /script/checkstyle-header.txt true - **/transfer/**/* + **/transfer/**/*,**/generated*/**/* diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ApiServiceImpl.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ApiServiceImpl.java index 979e6d7e1ab9..ad05dbc78855 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ApiServiceImpl.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ApiServiceImpl.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import java.util.Objects; import org.apache.shenyu.admin.disruptor.RegisterClientServerDisruptorPublisher; import org.apache.shenyu.admin.mapper.ApiMapper; import org.apache.shenyu.admin.mapper.TagMapper; @@ -40,7 +41,6 @@ import org.apache.shenyu.admin.model.vo.RuleVO; import org.apache.shenyu.admin.model.vo.TagVO; import org.apache.shenyu.admin.service.ApiService; -import org.apache.shenyu.common.enums.ApiSourceEnum; import org.apache.shenyu.common.enums.PluginEnum; import org.apache.shenyu.common.utils.JsonUtils; import org.apache.shenyu.common.utils.ListUtil; @@ -243,12 +243,14 @@ public ApiVO findById(final String id) { tagVOs = tagDOS.stream().map(TagVO::buildTagVO).collect(Collectors.toList()); } ApiVO apiVO = ApiVO.buildApiVO(item, tagVOs); - if (apiVO.getApiSource().equals(ApiSourceEnum.SWAGGER.getValue())) { + if (StringUtils.isNotBlank(apiVO.getDocument())) { DocItem docItem = JsonUtils.jsonToObject(apiVO.getDocument(), DocItem.class); - apiVO.setRequestHeaders(docItem.getRequestHeaders()); - apiVO.setRequestParameters(docItem.getRequestParameters()); - apiVO.setResponseParameters(docItem.getResponseParameters()); - apiVO.setBizCustomCodeList(docItem.getBizCodeList()); + if (Objects.nonNull(docItem)) { + apiVO.setRequestHeaders(docItem.getRequestHeaders()); + apiVO.setRequestParameters(docItem.getRequestParameters()); + apiVO.setResponseParameters(docItem.getResponseParameters()); + apiVO.setBizCustomCodeList(docItem.getBizCodeList()); + } } return apiVO; diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ApiServiceTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ApiServiceTest.java index dd7b719e769b..7ffc2c87b057 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ApiServiceTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ApiServiceTest.java @@ -108,6 +108,60 @@ public void testFindById() { assertNotNull(byId); } + @Test + public void testFindByIdWithDocumentNotBlank() { + String id = "456"; + ApiDTO apiDTO = new ApiDTO(); + apiDTO.setId(id); + apiDTO.setContextPath("string"); + apiDTO.setApiPath("string"); + apiDTO.setHttpMethod(0); + apiDTO.setConsume("string"); + apiDTO.setProduce("string"); + apiDTO.setVersion("string"); + apiDTO.setRpcType("string"); + apiDTO.setState(0); + apiDTO.setApiOwner("string"); + apiDTO.setApiDesc("string"); + apiDTO.setApiSource(0); + apiDTO.setDocument("{\"module\":\"test-module\",\"requestParameters\":[],\"responseParameters\":[]}"); + ApiDO apiDO = ApiDO.buildApiDO(apiDTO); + Timestamp now = Timestamp.valueOf(LocalDateTime.now()); + apiDO.setDateCreated(now); + apiDO.setDateUpdated(now); + given(this.apiMapper.selectByPrimaryKey(eq(id))).willReturn(apiDO); + ApiVO byId = this.apiService.findById(id); + assertNotNull(byId); + assertNotNull(byId.getRequestParameters()); + assertNotNull(byId.getResponseParameters()); + } + + @Test + public void testFindByIdWithBlankDocument() { + String id = "789"; + ApiDTO apiDTO = new ApiDTO(); + apiDTO.setId(id); + apiDTO.setContextPath("string"); + apiDTO.setApiPath("string"); + apiDTO.setHttpMethod(0); + apiDTO.setConsume("string"); + apiDTO.setProduce("string"); + apiDTO.setVersion("string"); + apiDTO.setRpcType("string"); + apiDTO.setState(0); + apiDTO.setApiOwner("string"); + apiDTO.setApiDesc("string"); + apiDTO.setApiSource(0); + apiDTO.setDocument(""); + ApiDO apiDO = ApiDO.buildApiDO(apiDTO); + Timestamp now = Timestamp.valueOf(LocalDateTime.now()); + apiDO.setDateCreated(now); + apiDO.setDateUpdated(now); + given(this.apiMapper.selectByPrimaryKey(eq(id))).willReturn(apiDO); + ApiVO byId = this.apiService.findById(id); + assertNotNull(byId); + } + @Test public void testListByPage() { PageParameter pageParameter = new PageParameter(); diff --git a/shenyu-client/shenyu-client-core/pom.xml b/shenyu-client/shenyu-client-core/pom.xml index 683afdcc7ce4..e792a70df701 100644 --- a/shenyu-client/shenyu-client-core/pom.xml +++ b/shenyu-client/shenyu-client-core/pom.xml @@ -81,5 +81,55 @@ spring-boot-starter-tomcat test + + com.google.protobuf + protobuf-java + test + + + io.grpc + grpc-stub + test + + + + + + kr.motd.maven + os-maven-plugin + 1.6.2 + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + true + + com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier} + ${project.basedir}/src/test/proto + ${project.build.directory}/generated-test-sources/protobuf/java + false + + + + + test-compile + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + ${project.basedir}/src/test/java + + + + + diff --git a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/client/AbstractContextRefreshedEventListener.java b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/client/AbstractContextRefreshedEventListener.java index 32ef07fcda57..1aef7389b781 100644 --- a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/client/AbstractContextRefreshedEventListener.java +++ b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/client/AbstractContextRefreshedEventListener.java @@ -17,7 +17,6 @@ package org.apache.shenyu.client.core.client; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; @@ -213,7 +212,7 @@ private List buildApiDocDTO(final Object bean, final Method m String apiPath = pathJoin(contextPath, superPath, value); ApiHttpMethodEnum[] value3 = sextet.getValue3(); for (ApiHttpMethodEnum apiHttpMethodEnum : value3) { - String documentJson = buildDocumentJson(pairs.getRight(), apiPath, method); + String documentJson = buildDocumentJson(pairs.getRight(), apiPath, method, sextet.getValue4()); String extJson = buildExtJson(method); ApiDocRegisterDTO build = ApiDocRegisterDTO.builder() .consume(sextet.getValue1()) @@ -258,15 +257,8 @@ protected ApiDocRegisterDTO.ApiExt customApiDocExt(final ApiDocRegisterDTO.ApiEx return ext; } - private String buildDocumentJson(final List tags, final String path, final Method method) { - Map documentMap = ImmutableMap.builder() - .put("tags", tags) - .put("operationId", path) - .put("parameters", OpenApiUtils.generateDocumentParameters(path, method)) - .put("responses", OpenApiUtils.generateDocumentResponse(path)) - .put("responseType", Collections.singletonList(OpenApiUtils.parseReturnType(method))) - .build(); - return GsonUtils.getInstance().toJson(documentMap); + private String buildDocumentJson(final List tags, final String path, final Method method, final RpcTypeEnum rpcTypeEnum) { + return OpenApiUtils.buildDocumentJson(tags, path, method, rpcTypeEnum); } protected abstract Sextet buildApiDocSextet(Method method, Annotation annotation, Map beans); diff --git a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/AbstractApiDocRegistrar.java b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/AbstractApiDocRegistrar.java index 6d2e5358b360..30ddb8828638 100644 --- a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/AbstractApiDocRegistrar.java +++ b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/AbstractApiDocRegistrar.java @@ -17,7 +17,6 @@ package org.apache.shenyu.client.core.register.registrar; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import org.apache.commons.lang3.StringUtils; import org.apache.shenyu.client.apidocs.annotations.ApiDoc; @@ -42,9 +41,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.Map; public abstract class AbstractApiDocRegistrar extends AbstractApiRegistrar { @@ -128,14 +125,7 @@ protected List parse(final ApiBean.ApiDefinition apiDefinitio } private String buildDocumentJson(final List tags, final String path, final Method method) { - Map documentMap = ImmutableMap.builder() - .put("tags", tags) - .put("operationId", path) - .put("parameters", OpenApiUtils.generateDocumentParameters(path, method)) - .put("responses", OpenApiUtils.generateDocumentResponse(path)) - .put("responseType", Collections.singletonList(OpenApiUtils.parseReturnType(method))) - .build(); - return GsonUtils.getInstance().toJson(documentMap); + return OpenApiUtils.buildDocumentJson(tags, path, method, rpcTypeEnum); } private String buildExtJson(final ApiBean.ApiDefinition apiDefinition) { diff --git a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/ApiDocRegistrarImpl.java b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/ApiDocRegistrarImpl.java index 91b842f97f5d..10d07877a4b2 100644 --- a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/ApiDocRegistrarImpl.java +++ b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/ApiDocRegistrarImpl.java @@ -17,7 +17,6 @@ package org.apache.shenyu.client.core.register.registrar; -import com.google.common.collect.ImmutableMap; import org.apache.commons.lang3.StringUtils; import org.apache.shenyu.client.core.constant.ShenyuClientConstants; import org.apache.shenyu.client.core.disruptor.ShenyuClientRegisterEventPublisher; @@ -38,7 +37,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -132,14 +130,9 @@ private String getDocument(final ApiBean.ApiDefinition api) { return document; } final String path = getPath(api); - final Map documentMap = ImmutableMap.builder() - .put("tags", buildTags(api)) - .put("operationId", path) - .put("parameters", OpenApiUtils.generateDocumentParameters(path, api.getApiMethod())) - .put("responses", OpenApiUtils.generateDocumentResponse(path)) - .put("responseType", Collections.singletonList(OpenApiUtils.parseReturnType(api.getApiMethod()))) - .build(); - return GsonUtils.getInstance().toJson(documentMap); + final String rpcType = getRpcType(api); + RpcTypeEnum rpcTypeEnum = RpcTypeEnum.acquireByName(rpcType); + return OpenApiUtils.buildDocumentJson(buildTags(api), path, api.getApiMethod(), rpcTypeEnum); } private String getRpcType(final ApiBean.ApiDefinition api) { diff --git a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/OpenApiUtils.java b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/OpenApiUtils.java index 5affbce77a91..d74ba59b6915 100644 --- a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/OpenApiUtils.java +++ b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/OpenApiUtils.java @@ -21,6 +21,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.shenyu.client.core.constant.ShenyuClientConstants; +import org.apache.shenyu.common.enums.RpcTypeEnum; +import org.apache.shenyu.common.utils.GsonUtils; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; @@ -37,6 +39,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -58,16 +61,73 @@ public class OpenApiUtils { private static final String[] QUERY_CLASSES = new String[]{"org.springframework.web.bind.annotation.RequestParam", "org.springframework.web.bind.annotation.RequestPart"}; + /** + * Check if the given RPC type uses Spring MVC parameter parsing. + * HTTP, WebSocket and Spring Cloud types use Spring MVC annotations for parameter resolution, + * while other RPC types (Dubbo, gRPC, etc.) parse parameters from Java method signatures directly. + * + * @param rpcTypeEnum the RPC type enum + * @return true if Spring MVC parameter parsing should be used + */ + public static boolean useSpringMvcParamParsing(final RpcTypeEnum rpcTypeEnum) { + return rpcTypeEnum == RpcTypeEnum.HTTP + || rpcTypeEnum == RpcTypeEnum.WEB_SOCKET + || rpcTypeEnum == RpcTypeEnum.SPRING_CLOUD; + } /** - * generateDocumentParameters. + * Build document JSON string for the given API method. + * Dispatches to the appropriate parameter/response generation based on RPC type. + * + * @param tags the API tags + * @param path the API path + * @param method the Java method + * @param rpcTypeEnum the RPC type + * @return document JSON string + */ + public static String buildDocumentJson(final List tags, final String path, + final Method method, final RpcTypeEnum rpcTypeEnum) { + boolean useSpringMvcParamParsing = useSpringMvcParamParsing(rpcTypeEnum); + Map documentMap; + if (useSpringMvcParamParsing) { + documentMap = ImmutableMap.builder() + .put("tags", tags) + .put("operationId", path) + .put("requestParameters", generateRequestDocParameters(path, method)) + .put("responseParameters", Collections.singletonList(parseReturnType(method))) + .put("responses", generateDocumentResponse(path)) + .build(); + } else if (rpcTypeEnum == RpcTypeEnum.GRPC) { + documentMap = ImmutableMap.builder() + .put("tags", tags) + .put("operationId", path) + .put("requestParameters", generateGrpcRequestDocParameters(method)) + .put("responseParameters", Collections.singletonList(parseGrpcReturnType(method))) + .put("responses", generateGrpcDocumentResponse(path, method)) + .build(); + } else { + documentMap = ImmutableMap.builder() + .put("tags", tags) + .put("operationId", path) + .put("requestParameters", generateRpcRequestDocParameters(method)) + .put("responseParameters", Collections.singletonList(parseReturnType(method))) + .put("responses", generateRpcDocumentResponse(path, method)) + .build(); + } + return GsonUtils.getInstance().toJson(documentMap); + } + + + /** + * Generate request parameters for HTTP methods. + * This produces Parameter objects with name, in, type, and refs fields. * * @param path the api path * @param method the method - * @return documentParameters + * @return request parameters */ - public static List generateDocumentParameters(final String path, final Method method) { - ArrayList list = new ArrayList<>(); + public static List generateRequestDocParameters(final String path, final Method method) { + List list = new ArrayList<>(); Pair query = isQuery(method); if (query.getLeft()) { for (Annotation[] annotations : query.getRight()) { @@ -89,34 +149,182 @@ public static List generateDocumentParameters(final String path, fina parameter.setIn("query"); parameter.setRequired(required); parameter.setName(name); - parameter.setSchema(new Schema("string", null)); + parameter.setType("string"); list.add(parameter); } } } - } else { - List segments = UrlPathUtils.getSegments(path); - for (String segment : segments) { - if (EVERY_PATH.equals(segment)) { - Parameter parameter = new Parameter(); - parameter.setIn("path"); - parameter.setName(segment); - parameter.setRequired(true); - parameter.setSchema(new Schema("string", null)); - list.add(parameter); + } + List segments = UrlPathUtils.getSegments(path); + for (String segment : segments) { + if (EVERY_PATH.equals(segment)) { + Parameter parameter = new Parameter(); + parameter.setIn("path"); + parameter.setName(segment); + parameter.setRequired(true); + parameter.setType("string"); + list.add(parameter); + } + if (segment.startsWith(LEFT_ANGLE_BRACKETS) && segment.endsWith(RIGHT_ANGLE_BRACKETS)) { + String name = segment.substring(1, segment.length() - 1); + Parameter parameter = new Parameter(); + parameter.setIn("path"); + parameter.setName(name); + parameter.setRequired(true); + parameter.setType("string"); + list.add(parameter); + } + } + return list; + } + + /** + * Generate request parameters for RPC methods. + * Unlike HTTP methods that use Spring annotations, RPC method parameters + * are parsed from Java method parameter types directly. + + * @param method the method + * @return request parameters + */ + public static List generateRpcRequestDocParameters(final Method method) { + List list = new ArrayList<>(); + java.lang.reflect.Parameter[] methodParams = method.getParameters(); + for (java.lang.reflect.Parameter methodParam : methodParams) { + Type paramType = methodParam.getParameterizedType(); + Schema schema = parseSchema(paramType, 0, new HashMap<>(16)); + Parameter parameter = convertSchemaToParameter(methodParam.getName(), schema); + parameter.setRequired(true); + list.add(parameter); + } + return list; + } + + /** + * Generate request parameters for gRPC methods. + * gRPC method signatures differ from other RPC types: + * - Unary/ServerStreaming: void method(Request req, StreamObserver{Response} observer) + * - ClientStreaming/BidiStreaming: StreamObserver{Request} method(StreamObserver{Response} observer) + * StreamObserver parameters are excluded from request parameters. + * + * @param method the method + * @return request parameters + */ + public static List generateGrpcRequestDocParameters(final Method method) { + List list = new ArrayList<>(); + java.lang.reflect.Parameter[] methodParams = method.getParameters(); + for (java.lang.reflect.Parameter methodParam : methodParams) { + if (isStreamObserver(methodParam.getType())) { + continue; + } + Type paramType = methodParam.getParameterizedType(); + Schema schema = parseSchema(paramType, 0, new HashMap<>(16)); + Parameter parameter = convertSchemaToParameter(methodParam.getName(), schema); + parameter.setRequired(true); + list.add(parameter); + } + if (list.isEmpty()) { + Type returnType = method.getGenericReturnType(); + Type actualType = extractStreamObserverTypeParam(returnType); + if (Objects.nonNull(actualType)) { + Schema schema = parseSchema(actualType, 0, new HashMap<>(16)); + Parameter parameter = convertSchemaToParameter("request", schema); + parameter.setRequired(true); + list.add(parameter); + } + } + return list; + } + + private static Parameter convertSchemaToParameter(final String name, final Schema schema) { + Parameter parameter = new Parameter(); + parameter.setName(name); + parameter.setType(schema.getType()); + if (Objects.nonNull(schema.getRefs()) && !schema.getRefs().isEmpty()) { + List refs = new ArrayList<>(); + for (Schema ref : schema.getRefs()) { + refs.add(convertSchemaToParameter(ref.getName(), ref)); + } + parameter.setRefs(refs); + } + return parameter; + } + + /** + * Parse return type for gRPC methods. + * - Unary/ServerStreaming: response type is extracted from StreamObserver{Response} parameter + * - ClientStreaming/BidiStreaming: response type is extracted from StreamObserver{Response} parameter + * + * @param method the method + * @return response type + */ + public static ResponseType parseGrpcReturnType(final Method method) { + java.lang.reflect.Parameter[] methodParams = method.getParameters(); + for (java.lang.reflect.Parameter methodParam : methodParams) { + if (isStreamObserver(methodParam.getType())) { + Type paramType = methodParam.getParameterizedType(); + Type actualType = extractStreamObserverTypeParam(paramType); + if (Objects.nonNull(actualType)) { + return parseType("ROOT", actualType, 0, new HashMap<>(16)); } - if (segment.startsWith(LEFT_ANGLE_BRACKETS) && segment.endsWith(RIGHT_ANGLE_BRACKETS)) { - String name = segment.substring(1, segment.length() - 1); - Parameter parameter = new Parameter(); - parameter.setIn("path"); - parameter.setName(name); - parameter.setRequired(true); - parameter.setSchema(new Schema("string", null)); - list.add(parameter); + } + } + ResponseType voidType = new ResponseType(); + voidType.setName("ROOT"); + voidType.setType("void"); + return voidType; + } + + /** + * Generate document response for gRPC methods. + * + * @param path the api path + * @param method the method + * @return documentResponseMap + */ + public static Map generateGrpcDocumentResponse(final String path, final Method method) { + String returnTypeStr = "void"; + java.lang.reflect.Parameter[] methodParams = method.getParameters(); + for (java.lang.reflect.Parameter methodParam : methodParams) { + if (isStreamObserver(methodParam.getType())) { + Type paramType = methodParam.getParameterizedType(); + Type actualType = extractStreamObserverTypeParam(paramType); + if (Objects.nonNull(actualType)) { + returnTypeStr = resolveTypeName(actualType); } + break; } } - return list; + ImmutableMap contentMap = ImmutableMap.builder() + .put(ShenyuClientConstants.MEDIA_TYPE_ALL_VALUE, ImmutableMap.of("schema", ImmutableMap.of("type", returnTypeStr))) + .build(); + ImmutableMap successMap = ImmutableMap.builder() + .put("description", path) + .put("content", contentMap).build(); + ImmutableMap notFoundMap = ImmutableMap.builder() + .put("description", StringUtils.join("the path [", path, "] not found")) + .put("content", ImmutableMap.of(ShenyuClientConstants.MEDIA_TYPE_ALL_VALUE, + ImmutableMap.of("schema", ImmutableMap.of("type", "string")))).build(); + return ImmutableMap.builder() + .put("200", successMap) + .put("404", notFoundMap) + .build(); + } + + private static boolean isStreamObserver(final Class clazz) { + return "io.grpc.stub.StreamObserver".equals(clazz.getName()); + } + + private static Type extractStreamObserverTypeParam(final Type type) { + if (type instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) type; + if ("io.grpc.stub.StreamObserver".equals(((Class) pt.getRawType()).getName())) { + Type[] actualTypes = pt.getActualTypeArguments(); + if (actualTypes.length > 0) { + return actualTypes[0]; + } + } + } + return null; } private static Pair isQuery(final Method method) { @@ -125,7 +333,6 @@ private static Pair isQuery(final Method method) { if (parameterAnnotation.length > 0 && isQueryName(parameterAnnotation[0].annotationType().getName(), QUERY_CLASSES)) { return Pair.of(true, parameterAnnotations); } - return Pair.of(false, null); } return Pair.of(false, null); } @@ -139,6 +346,248 @@ private static boolean isQueryName(final String name, final String[] names) { return false; } + /** + * Generate document response for RPC methods with actual return type info. + * + * @param path the api path + * @param method the method + * @return documentResponseMap + */ + public static Map generateRpcDocumentResponse(final String path, final Method method) { + Type returnType = method.getGenericReturnType(); + String returnTypeStr = "void".equals(returnType.getTypeName()) ? "void" : resolveTypeName(returnType); + ImmutableMap contentMap = ImmutableMap.builder() + .put(ShenyuClientConstants.MEDIA_TYPE_ALL_VALUE, ImmutableMap.of("schema", ImmutableMap.of("type", returnTypeStr))) + .build(); + ImmutableMap successMap = ImmutableMap.builder() + .put("description", path) + .put("content", contentMap).build(); + ImmutableMap notFoundMap = ImmutableMap.builder() + .put("description", StringUtils.join("the path [", path, "] not found")) + .put("content", ImmutableMap.of(ShenyuClientConstants.MEDIA_TYPE_ALL_VALUE, + ImmutableMap.of("schema", ImmutableMap.of("type", "string")))).build(); + return ImmutableMap.builder() + .put("200", successMap) + .put("404", notFoundMap) + .build(); + } + + private static String resolveTypeName(final Type type) { + if (type instanceof Class) { + return resolveTypeName((Class) type); + } + if (type instanceof ParameterizedType) { + Class rawType = (Class) ((ParameterizedType) type).getRawType(); + if (Collection.class.isAssignableFrom(rawType)) { + return "array"; + } + return "object"; + } + return "object"; + } + + private static String resolveTypeName(final Class clazz) { + if (isBooleanType(clazz)) { + return "boolean"; + } else if (isIntegerType(clazz)) { + return "integer"; + } else if (isNumberType(clazz)) { + return "number"; + } else if (isStringType(clazz)) { + return "string"; + } else if (isDateType(clazz)) { + return "string"; + } else if (clazz.isArray() || Collection.class.isAssignableFrom(clazz)) { + return "array"; + } else if (clazz.isEnum()) { + return "string"; + } else if (isProtobufMessage(clazz)) { + return "object"; + } else if (Map.class.isAssignableFrom(clazz)) { + return "object"; + } else { + return "object"; + } + } + + /** + * Check if the class is a protobuf-generated message class. + * Uses reflection to avoid direct protobuf dependency. + * + * @param clazz the class to check + * @return true if it's a protobuf message class + */ + static boolean isProtobufMessage(final Class clazz) { + if (Objects.isNull(clazz)) { + return false; + } + Class current = clazz; + while (Objects.nonNull(current) && current != Object.class) { + String className = current.getName(); + if ("com.google.protobuf.GeneratedMessageV3".equals(className) + || "com.google.protobuf.GeneratedMessage".equals(className) + || "com.google.protobuf.GeneratedMessageLite".equals(className)) { + return true; + } + current = current.getSuperclass(); + } + return false; + } + + /** + * Check if the class is com.google.protobuf.Empty. + * + * @param clazz the class to check + * @return true if it's protobuf Empty + */ + private static boolean isProtobufEmpty(final Class clazz) { + return Objects.nonNull(clazz) && "com.google.protobuf.Empty".equals(clazz.getName()); + } + + /** + * Parse protobuf message fields using reflection on getDescriptor(). + * Protobuf descriptor types are mapped to OpenAPI types. + * + * @param responseType the response type to populate + * @param clazz the protobuf message class + * @param depth current recursion depth + * @param typeVariableMap type variable map + * @return populated ResponseType + */ + private static ResponseType parseProtobufClass(final ResponseType responseType, final Class clazz, + final int depth, final Map, Type> typeVariableMap) { + if (isProtobufEmpty(clazz)) { + responseType.setType("object"); + return responseType; + } + try { + java.lang.reflect.Method getDescriptorMethod = clazz.getMethod("getDescriptor"); + Object descriptor = getDescriptorMethod.invoke(null); + java.lang.reflect.Method getFieldsMethod = descriptor.getClass().getMethod("getFields"); + @SuppressWarnings("unchecked") + List fields = (List) getFieldsMethod.invoke(descriptor); + List refs = parseProtobufFields(fields, clazz, depth, typeVariableMap); + responseType.setType("object"); + responseType.setRefs(refs); + } catch (Exception e) { + responseType.setType("object"); + } + return responseType; + } + + private static List parseProtobufFields(final List fields, final Class clazz, + final int depth, final Map, Type> typeVariableMap) throws Exception { + List refs = new ArrayList<>(); + java.lang.reflect.Method getNameMethod = null; + java.lang.reflect.Method getTypeMethod = null; + java.lang.reflect.Method isRepeatedMethod = null; + java.lang.reflect.Method getMessageTypeMethod = null; + for (Object field : fields) { + if (Objects.isNull(getNameMethod)) { + getNameMethod = field.getClass().getMethod("getName"); + getTypeMethod = field.getClass().getMethod("getType"); + isRepeatedMethod = field.getClass().getMethod("isRepeated"); + getMessageTypeMethod = field.getClass().getMethod("getMessageType"); + } + String fieldName = (String) getNameMethod.invoke(field); + Object fieldType = getTypeMethod.invoke(field); + boolean isRepeated = (boolean) isRepeatedMethod.invoke(field); + refs.add(parseProtobufField(fieldName, fieldType, isRepeated, + getMessageTypeMethod, field, clazz, depth, typeVariableMap)); + } + return refs; + } + + private static ResponseType parseProtobufField(final String fieldName, final Object fieldType, final boolean isRepeated, + final java.lang.reflect.Method getMessageTypeMethod, final Object field, + final Class clazz, final int depth, + final Map, Type> typeVariableMap) throws Exception { + if (isRepeated) { + return parseRepeatedProtobufField(fieldName, fieldType, getMessageTypeMethod, field); + } + String fieldTypeName = fieldType.toString(); + ResponseType fieldResponse = new ResponseType(); + fieldResponse.setName(fieldName); + if ("MESSAGE".equals(fieldTypeName)) { + resolveProtobufMessageField(fieldResponse, getMessageTypeMethod, field, clazz, depth, typeVariableMap); + } else if ("ENUM".equals(fieldTypeName)) { + fieldResponse.setType("string"); + } else { + fieldResponse.setType(mapProtobufTypeToOpenApi(fieldTypeName)); + } + return fieldResponse; + } + + private static ResponseType parseRepeatedProtobufField(final String fieldName, final Object fieldType, + final java.lang.reflect.Method getMessageTypeMethod, + final Object field) throws Exception { + ResponseType arrayType = new ResponseType(); + arrayType.setName(fieldName); + arrayType.setType("array"); + String fieldTypeName = fieldType.toString(); + ResponseType elementType = new ResponseType(); + elementType.setName("ITEMS"); + if ("MESSAGE".equals(fieldTypeName)) { + Object msgDescriptor = getMessageTypeMethod.invoke(field); + java.lang.reflect.Method getFullNameMethod = msgDescriptor.getClass().getMethod("getFullName"); + String fullMsgName = (String) getFullNameMethod.invoke(msgDescriptor); + elementType.setType("object"); + elementType.setDescription(fullMsgName); + } else { + elementType.setType(mapProtobufTypeToOpenApi(fieldTypeName)); + } + arrayType.setRefs(Collections.singletonList(elementType)); + return arrayType; + } + + private static void resolveProtobufMessageField(final ResponseType fieldResponse, + final java.lang.reflect.Method getMessageTypeMethod, + final Object field, final Class clazz, + final int depth, + final Map, Type> typeVariableMap) throws Exception { + Object msgDescriptor = getMessageTypeMethod.invoke(field); + java.lang.reflect.Method getFullNameMethod = msgDescriptor.getClass().getMethod("getFullName"); + String fullMsgName = (String) getFullNameMethod.invoke(msgDescriptor); + fieldResponse.setType("object"); + fieldResponse.setDescription(fullMsgName); + if (depth < 5) { + try { + Class nestedClass = Class.forName(toJavaClassName(clazz, fullMsgName)); + List nestedRefs = parseProtobufClass(new ResponseType(), nestedClass, depth + 1, typeVariableMap).getRefs(); + if (Objects.nonNull(nestedRefs) && !nestedRefs.isEmpty()) { + fieldResponse.setRefs(nestedRefs); + } + } catch (ClassNotFoundException ignored) { + } + } + } + + private static String mapProtobufTypeToOpenApi(final String protobufType) { + switch (protobufType) { + case "INT32": + case "INT64": + case "UINT32": + case "UINT64": + case "SINT32": + case "SINT64": + case "FIXED32": + case "FIXED64": + case "SFIXED32": + case "SFIXED64": + return "integer"; + case "FLOAT": + case "DOUBLE": + return "number"; + case "BOOL": + return "boolean"; + case "STRING": + case "BYTES": + return "string"; + default: + return "string"; + } + } + /** * generateDocumentResponse. * @@ -236,6 +685,8 @@ private static ResponseType parseClass(final ResponseType responseType, final Cl } else if (isDateType(clazz)) { responseType.setType("date"); return responseType; + } else if (isProtobufMessage(clazz)) { + return parseProtobufClass(responseType, clazz, depth, typeVariableMap); } else { List refs = new ArrayList<>(); for (Field field : clazz.getDeclaredFields()) { @@ -296,6 +747,228 @@ private static ResponseType parseGenericArrayType(final ResponseType responseTyp return responseType; } + private static Schema parseSchema(final Type type, final int depth, final Map, Type> typeVariableMap) { + if (depth > 5) { + return new Schema("object", null); + } + if (type instanceof Class) { + return parseClassSchema((Class) type, depth, typeVariableMap); + } else if (type instanceof ParameterizedType) { + return parseParameterizedTypeSchema((ParameterizedType) type, depth, typeVariableMap); + } else if (type instanceof GenericArrayType) { + Schema elementSchema = parseSchema(((GenericArrayType) type).getGenericComponentType(), depth + 1, typeVariableMap); + Schema schema = new Schema("array", null); + schema.setRefs(Collections.singletonList(elementSchema)); + return schema; + } else if (type instanceof TypeVariable) { + Type actualType = typeVariableMap.get(type); + if (Objects.nonNull(actualType)) { + return parseSchema(actualType, depth, typeVariableMap); + } else if (((TypeVariable) type).getBounds().length > 0) { + return parseSchema(((TypeVariable) type).getBounds()[0], depth, typeVariableMap); + } else { + return new Schema("object", null); + } + } else { + return new Schema("object", null); + } + } + + private static Schema parseClassSchema(final Class clazz, final int depth, final Map, Type> typeVariableMap) { + if (clazz.isArray()) { + Schema elementSchema = parseSchema(clazz.getComponentType(), depth + 1, typeVariableMap); + Schema schema = new Schema("array", null); + schema.setRefs(Collections.singletonList(elementSchema)); + return schema; + } else if (clazz.isEnum()) { + return new Schema("string", null); + } else if (isBooleanType(clazz)) { + return new Schema("boolean", null); + } else if (isIntegerType(clazz)) { + return new Schema("integer", null); + } else if (isNumberType(clazz)) { + return new Schema("number", null); + } else if (isStringType(clazz)) { + return new Schema("string", null); + } else if (isDateType(clazz)) { + return new Schema("string", "date"); + } else if (Collection.class.isAssignableFrom(clazz)) { + return new Schema("array", null); + } else if (Map.class.isAssignableFrom(clazz)) { + return new Schema("object", null); + } else if (isProtobufMessage(clazz)) { + return parseProtobufClassSchema(clazz, depth, typeVariableMap); + } else { + List refs = new ArrayList<>(); + for (Field field : clazz.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + Schema fieldSchema = parseSchema(field.getGenericType(), depth + 1, typeVariableMap); + fieldSchema.setName(field.getName()); + refs.add(fieldSchema); + } + Schema schema = new Schema("object", null); + schema.setRefs(refs); + return schema; + } + } + + private static Schema parseParameterizedTypeSchema(final ParameterizedType type, final int depth, final Map, Type> typeVariableMap) { + Class rawType = (Class) type.getRawType(); + Type[] actualTypeArguments = type.getActualTypeArguments(); + TypeVariable[] typeVariables = rawType.getTypeParameters(); + Map, Type> newTypeVariableMap = new HashMap<>(typeVariableMap); + for (int i = 0; i < typeVariables.length; i++) { + newTypeVariableMap.put(typeVariables[i], actualTypeArguments[i]); + } + if (Collection.class.isAssignableFrom(rawType)) { + Schema elementSchema = parseSchema(actualTypeArguments[0], depth + 1, newTypeVariableMap); + elementSchema.setName("items"); + Schema schema = new Schema("array", null); + schema.setRefs(Collections.singletonList(elementSchema)); + return schema; + } else if (Map.class.isAssignableFrom(rawType)) { + Schema keySchema = parseSchema(actualTypeArguments[0], depth + 1, newTypeVariableMap); + keySchema.setName("key"); + Schema valueSchema = parseSchema(actualTypeArguments[1], depth + 1, newTypeVariableMap); + valueSchema.setName("value"); + Schema schema = new Schema("object", null); + schema.setRefs(Arrays.asList(keySchema, valueSchema)); + return schema; + } else { + List refs = new ArrayList<>(); + for (Field field : rawType.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + Schema fieldSchema = parseSchema(field.getGenericType(), depth + 1, newTypeVariableMap); + fieldSchema.setName(field.getName()); + refs.add(fieldSchema); + } + Schema schema = new Schema("object", null); + schema.setRefs(refs); + return schema; + } + } + + private static Schema parseProtobufClassSchema(final Class clazz, final int depth, final Map, Type> typeVariableMap) { + if (isProtobufEmpty(clazz)) { + return new Schema("object", null); + } + try { + java.lang.reflect.Method getDescriptorMethod = clazz.getMethod("getDescriptor"); + Object descriptor = getDescriptorMethod.invoke(null); + java.lang.reflect.Method getFieldsMethod = descriptor.getClass().getMethod("getFields"); + @SuppressWarnings("unchecked") + List fields = (List) getFieldsMethod.invoke(descriptor); + List refs = parseProtobufFieldsSchema(fields, clazz, depth, typeVariableMap); + Schema schema = new Schema("object", null); + schema.setRefs(refs); + return schema; + } catch (Exception e) { + return new Schema("object", null); + } + } + + private static List parseProtobufFieldsSchema(final List fields, final Class clazz, + final int depth, final Map, Type> typeVariableMap) throws Exception { + List refs = new ArrayList<>(); + java.lang.reflect.Method getNameMethod = null; + java.lang.reflect.Method getTypeMethod = null; + java.lang.reflect.Method isRepeatedMethod = null; + java.lang.reflect.Method getMessageTypeMethod = null; + for (Object field : fields) { + if (Objects.isNull(getNameMethod)) { + getNameMethod = field.getClass().getMethod("getName"); + getTypeMethod = field.getClass().getMethod("getType"); + isRepeatedMethod = field.getClass().getMethod("isRepeated"); + getMessageTypeMethod = field.getClass().getMethod("getMessageType"); + } + String fieldName = (String) getNameMethod.invoke(field); + Object fieldType = getTypeMethod.invoke(field); + boolean isRepeated = (boolean) isRepeatedMethod.invoke(field); + refs.add(parseProtobufFieldSchema(fieldName, fieldType, isRepeated, + getMessageTypeMethod, field, clazz, depth, typeVariableMap)); + } + return refs; + } + + private static Schema parseProtobufFieldSchema(final String fieldName, final Object fieldType, final boolean isRepeated, + final java.lang.reflect.Method getMessageTypeMethod, final Object field, + final Class clazz, final int depth, + final Map, Type> typeVariableMap) throws Exception { + if (isRepeated) { + return parseRepeatedProtobufFieldSchema(fieldName, fieldType, getMessageTypeMethod, field, clazz, depth, typeVariableMap); + } + String fieldTypeName = fieldType.toString(); + Schema schema; + if ("MESSAGE".equals(fieldTypeName)) { + schema = resolveProtobufMessageFieldSchema(getMessageTypeMethod, field, clazz, depth, typeVariableMap); + } else if ("ENUM".equals(fieldTypeName)) { + schema = new Schema("string", null); + } else { + schema = new Schema(mapProtobufTypeToOpenApi(fieldTypeName), null); + } + schema.setName(fieldName); + return schema; + } + + private static Schema parseRepeatedProtobufFieldSchema(final String fieldName, final Object fieldType, + final java.lang.reflect.Method getMessageTypeMethod, + final Object field, + final Class clazz, final int depth, + final Map, Type> typeVariableMap) throws Exception { + String fieldTypeName = fieldType.toString(); + Schema elementSchema; + if ("MESSAGE".equals(fieldTypeName)) { + elementSchema = resolveProtobufMessageFieldSchema(getMessageTypeMethod, field, clazz, depth, typeVariableMap); + } else if ("ENUM".equals(fieldTypeName)) { + elementSchema = new Schema("string", null); + } else { + elementSchema = new Schema(mapProtobufTypeToOpenApi(fieldTypeName), null); + } + elementSchema.setName("items"); + Schema schema = new Schema("array", null); + schema.setName(fieldName); + schema.setRefs(Collections.singletonList(elementSchema)); + return schema; + } + + private static Schema resolveProtobufMessageFieldSchema(final java.lang.reflect.Method getMessageTypeMethod, + final Object field, final Class clazz, + final int depth, + final Map, Type> typeVariableMap) throws Exception { + Object msgDescriptor = getMessageTypeMethod.invoke(field); + java.lang.reflect.Method getFullNameMethod = msgDescriptor.getClass().getMethod("getFullName"); + String fullMsgName = (String) getFullNameMethod.invoke(msgDescriptor); + Schema schema = new Schema("object", null); + if (depth < 5) { + try { + Class nestedClass = Class.forName(toJavaClassName(clazz, fullMsgName)); + List nestedRefs = parseProtobufClassSchema(nestedClass, depth + 1, typeVariableMap).getRefs(); + if (Objects.nonNull(nestedRefs) && !nestedRefs.isEmpty()) { + schema.setRefs(nestedRefs); + } + } catch (ClassNotFoundException ignored) { + } + } + return schema; + } + + private static String toJavaClassName(final Class contextClass, final String protobufFullName) { + String packageName = contextClass.getPackage().getName(); + if (protobufFullName.startsWith(packageName + ".")) { + String relativeName = protobufFullName.substring(packageName.length() + 1).replace('.', '$'); + Class declaringClass = contextClass.getDeclaringClass(); + if (Objects.nonNull(declaringClass)) { + return declaringClass.getName() + "$" + relativeName; + } + return packageName + "." + relativeName; + } + return protobufFullName.replace('.', '$'); + } + private static boolean isDateType(final Class clazz) { return clazz == Date.class || clazz == LocalDate.class || clazz == LocalDateTime.class || clazz == LocalTime.class; @@ -368,7 +1041,6 @@ public void setRefs(final List refs) { } } - public static class Parameter { private String name; @@ -379,145 +1051,105 @@ public static class Parameter { private boolean required; - private Schema schema; + private String type; + + private List refs; - /** - * get name. - * - * @return name - */ public String getName() { return name; } - /** - * set name. - * - * @param name name - */ public void setName(final String name) { this.name = name; } - /** - * get in. - * - * @return in - */ public String getIn() { return in; } - /** - * set in. - * - * @param in in - */ public void setIn(final String in) { this.in = in; } - /** - * get description. - * - * @return description - */ public String getDescription() { return description; } - /** - * set description. - * - * @param description description - */ public void setDescription(final String description) { this.description = description; } - /** - * get required. - * - * @return required - */ public boolean isRequired() { return required; } - /** - * set required. - * - * @param required required - */ public void setRequired(final boolean required) { this.required = required; } - /** - * get schema. - * - * @return schema - */ - public Schema getSchema() { - return schema; + public String getType() { + return type; } - /** - * set schema. - * - * @param schema schema - */ - public void setSchema(final Schema schema) { - this.schema = schema; + public void setType(final String type) { + this.type = type; + } + + public List getRefs() { + return refs; + } + + public void setRefs(final List refs) { + this.refs = refs; } } public static class Schema { + private String name; + private String type; private String format; + private List refs; + public Schema(final String type, final String format) { this.type = type; this.format = format; } - /** - * get type. - * - * @return type - */ + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + public String getType() { return type; } - /** - * set type. - * - * @param type type - */ public void setType(final String type) { this.type = type; } - /** - * get format. - * - * @return format - */ public String getFormat() { return format; } - /** - * set format. - * - * @param format format - */ public void setFormat(final String format) { this.format = format; } + + public List getRefs() { + return refs; + } + + public void setRefs(final List refs) { + this.refs = refs; + } } } diff --git a/shenyu-client/shenyu-client-core/src/test/java/org/apache/shenyu/client/core/register/registrar/ApiDocRegistrarImplTest.java b/shenyu-client/shenyu-client-core/src/test/java/org/apache/shenyu/client/core/register/registrar/ApiDocRegistrarImplTest.java new file mode 100644 index 000000000000..d3b3857de000 --- /dev/null +++ b/shenyu-client/shenyu-client-core/src/test/java/org/apache/shenyu/client/core/register/registrar/ApiDocRegistrarImplTest.java @@ -0,0 +1,263 @@ +/* + * 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.shenyu.client.core.register.registrar; + +import org.apache.shenyu.client.core.register.ApiBean; +import org.apache.shenyu.client.core.register.ClientRegisterConfig; +import org.apache.shenyu.common.enums.RpcTypeEnum; +import org.apache.shenyu.common.utils.GsonUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class ApiDocRegistrarImplTest { + + private ApiDocRegistrarImpl dubboRegistrar; + + private ApiDocRegistrarImpl httpRegistrar; + + private ApiDocRegistrarImpl grpcRegistrar; + + @BeforeEach + public void init() { + dubboRegistrar = new ApiDocRegistrarImpl(new DubboClientRegisterConfig()); + httpRegistrar = new ApiDocRegistrarImpl(new HttpClientRegisterConfig()); + grpcRegistrar = new ApiDocRegistrarImpl(new GrpcClientRegisterConfig()); + } + + @Test + void testGetDocumentForRpcType() throws Exception { + ApiBean apiBean = new ApiBean(RpcTypeEnum.DUBBO.getName(), + TestDubboService.class.getName(), + TestDubboService.class.getDeclaredConstructor().newInstance(), + "dubboTestService"); + + apiBean.addApiDefinition(TestDubboService.class.getMethod("findById", String.class), "/findById"); + + String document = invokeGetDocument(dubboRegistrar, apiBean.getApiDefinitions().get(0)); + + // RPC document should use requestParameters/responseParameters (not parameters/responseType) + Map docMap = GsonUtils.getInstance().toObjectMap(document); + assertThat("RPC document should contain requestParameters", docMap.containsKey("requestParameters"), is(true)); + assertThat("RPC document should contain responseParameters", docMap.containsKey("responseParameters"), is(true)); + assertThat("RPC document should NOT contain parameters (old field)", docMap.containsKey("parameters"), is(false)); + assertThat("RPC document should NOT contain responseType (old field)", docMap.containsKey("responseType"), is(false)); + } + + @Test + void testGetDocumentForHttpType() throws Exception { + ApiBean apiBean = new ApiBean(RpcTypeEnum.HTTP.getName(), + TestHttpService.class.getName(), + TestHttpService.class.getDeclaredConstructor().newInstance(), + "httpTestService"); + + apiBean.addApiDefinition(TestHttpService.class.getMethod("findById", String.class), "/findById"); + + String document = invokeGetDocument(httpRegistrar, apiBean.getApiDefinitions().get(0)); + + // HTTP document should also use requestParameters/responseParameters + Map docMap = GsonUtils.getInstance().toObjectMap(document); + assertThat("HTTP document should contain requestParameters", docMap.containsKey("requestParameters"), is(true)); + assertThat("HTTP document should contain responseParameters", docMap.containsKey("responseParameters"), is(true)); + assertThat("HTTP document should NOT contain parameters (old field)", docMap.containsKey("parameters"), is(false)); + assertThat("HTTP document should NOT contain responseType (old field)", docMap.containsKey("responseType"), is(false)); + } + + @Test + void testGetDocumentForGrpcType() throws Exception { + ApiBean apiBean = new ApiBean(RpcTypeEnum.GRPC.getName(), + TestGrpcService.class.getName(), + TestGrpcService.class.getDeclaredConstructor().newInstance(), + "grpcTestService"); + + apiBean.addApiDefinition(TestGrpcService.class.getMethod("findById", String.class), "/findById"); + + String document = invokeGetDocument(grpcRegistrar, apiBean.getApiDefinitions().get(0)); + + // gRPC document should use requestParameters/responseParameters + Map docMap = GsonUtils.getInstance().toObjectMap(document); + assertThat("gRPC document should contain requestParameters", docMap.containsKey("requestParameters"), is(true)); + assertThat("gRPC document should contain responseParameters", docMap.containsKey("responseParameters"), is(true)); + assertThat("gRPC document should NOT contain parameters (old field)", docMap.containsKey("parameters"), is(false)); + assertThat("gRPC document should NOT contain responseType (old field)", docMap.containsKey("responseType"), is(false)); + } + + @Test + void testGetDocumentWithCustomDocument() throws Exception { + ApiBean apiBean = new ApiBean(RpcTypeEnum.DUBBO.getName(), + TestDubboService.class.getName(), + TestDubboService.class.getDeclaredConstructor().newInstance(), + "dubboTestService"); + + apiBean.addApiDefinition(TestDubboService.class.getMethod("findById", String.class), "/findById"); + + // Set custom document via properties + String customDoc = "{\"tags\":[\"custom\"],\"operationId\":\"/custom\"}"; + apiBean.getApiDefinitions().get(0).addProperties("document", customDoc); + + String document = invokeGetDocument(dubboRegistrar, apiBean.getApiDefinitions().get(0)); + + // Should return the custom document as-is + Map docMap = GsonUtils.getInstance().toObjectMap(document); + assertThat("Should use custom document", docMap.containsKey("operationId"), is(true)); + assertThat(docMap.get("operationId"), is("/custom")); + } + + @SuppressWarnings("unchecked") + private String invokeGetDocument(final ApiDocRegistrarImpl registrar, final ApiBean.ApiDefinition api) throws Exception { + Method getDocumentMethod = ApiDocRegistrarImpl.class.getDeclaredMethod("getDocument", ApiBean.ApiDefinition.class); + getDocumentMethod.setAccessible(true); + return (String) getDocumentMethod.invoke(registrar, api); + } + + // --- Inner types (must be after all methods per checkstyle InnerTypeLast) --- + + public static class TestDubboService { + public Object findById(final String id) { + return null; + } + } + + public static class TestHttpService { + public Object findById(final String id) { + return null; + } + } + + public static class TestGrpcService { + public Object findById(final String id) { + return null; + } + } + + static class DubboClientRegisterConfig implements ClientRegisterConfig { + @Override + public Integer getPort() { + return 20880; + } + + @Override + public String getHost() { + return "127.0.0.1"; + } + + @Override + public String getAppName() { + return "test-dubbo"; + } + + @Override + public String getContextPath() { + return "/dubbo"; + } + + @Override + public String getIpAndPort() { + return "127.0.0.1:20880"; + } + + @Override + public Boolean getAddPrefixed() { + return false; + } + + @Override + public RpcTypeEnum getRpcTypeEnum() { + return RpcTypeEnum.DUBBO; + } + } + + static class HttpClientRegisterConfig implements ClientRegisterConfig { + @Override + public Integer getPort() { + return 8080; + } + + @Override + public String getHost() { + return "127.0.0.1"; + } + + @Override + public String getAppName() { + return "test-http"; + } + + @Override + public String getContextPath() { + return "/http"; + } + + @Override + public String getIpAndPort() { + return "127.0.0.1:8080"; + } + + @Override + public Boolean getAddPrefixed() { + return false; + } + + @Override + public RpcTypeEnum getRpcTypeEnum() { + return RpcTypeEnum.HTTP; + } + } + + static class GrpcClientRegisterConfig implements ClientRegisterConfig { + @Override + public Integer getPort() { + return 9090; + } + + @Override + public String getHost() { + return "127.0.0.1"; + } + + @Override + public String getAppName() { + return "test-grpc"; + } + + @Override + public String getContextPath() { + return "/grpc"; + } + + @Override + public String getIpAndPort() { + return "127.0.0.1:9090"; + } + + @Override + public Boolean getAddPrefixed() { + return false; + } + + @Override + public RpcTypeEnum getRpcTypeEnum() { + return RpcTypeEnum.GRPC; + } + } +} diff --git a/shenyu-client/shenyu-client-core/src/test/java/org/apache/shenyu/client/core/register/registrar/NoHttpApiDocRegistrarTest.java b/shenyu-client/shenyu-client-core/src/test/java/org/apache/shenyu/client/core/register/registrar/NoHttpApiDocRegistrarTest.java index 835c57a6357f..e9760c6dc938 100644 --- a/shenyu-client/shenyu-client-core/src/test/java/org/apache/shenyu/client/core/register/registrar/NoHttpApiDocRegistrarTest.java +++ b/shenyu-client/shenyu-client-core/src/test/java/org/apache/shenyu/client/core/register/registrar/NoHttpApiDocRegistrarTest.java @@ -20,41 +20,295 @@ import org.apache.shenyu.client.apidocs.annotations.ApiDoc; import org.apache.shenyu.client.apidocs.annotations.ApiModule; import org.apache.shenyu.client.core.constant.ShenyuClientConstants; +import org.apache.shenyu.client.core.disruptor.ShenyuClientRegisterEventPublisher; import org.apache.shenyu.client.core.register.ApiBean; +import org.apache.shenyu.client.core.register.ClientRegisterConfig; import org.apache.shenyu.common.enums.ApiHttpMethodEnum; import org.apache.shenyu.common.enums.RpcTypeEnum; +import org.apache.shenyu.register.client.api.ShenyuClientRegisterRepository; +import org.apache.shenyu.register.common.dto.ApiDocRegisterDTO; +import org.apache.shenyu.register.common.type.DataTypeParent; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; public class NoHttpApiDocRegistrarTest { - private final NoHttpApiDocRegistrar noHttpApiDocRegistrar = - new NoHttpApiDocRegistrar(null, new TestClientRegisterConfig()); - + + private TestShenyuClientRegisterEventPublisher testPublisher; + + private NoHttpApiDocRegistrar noHttpApiDocRegistrar; + + private NoHttpApiDocRegistrar grpcApiDocRegistrar; + + @BeforeEach + public void init() { + testPublisher = new TestShenyuClientRegisterEventPublisher(); + noHttpApiDocRegistrar = new NoHttpApiDocRegistrar(testPublisher, new DubboTestClientRegisterConfig()); + grpcApiDocRegistrar = new NoHttpApiDocRegistrar(testPublisher, new GrpcTestClientRegisterConfig()); + } + @Test public void testDoParse() { final TestApiBeanAnnotatedClassAndMethod bean = new TestApiBeanAnnotatedClassAndMethod(); - - ApiBean apiBean = new ApiBean(RpcTypeEnum.HTTP.getName(), "bean", bean); - + + ApiBean apiBean = new ApiBean(RpcTypeEnum.DUBBO.getName(), "bean", bean); + apiBean.addApiDefinition(null, null); - + AbstractApiDocRegistrar.HttpApiSpecificInfo httpApiSpecificInfo = noHttpApiDocRegistrar.doParse(apiBean.getApiDefinitions().get(0)); - + assertThat(httpApiSpecificInfo.getApiHttpMethodEnums().get(0), is(ApiHttpMethodEnum.NOT_HTTP)); - + assertThat(httpApiSpecificInfo.getConsume(), is(ShenyuClientConstants.MEDIA_TYPE_ALL_VALUE)); - + assertThat(httpApiSpecificInfo.getProduce(), is(ShenyuClientConstants.MEDIA_TYPE_ALL_VALUE)); } - + + @Test + public void testRpcDocumentGenerationWithParameters() throws Exception { + ApiBean apiBean = new ApiBean(RpcTypeEnum.DUBBO.getName(), + DubboTestServiceImpl.class.getName(), + DubboTestServiceImpl.class.getDeclaredConstructor().newInstance(), + "dubboTestService"); + + apiBean.addApiDefinition(DubboTestServiceImpl.class.getMethod("findById", String.class), "/findById"); + + noHttpApiDocRegistrar.register(apiBean); + + ApiDocRegisterDTO dto = testPublisher.metaData; + assertThat(dto, notNullValue()); + + // Verify document contains requestParameters (ResponseType format for RPC) + assertThat(dto.getDocument(), notNullValue()); + assertThat("Document should contain requestParameters field", dto.getDocument().contains("\"requestParameters\""), is(true)); + assertThat("Parameters should have string type", dto.getDocument().contains("\"type\":\"string\""), is(true)); + } + + @Test + public void testRpcDocumentGenerationWithComplexParameter() throws Exception { + ApiBean apiBean = new ApiBean(RpcTypeEnum.DUBBO.getName(), + DubboTestServiceImpl.class.getName(), + DubboTestServiceImpl.class.getDeclaredConstructor().newInstance(), + "dubboTestService"); + + apiBean.addApiDefinition(DubboTestServiceImpl.class.getMethod("insert", DubboTest.class), "/insert"); + + noHttpApiDocRegistrar.register(apiBean); + + ApiDocRegisterDTO dto = testPublisher.metaData; + assertThat(dto, notNullValue()); + + // Verify document contains object-type parameter with nested refs + assertThat(dto.getDocument(), notNullValue()); + assertThat("Document should contain requestParameters with object type", + dto.getDocument().contains("\"type\":\"object\""), is(true)); + assertThat("Complex parameter should have nested refs", + dto.getDocument().contains("\"refs\""), is(true)); + } + + @Test + public void testRpcDocumentGenerationResponseType() throws Exception { + ApiBean apiBean = new ApiBean(RpcTypeEnum.DUBBO.getName(), + DubboTestServiceImpl.class.getName(), + DubboTestServiceImpl.class.getDeclaredConstructor().newInstance(), + "dubboTestService"); + + apiBean.addApiDefinition(DubboTestServiceImpl.class.getMethod("findById", String.class), "/findById"); + + noHttpApiDocRegistrar.register(apiBean); + + ApiDocRegisterDTO dto = testPublisher.metaData; + assertThat(dto, notNullValue()); + + // Verify responseParameters contains object with id and name fields + assertThat(dto.getDocument(), notNullValue()); + assertThat("responseParameters should contain object type", + dto.getDocument().contains("\"responseParameters\""), is(true)); + } + + @Test + public void testGrpcDocumentGenerationWithParameters() throws Exception { + ApiBean apiBean = new ApiBean(RpcTypeEnum.GRPC.getName(), + GrpcTestServiceImpl.class.getName(), + GrpcTestServiceImpl.class.getDeclaredConstructor().newInstance(), + "grpcTestService"); + + apiBean.addApiDefinition(GrpcTestServiceImpl.class.getMethod("unaryCall", String.class, io.grpc.stub.StreamObserver.class), "/unaryCall"); + + grpcApiDocRegistrar.register(apiBean); + + ApiDocRegisterDTO dto = testPublisher.metaData; + assertThat(dto, notNullValue()); + assertThat(dto.getDocument(), notNullValue()); + assertThat("gRPC document should contain requestParameters field", dto.getDocument().contains("\"requestParameters\""), is(true)); + assertThat("gRPC document should contain responseParameters field", dto.getDocument().contains("\"responseParameters\""), is(true)); + } + + @Test + public void testGrpcDocumentGenerationWithVoidReturn() throws Exception { + ApiBean apiBean = new ApiBean(RpcTypeEnum.GRPC.getName(), + GrpcTestServiceImpl.class.getName(), + GrpcTestServiceImpl.class.getDeclaredConstructor().newInstance(), + "grpcTestService"); + + apiBean.addApiDefinition(GrpcTestServiceImpl.class.getMethod("noStreamObserver", String.class), "/noStream"); + + grpcApiDocRegistrar.register(apiBean); + + ApiDocRegisterDTO dto = testPublisher.metaData; + assertThat(dto, notNullValue()); + assertThat(dto.getDocument(), notNullValue()); + assertThat("gRPC document should contain requestParameters field", dto.getDocument().contains("\"requestParameters\""), is(true)); + assertThat("gRPC document should contain responseParameters field", dto.getDocument().contains("\"responseParameters\""), is(true)); + } + + // --- Inner types (must be after all methods per checkstyle InnerTypeLast) --- + + public static class DubboTest { + + private String id; + + private String name; + + public DubboTest() { + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + } + + @ApiModule("dubboTestService") + public static class DubboTestServiceImpl { + + @ApiDoc(desc = "findById") + public DubboTest findById(final String id) { + return null; + } + + @ApiDoc(desc = "insert") + public DubboTest insert(final DubboTest dubboTest) { + return null; + } + } + + @ApiModule("grpcTestService") + public static class GrpcTestServiceImpl { + + @ApiDoc(desc = "unaryCall") + public void unaryCall(final String request, final io.grpc.stub.StreamObserver responseObserver) { + } + + @ApiDoc(desc = "noStreamObserver") + public String noStreamObserver(final String request) { + return null; + } + } + @ApiModule("testClass") static class TestApiBeanAnnotatedClassAndMethod { + @ApiDoc(desc = "testMethod") public String testMethod() { return ""; } } + + static class DubboTestClientRegisterConfig implements ClientRegisterConfig { + + @Override + public Integer getPort() { + return 20880; + } + + @Override + public String getHost() { + return "127.0.0.1"; + } + + @Override + public String getAppName() { + return "test-dubbo"; + } + + @Override + public String getContextPath() { + return "/dubbo"; + } + + @Override + public String getIpAndPort() { + return "127.0.0.1:20880"; + } + + @Override + public Boolean getAddPrefixed() { + return false; + } + + @Override + public RpcTypeEnum getRpcTypeEnum() { + return RpcTypeEnum.DUBBO; + } + } + + static class GrpcTestClientRegisterConfig implements ClientRegisterConfig { + + @Override + public Integer getPort() { + return 9090; + } + + @Override + public String getHost() { + return "127.0.0.1"; + } + + @Override + public String getAppName() { + return "test-grpc"; + } + + @Override + public String getContextPath() { + return "/grpc"; + } + + @Override + public String getIpAndPort() { + return "127.0.0.1:9090"; + } + + @Override + public Boolean getAddPrefixed() { + return false; + } + + @Override + public RpcTypeEnum getRpcTypeEnum() { + return RpcTypeEnum.GRPC; + } + } + + static class TestShenyuClientRegisterEventPublisher extends ShenyuClientRegisterEventPublisher { + + private ApiDocRegisterDTO metaData; + + @Override + public void start(final ShenyuClientRegisterRepository shenyuClientRegisterRepository) { + } + + @Override + public void publishEvent(final DataTypeParent data) { + this.metaData = (ApiDocRegisterDTO) data; + } + } } diff --git a/shenyu-client/shenyu-client-core/src/test/java/org/apache/shenyu/client/core/utils/OpenApiUtilsTest.java b/shenyu-client/shenyu-client-core/src/test/java/org/apache/shenyu/client/core/utils/OpenApiUtilsTest.java new file mode 100644 index 000000000000..350969927fe2 --- /dev/null +++ b/shenyu-client/shenyu-client-core/src/test/java/org/apache/shenyu/client/core/utils/OpenApiUtilsTest.java @@ -0,0 +1,519 @@ +/* + * 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.shenyu.client.core.utils; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.protobuf.Empty; +import org.apache.shenyu.client.core.test.Test.Address; +import org.apache.shenyu.client.core.test.Test.TestRequest; +import org.apache.shenyu.client.core.utils.OpenApiUtils.Parameter; +import org.apache.shenyu.client.core.utils.OpenApiUtils.ResponseType; +import org.apache.shenyu.common.enums.RpcTypeEnum; +import org.junit.jupiter.api.Test; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class OpenApiUtilsTest { + + @Test + void testGenerateRpcDocumentResponse() throws Exception { + Method method = DubboTestService.class.getMethod("findById", String.class); + Map response = OpenApiUtils.generateRpcDocumentResponse("/dubbo/findById", method); + + assertThat(response.containsKey("200"), is(true)); + assertThat(response.containsKey("404"), is(true)); + assertThat(response.containsKey("409"), is(false)); + } + + @Test + void testGenerateRpcDocumentResponseVoidReturn() throws Exception { + Method method = RpcComplexParamService.class.getMethod("deleteById", String.class); + Map response = OpenApiUtils.generateRpcDocumentResponse("/rpc/deleteById", method); + assertThat(response.containsKey("200"), is(true)); + assertThat(response.containsKey("404"), is(true)); + assertThat(response.containsKey("409"), is(false)); + Map successResponse = (Map) response.get("200"); + Map content = (Map) successResponse.get("content"); + Map schema = (Map) ((Map) content.get("*/*")).get("schema"); + assertThat(schema.get("type"), is("void")); + } + + @Test + void testParseReturnTypeObject() throws Exception { + Method method = DubboTestService.class.getMethod("findById", String.class); + ResponseType returnType = OpenApiUtils.parseReturnType(method); + + assertThat(returnType.getType(), is("object")); + assertThat(returnType.getRefs(), notNullValue()); + assertThat(returnType.getRefs(), hasSize(2)); + assertThat(returnType.getRefs().get(0).getName(), is("id")); + assertThat(returnType.getRefs().get(0).getType(), is("string")); + assertThat(returnType.getRefs().get(1).getName(), is("name")); + assertThat(returnType.getRefs().get(1).getType(), is("string")); + } + + @Test + void testParseReturnTypeList() throws Exception { + Method method = DubboTestService.class.getMethod("findAll"); + ResponseType returnType = OpenApiUtils.parseReturnType(method); + + assertThat(returnType.getType(), is("array")); + assertThat(returnType.getRefs(), notNullValue()); + assertThat(returnType.getRefs(), hasSize(1)); + assertThat(returnType.getRefs().get(0).getType(), is("object")); + } + + @Test + void testIsProtobufMessageNegative() { + assertThat(OpenApiUtils.isProtobufMessage(DubboTest.class), is(false)); + assertThat(OpenApiUtils.isProtobufMessage(String.class), is(false)); + assertThat(OpenApiUtils.isProtobufMessage(null), is(false)); + assertThat(OpenApiUtils.isProtobufMessage(int.class), is(false)); + } + + @Test + void testIsProtobufMessagePositive() { + assertThat(OpenApiUtils.isProtobufMessage(TestRequest.class), is(true)); + assertThat(OpenApiUtils.isProtobufMessage(Address.class), is(true)); + assertThat(OpenApiUtils.isProtobufMessage(Empty.class), is(true)); + } + + @Test + void testParseReturnTypeProtobufWithAllFieldTypes() throws Exception { + Method method = ProtobufTestService.class.getMethod("getTestRequest"); + ResponseType returnType = OpenApiUtils.parseReturnType(method); + assertThat(returnType.getType(), is("object")); + assertThat(returnType.getRefs(), notNullValue()); + assertThat(returnType.getRefs(), hasSize(7)); + + assertThat(returnType.getRefs().get(0).getName(), is("id")); + assertThat(returnType.getRefs().get(0).getType(), is("string")); + + assertThat(returnType.getRefs().get(1).getName(), is("count")); + assertThat(returnType.getRefs().get(1).getType(), is("integer")); + + assertThat(returnType.getRefs().get(2).getName(), is("enabled")); + assertThat(returnType.getRefs().get(2).getType(), is("boolean")); + + assertThat(returnType.getRefs().get(3).getName(), is("status")); + assertThat(returnType.getRefs().get(3).getType(), is("string")); + + assertThat(returnType.getRefs().get(4).getName(), is("address")); + assertThat(returnType.getRefs().get(4).getType(), is("object")); + assertThat(returnType.getRefs().get(4).getRefs(), notNullValue()); + assertThat(returnType.getRefs().get(4).getRefs(), hasSize(2)); + assertThat(returnType.getRefs().get(4).getRefs().get(0).getName(), is("street")); + assertThat(returnType.getRefs().get(4).getRefs().get(0).getType(), is("string")); + assertThat(returnType.getRefs().get(4).getRefs().get(1).getName(), is("city")); + assertThat(returnType.getRefs().get(4).getRefs().get(1).getType(), is("string")); + + assertThat(returnType.getRefs().get(5).getName(), is("tags")); + assertThat(returnType.getRefs().get(5).getType(), is("array")); + assertThat(returnType.getRefs().get(5).getRefs(), notNullValue()); + assertThat(returnType.getRefs().get(5).getRefs(), hasSize(1)); + assertThat(returnType.getRefs().get(5).getRefs().get(0).getType(), is("string")); + + assertThat(returnType.getRefs().get(6).getName(), is("addresses")); + assertThat(returnType.getRefs().get(6).getType(), is("array")); + assertThat(returnType.getRefs().get(6).getRefs(), notNullValue()); + assertThat(returnType.getRefs().get(6).getRefs(), hasSize(1)); + assertThat(returnType.getRefs().get(6).getRefs().get(0).getType(), is("object")); + } + + @Test + void testParseReturnTypeProtobufEmpty() throws Exception { + Method method = ProtobufTestService.class.getMethod("getEmpty"); + ResponseType returnType = OpenApiUtils.parseReturnType(method); + assertThat(returnType.getType(), is("object")); + assertThat(returnType.getRefs(), nullValue()); + } + + @Test + void testGenerateRpcRequestDocParametersProtobuf() throws Exception { + Method method = ProtobufTestService.class.getMethod("sendTestRequest", TestRequest.class); + List params = OpenApiUtils.generateRpcRequestDocParameters(method); + assertThat(params, hasSize(1)); + assertThat(params.get(0).getName(), is("request")); + assertThat(params.get(0).getType(), is("object")); + assertThat(params.get(0).getRefs(), notNullValue()); + assertThat(params.get(0).getRefs(), hasSize(7)); + assertThat(params.get(0).getRefs().get(0).getName(), is("id")); + assertThat(params.get(0).getRefs().get(0).getType(), is("string")); + assertThat(params.get(0).getRefs().get(3).getName(), is("status")); + assertThat(params.get(0).getRefs().get(3).getType(), is("string")); + assertThat(params.get(0).getRefs().get(5).getName(), is("tags")); + assertThat(params.get(0).getRefs().get(5).getType(), is("array")); + } + + @Test + void testGenerateDocumentResponseExistingBehavior() { + Map response = OpenApiUtils.generateDocumentResponse("/test/path"); + assertThat(response.containsKey("200"), is(true)); + assertThat(response.containsKey("404"), is(true)); + assertThat(response.containsKey("409"), is(true)); + } + + @Test + void testGenerateRequestDocParametersWithRequestParam() throws Exception { + Method method = SpringMvcController.class.getMethod("query", String.class); + List params = OpenApiUtils.generateRequestDocParameters("/test/query", method); + assertThat(params, hasSize(1)); + assertThat(params.get(0).getName(), is("name")); + assertThat(params.get(0).getType(), is("string")); + assertThat(params.get(0).isRequired(), is(true)); + } + + @Test + void testGenerateRequestDocParametersWithPathVariable() throws Exception { + Method method = SpringMvcController.class.getMethod("getByPath", String.class); + List params = OpenApiUtils.generateRequestDocParameters("/test/{id}", method); + assertThat(params, hasSize(1)); + assertThat(params.get(0).getName(), is("id")); + assertThat(params.get(0).getType(), is("string")); + assertThat(params.get(0).isRequired(), is(true)); + } + + @Test + void testGenerateRequestDocParametersWithQueryAndPath() throws Exception { + Method method = SpringMvcController.class.getMethod("getByPathWithQuery", String.class, String.class); + List params = OpenApiUtils.generateRequestDocParameters("/test/{id}/detail", method); + assertThat(params, hasSize(2)); + assertThat(params.get(0).getName(), is("name")); + assertThat(params.get(0).getIn(), is("query")); + assertThat(params.get(1).getName(), is("id")); + assertThat(params.get(1).getIn(), is("path")); + } + + @Test + void testGenerateRequestDocParametersNoAnnotations() throws Exception { + Method method = DubboTestService.class.getMethod("findById", String.class); + List params = OpenApiUtils.generateRequestDocParameters("/dubbo/findById", method); + assertThat(params, hasSize(0)); + } + + @Test + void testGenerateRpcRequestDocParametersSimpleType() throws Exception { + Method method = DubboTestService.class.getMethod("findById", String.class); + List params = OpenApiUtils.generateRpcRequestDocParameters(method); + assertThat(params, hasSize(1)); + assertThat(params.get(0).getName(), is("id")); + assertThat(params.get(0).getType(), is("string")); + assertThat(params.get(0).isRequired(), is(true)); + } + + @Test + void testGenerateRpcRequestDocParametersComplexType() throws Exception { + Method method = DubboTestService.class.getMethod("insert", DubboTest.class); + List params = OpenApiUtils.generateRpcRequestDocParameters(method); + assertThat(params, hasSize(1)); + assertThat(params.get(0).getType(), is("object")); + assertThat(params.get(0).getRefs(), notNullValue()); + assertThat(params.get(0).getRefs(), hasSize(2)); + assertThat(params.get(0).isRequired(), is(true)); + } + + @Test + void testGenerateRpcRequestDocParametersWithListParameter() throws Exception { + Method method = RpcComplexParamService.class.getMethod("batchInsert", List.class); + List params = OpenApiUtils.generateRpcRequestDocParameters(method); + assertThat(params, hasSize(1)); + assertThat(params.get(0).getName(), is("ids")); + assertThat(params.get(0).getType(), is("array")); + } + + @Test + void testGenerateRpcRequestDocParametersWithMapParameter() throws Exception { + Method method = RpcComplexParamService.class.getMethod("searchByMap", Map.class); + List params = OpenApiUtils.generateRpcRequestDocParameters(method); + assertThat(params, hasSize(1)); + assertThat(params.get(0).getName(), is("params")); + assertThat(params.get(0).getType(), is("object")); + } + + @Test + void testUseSpringMvcParamParsing() { + assertThat(OpenApiUtils.useSpringMvcParamParsing(RpcTypeEnum.HTTP), is(true)); + assertThat(OpenApiUtils.useSpringMvcParamParsing(RpcTypeEnum.WEB_SOCKET), is(true)); + assertThat(OpenApiUtils.useSpringMvcParamParsing(RpcTypeEnum.SPRING_CLOUD), is(true)); + assertThat(OpenApiUtils.useSpringMvcParamParsing(RpcTypeEnum.DUBBO), is(false)); + assertThat(OpenApiUtils.useSpringMvcParamParsing(RpcTypeEnum.GRPC), is(false)); + assertThat(OpenApiUtils.useSpringMvcParamParsing(RpcTypeEnum.SOFA), is(false)); + } + + // --- gRPC tests --- + + @Test + void testGenerateGrpcRequestDocParametersSimpleType() throws Exception { + Method method = GrpcTestService.class.getMethod("unaryCall", String.class, io.grpc.stub.StreamObserver.class); + List params = OpenApiUtils.generateGrpcRequestDocParameters(method); + assertThat(params, hasSize(1)); + assertThat(params.get(0).getName(), is("request")); + assertThat(params.get(0).getType(), is("string")); + assertThat(params.get(0).isRequired(), is(true)); + } + + @Test + void testGenerateGrpcRequestDocParametersComplexType() throws Exception { + Method method = GrpcTestService.class.getMethod("unaryCallComplex", GrpcTestClass.class, io.grpc.stub.StreamObserver.class); + List params = OpenApiUtils.generateGrpcRequestDocParameters(method); + assertThat(params, hasSize(1)); + assertThat(params.get(0).getName(), is("request")); + assertThat(params.get(0).getType(), is("object")); + assertThat(params.get(0).getRefs(), notNullValue()); + assertThat(params.get(0).isRequired(), is(true)); + } + + @Test + void testGenerateGrpcRequestDocParametersClientStreaming() throws Exception { + Method method = GrpcClientStreamingService.class.getMethod("clientStreaming", io.grpc.stub.StreamObserver.class); + List params = OpenApiUtils.generateGrpcRequestDocParameters(method); + assertThat(params, hasSize(1)); + assertThat(params.get(0).getName(), is("request")); + assertThat(params.get(0).getType(), is("string")); + assertThat(params.get(0).isRequired(), is(true)); + } + + @Test + void testParseGrpcReturnTypeWithStreamObserver() throws Exception { + Method method = GrpcTestService.class.getMethod("unaryCall", String.class, io.grpc.stub.StreamObserver.class); + ResponseType returnType = OpenApiUtils.parseGrpcReturnType(method); + assertThat(returnType.getName(), is("ROOT")); + assertThat(returnType.getType(), is("object")); + assertThat(returnType.getRefs(), notNullValue()); + assertThat(returnType.getRefs(), hasSize(2)); + } + + @Test + void testParseGrpcReturnTypeVoid() throws Exception { + Method method = GrpcTestService.class.getMethod("noStreamObserver", String.class); + ResponseType returnType = OpenApiUtils.parseGrpcReturnType(method); + assertThat(returnType.getName(), is("ROOT")); + assertThat(returnType.getType(), is("void")); + } + + @Test + void testParseGrpcReturnTypeClientStreaming() throws Exception { + Method method = GrpcClientStreamingService.class.getMethod("clientStreaming", io.grpc.stub.StreamObserver.class); + ResponseType returnType = OpenApiUtils.parseGrpcReturnType(method); + assertThat(returnType.getName(), is("ROOT")); + assertThat(returnType.getType(), is("object")); + assertThat(returnType.getRefs(), notNullValue()); + assertThat(returnType.getRefs(), hasSize(2)); + } + + @Test + void testGenerateGrpcDocumentResponseWithStreamObserver() throws Exception { + Method method = GrpcTestService.class.getMethod("unaryCall", String.class, io.grpc.stub.StreamObserver.class); + Map response = OpenApiUtils.generateGrpcDocumentResponse("/grpc/unaryCall", method); + assertThat(response.containsKey("200"), is(true)); + assertThat(response.containsKey("404"), is(true)); + assertThat(response.containsKey("409"), is(false)); + } + + @Test + void testGenerateGrpcDocumentResponseVoid() throws Exception { + Method method = GrpcTestService.class.getMethod("noStreamObserver", String.class); + Map response = OpenApiUtils.generateGrpcDocumentResponse("/grpc/noStream", method); + assertThat(response.containsKey("200"), is(true)); + assertThat(response.containsKey("404"), is(true)); + } + + // --- buildDocumentJson dispatch tests --- + + @Test + void testBuildDocumentJsonHttp() throws Exception { + Method method = SpringMvcController.class.getMethod("query", String.class); + String json = OpenApiUtils.buildDocumentJson(Arrays.asList("tag1"), "/test/query", method, RpcTypeEnum.HTTP); + JsonObject doc = JsonParser.parseString(json).getAsJsonObject(); + assertThat(doc.has("requestParameters"), is(true)); + assertThat(doc.has("responseParameters"), is(true)); + assertThat(doc.has("parameters"), is(false)); + assertThat(doc.has("responseType"), is(false)); + JsonObject responses = doc.getAsJsonObject("responses"); + assertThat(responses.has("200"), is(true)); + assertThat(responses.has("404"), is(true)); + assertThat(responses.has("409"), is(true)); + } + + @Test + void testBuildDocumentJsonGrpc() throws Exception { + Method method = GrpcTestService.class.getMethod("unaryCall", String.class, io.grpc.stub.StreamObserver.class); + String json = OpenApiUtils.buildDocumentJson(Arrays.asList("tag1"), "/grpc/unaryCall", method, RpcTypeEnum.GRPC); + JsonObject doc = JsonParser.parseString(json).getAsJsonObject(); + assertThat(doc.has("requestParameters"), is(true)); + assertThat(doc.has("responseParameters"), is(true)); + assertThat(doc.has("parameters"), is(false)); + assertThat(doc.has("responseType"), is(false)); + JsonObject responses = doc.getAsJsonObject("responses"); + assertThat(responses.has("200"), is(true)); + assertThat(responses.has("404"), is(true)); + assertThat(responses.has("409"), is(false)); + } + + @Test + void testBuildDocumentJsonRpc() throws Exception { + Method method = DubboTestService.class.getMethod("findById", String.class); + String json = OpenApiUtils.buildDocumentJson(Arrays.asList("tag1"), "/dubbo/findById", method, RpcTypeEnum.DUBBO); + JsonObject doc = JsonParser.parseString(json).getAsJsonObject(); + assertThat(doc.has("requestParameters"), is(true)); + assertThat(doc.has("responseParameters"), is(true)); + assertThat(doc.has("parameters"), is(false)); + assertThat(doc.has("responseType"), is(false)); + JsonObject responses = doc.getAsJsonObject("responses"); + assertThat(responses.has("200"), is(true)); + assertThat(responses.has("404"), is(true)); + assertThat(responses.has("409"), is(false)); + } + + // --- Inner types (must be after all methods per checkstyle InnerTypeLast) --- + + public static class DubboTest { + + private String id; + + private String name; + + public DubboTest() { + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + } + + public static class DubboTestService { + + public DubboTest findById(final String id) { + return null; + } + + public DubboTest insert(final DubboTest dubboTest) { + return null; + } + + public List findAll() { + return null; + } + } + + @RestController + @RequestMapping("/test") + public static class SpringMvcController { + + @RequestMapping("/query") + public String query(@RequestParam("name") final String name) { + return ""; + } + + @RequestMapping("/{id}") + public String getByPath(@PathVariable("id") final String id) { + return ""; + } + + @RequestMapping("/{id}/detail") + public String getByPathWithQuery(@PathVariable("id") final String id, @RequestParam("name") final String name) { + return ""; + } + } + + public static class GrpcTestClass { + + private String id; + + private String name; + + public GrpcTestClass() { + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + } + + public static class GrpcTestService { + + public void unaryCall(final String request, final io.grpc.stub.StreamObserver responseObserver) { + } + + public void unaryCallComplex(final GrpcTestClass request, final io.grpc.stub.StreamObserver responseObserver) { + } + + public String noStreamObserver(final String request) { + return null; + } + } + + public static class GrpcClientStreamingService { + + public io.grpc.stub.StreamObserver clientStreaming( + final io.grpc.stub.StreamObserver responseObserver) { + return null; + } + } + + public static class ProtobufTestService { + + public TestRequest getTestRequest() { + return null; + } + + public TestRequest sendTestRequest(final TestRequest request) { + return null; + } + + public Empty getEmpty() { + return null; + } + } + + public static class RpcComplexParamService { + + public String batchInsert(final List ids) { + return null; + } + + public String searchByMap(final Map params) { + return null; + } + + public void deleteById(final String id) { + } + } +} diff --git a/shenyu-client/shenyu-client-core/src/test/proto/test.proto b/shenyu-client/shenyu-client-core/src/test/proto/test.proto new file mode 100644 index 000000000000..fc3e0d305d16 --- /dev/null +++ b/shenyu-client/shenyu-client-core/src/test/proto/test.proto @@ -0,0 +1,41 @@ +// 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. + +syntax = "proto3"; + +package org.apache.shenyu.client.core.test; + +option java_package = "org.apache.shenyu.client.core.test"; + +enum Status { + UNKNOWN = 0; + ACTIVE = 1; + INACTIVE = 2; +} + +message Address { + string street = 1; + string city = 2; +} + +message TestRequest { + string id = 1; + int32 count = 2; + bool enabled = 3; + Status status = 4; + Address address = 5; + repeated string tags = 6; + repeated Address addresses = 7; +} diff --git a/shenyu-client/shenyu-client-sofa/src/main/java/org/apache/shenyu/client/sofa/SofaServiceEventListener.java b/shenyu-client/shenyu-client-sofa/src/main/java/org/apache/shenyu/client/sofa/SofaServiceEventListener.java index 167c580da687..bc3d40ac1c6b 100644 --- a/shenyu-client/shenyu-client-sofa/src/main/java/org/apache/shenyu/client/sofa/SofaServiceEventListener.java +++ b/shenyu-client/shenyu-client-sofa/src/main/java/org/apache/shenyu/client/sofa/SofaServiceEventListener.java @@ -37,9 +37,9 @@ import org.slf4j.LoggerFactory; import org.springframework.aop.support.AopUtils; import org.springframework.context.ApplicationContext; -import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import java.lang.annotation.Annotation; @@ -102,25 +102,41 @@ protected Class getAnnotationType() { return ShenyuSofaClient.class; } + @Override + protected void handleClass(final Class clazz, + final ServiceFactoryBean bean, + @NonNull final ShenyuSofaClient beanShenyuClient, + final String superPath) { + List namespaceIds = super.getNamespace(); + Method[] methods = ReflectionUtils.getDeclaredMethods(clazz); + for (String namespaceId : namespaceIds) { + for (Method method : methods) { + final MetaDataRegisterDTO metaData = buildMetaDataDTO(bean, beanShenyuClient, + buildApiPath(method, superPath, null), clazz, method, namespaceId); + getPublisher().publishEvent(metaData); + getMetaDataMap().put(method, metaData); + } + } + } + @Override protected String buildApiPath(final Method method, final String superPath, - @NonNull final ShenyuSofaClient shenyuSofaClient) { + @Nullable final ShenyuSofaClient shenyuSofaClient) { final String contextPath = this.getContextPath(); return superPath.contains("*") ? pathJoin(contextPath, superPath.replace("*", ""), method.getName()) - : pathJoin(contextPath, superPath, shenyuSofaClient.path()); + : pathJoin(contextPath, superPath, Objects.requireNonNull(shenyuSofaClient).path()); } @Override protected MetaDataRegisterDTO buildMetaDataDTO(final ServiceFactoryBean serviceBean, @NonNull final ShenyuSofaClient shenyuSofaClient, - final String superPath, + final String path, final Class clazz, final Method method, final String namespaceId) { String appName = this.getAppName(); String contextPath = this.getContextPath(); - String path = pathJoin(contextPath, superPath, shenyuSofaClient.path()); String serviceName = serviceBean.getInterfaceClass().getName(); String desc = shenyuSofaClient.desc(); String configRuleName = shenyuSofaClient.ruleName(); @@ -157,11 +173,18 @@ protected MetaDataRegisterDTO buildMetaDataDTO(final ServiceFactoryBean serviceB } @Override - public void onApplicationEvent(final ContextRefreshedEvent contextRefreshedEvent) { - Map serviceBean = contextRefreshedEvent.getApplicationContext().getBeansOfType(ServiceFactoryBean.class); - for (Map.Entry entry : serviceBean.entrySet()) { - handler(entry.getValue()); + protected Class getCorrectedClass(final ServiceFactoryBean bean) { + Object targetProxy; + try { + targetProxy = ((Service) Objects.requireNonNull(bean.getObject())).getTarget(); + } catch (Exception e) { + LOG.error("failed to get sofa target class", e); + return bean.getClass(); } + if (AopUtils.isAopProxy(targetProxy)) { + return AopUtils.getTargetClass(targetProxy); + } + return targetProxy.getClass(); } @Override @@ -179,42 +202,6 @@ protected Sextet clazz; - Object targetProxy; - try { - targetProxy = ((Service) Objects.requireNonNull(serviceBean.getObject())).getTarget(); - clazz = targetProxy.getClass(); - } catch (Exception e) { - LOG.error("failed to get sofa target class", e); - return; - } - if (AopUtils.isAopProxy(targetProxy)) { - clazz = AopUtils.getTargetClass(targetProxy); - } - final ShenyuSofaClient beanSofaClient = AnnotatedElementUtils.findMergedAnnotation(clazz, ShenyuSofaClient.class); - final String superPath = buildApiSuperPath(clazz, beanSofaClient); - List namespaceIds = super.getNamespace(); - if (superPath.contains("*") && Objects.nonNull(beanSofaClient)) { - Method[] declaredMethods = ReflectionUtils.getDeclaredMethods(clazz); - for (String namespaceId : namespaceIds) { - for (Method declaredMethod : declaredMethods) { - getPublisher().publishEvent(buildMetaDataDTO(serviceBean, beanSofaClient, superPath, clazz, declaredMethod, namespaceId)); - } - } - return; - } - Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(clazz); - for (String namespaceId : namespaceIds) { - for (Method method : methods) { - ShenyuSofaClient methodSofaClient = AnnotatedElementUtils.findMergedAnnotation(method, ShenyuSofaClient.class); - if (Objects.nonNull(methodSofaClient)) { - getPublisher().publishEvent(buildMetaDataDTO(serviceBean, methodSofaClient, superPath, clazz, method, namespaceId)); - } - } - } - } - private String buildRpcExt(final ShenyuSofaClient shenyuSofaClient) { SofaRpcExt build = SofaRpcExt.builder() .loadbalance(shenyuSofaClient.loadBalance()) diff --git a/shenyu-client/shenyu-client-sofa/src/test/java/org/apache/shenyu/client/sofa/SofaServiceEventListenerTest.java b/shenyu-client/shenyu-client-sofa/src/test/java/org/apache/shenyu/client/sofa/SofaServiceEventListenerTest.java index a38615c987d7..5e0bc07b6ed9 100644 --- a/shenyu-client/shenyu-client-sofa/src/test/java/org/apache/shenyu/client/sofa/SofaServiceEventListenerTest.java +++ b/shenyu-client/shenyu-client-sofa/src/test/java/org/apache/shenyu/client/sofa/SofaServiceEventListenerTest.java @@ -204,11 +204,12 @@ public void testBuildMetaDataDTO() throws NoSuchMethodException { String expectedPath = "/sofa/findByIdsAndName/path"; String expectedRpcExt = "{\"loadbalance\":\"loadBalance\",\"retries\":0,\"timeout\":0}"; + String apiPath = sofaServiceEventListener.buildApiPath(method, SUPER_PATH_NOT_CONTAINS_STAR, shenyuSofaClient); MetaDataRegisterDTO realMetaDataRegisterDTO = sofaServiceEventListener .buildMetaDataDTO( serviceFactoryBean, shenyuSofaClient, - SUPER_PATH_NOT_CONTAINS_STAR, + apiPath, SofaServiceEventListener.class, method, Constants.SYS_DEFAULT_NAMESPACE_ID); MetaDataRegisterDTO expectedMetaDataRegisterDTO = MetaDataRegisterDTO diff --git a/shenyu-examples/shenyu-examples-dubbo/shenyu-examples-apache-dubbo-service-annotation/src/main/java/org/apache/shenyu/examples/apache/dubbo/service/annotation/impl/DubboProtobufServiceImpl.java b/shenyu-examples/shenyu-examples-dubbo/shenyu-examples-apache-dubbo-service-annotation/src/main/java/org/apache/shenyu/examples/apache/dubbo/service/annotation/impl/DubboProtobufServiceImpl.java index e589bd0c5331..8d8a93f345d7 100644 --- a/shenyu-examples/shenyu-examples-dubbo/shenyu-examples-apache-dubbo-service-annotation/src/main/java/org/apache/shenyu/examples/apache/dubbo/service/annotation/impl/DubboProtobufServiceImpl.java +++ b/shenyu-examples/shenyu-examples-dubbo/shenyu-examples-apache-dubbo-service-annotation/src/main/java/org/apache/shenyu/examples/apache/dubbo/service/annotation/impl/DubboProtobufServiceImpl.java @@ -19,27 +19,33 @@ import com.google.protobuf.Empty; import org.apache.dubbo.config.annotation.DubboService; +import org.apache.shenyu.client.apidocs.annotations.ApiDoc; +import org.apache.shenyu.client.apidocs.annotations.ApiModule; import org.apache.shenyu.client.dubbo.common.annotation.ShenyuDubboClient; import org.apache.shenyu.examples.dubbo.api.service.DubboProtobufService; import org.apache.shenyu.examples.dubbo.api.service.DubboTestProtobuf; @DubboService(serialization = "protobuf") @ShenyuDubboClient(value = "/protobufSerialization") +@ApiModule(value = "dubboProtobufService") public class DubboProtobufServiceImpl implements DubboProtobufService { @ShenyuDubboClient("/insert") + @ApiDoc(desc = "insert") @Override public DubboTestProtobuf insert(final DubboTestProtobuf request) { return request; } @ShenyuDubboClient("/update") + @ApiDoc(desc = "update") @Override public Empty update(final DubboTestProtobuf request) { return Empty.getDefaultInstance(); } @ShenyuDubboClient("/findOne") + @ApiDoc(desc = "findOne") @Override public DubboTestProtobuf findOne(final Empty request) { return DubboTestProtobuf.newBuilder().setId("1").setName("test1").build(); diff --git a/shenyu-examples/shenyu-examples-dubbo/shenyu-examples-apache-dubbo-service-xml/src/main/java/org/apache/shenyu/examples/apache/dubbo/service/xml/impl/DubboProtobufServiceImpl.java b/shenyu-examples/shenyu-examples-dubbo/shenyu-examples-apache-dubbo-service-xml/src/main/java/org/apache/shenyu/examples/apache/dubbo/service/xml/impl/DubboProtobufServiceImpl.java index 6084bc2470f0..c8f5135e14e5 100644 --- a/shenyu-examples/shenyu-examples-dubbo/shenyu-examples-apache-dubbo-service-xml/src/main/java/org/apache/shenyu/examples/apache/dubbo/service/xml/impl/DubboProtobufServiceImpl.java +++ b/shenyu-examples/shenyu-examples-dubbo/shenyu-examples-apache-dubbo-service-xml/src/main/java/org/apache/shenyu/examples/apache/dubbo/service/xml/impl/DubboProtobufServiceImpl.java @@ -18,6 +18,8 @@ package org.apache.shenyu.examples.apache.dubbo.service.xml.impl; import com.google.protobuf.Empty; +import org.apache.shenyu.client.apidocs.annotations.ApiDoc; +import org.apache.shenyu.client.apidocs.annotations.ApiModule; import org.apache.shenyu.client.dubbo.common.annotation.ShenyuDubboClient; import org.apache.shenyu.examples.dubbo.api.service.DubboProtobufService; import org.apache.shenyu.examples.dubbo.api.service.DubboTestProtobuf; @@ -25,21 +27,25 @@ @Service("dubboProtobufService") @ShenyuDubboClient(value = "/protobufSerialization") +@ApiModule(value = "dubboProtobufService") public class DubboProtobufServiceImpl implements DubboProtobufService { @ShenyuDubboClient("/insert") + @ApiDoc(desc = "insert") @Override public DubboTestProtobuf insert(final DubboTestProtobuf request) { return request; } @ShenyuDubboClient("/update") + @ApiDoc(desc = "update") @Override public Empty update(final DubboTestProtobuf request) { return Empty.getDefaultInstance(); } @ShenyuDubboClient("/findOne") + @ApiDoc(desc = "findOne") @Override public DubboTestProtobuf findOne(final Empty request) { return DubboTestProtobuf.newBuilder().setId("1").setName("test1").build(); diff --git a/shenyu-examples/shenyu-examples-dubbo/shenyu-examples-apache-dubbo-service/src/main/java/org/apache/shenyu/examples/apache/dubbo/service/impl/DubboProtobufServiceImpl.java b/shenyu-examples/shenyu-examples-dubbo/shenyu-examples-apache-dubbo-service/src/main/java/org/apache/shenyu/examples/apache/dubbo/service/impl/DubboProtobufServiceImpl.java index da96e87072c1..d2efd24320a2 100644 --- a/shenyu-examples/shenyu-examples-dubbo/shenyu-examples-apache-dubbo-service/src/main/java/org/apache/shenyu/examples/apache/dubbo/service/impl/DubboProtobufServiceImpl.java +++ b/shenyu-examples/shenyu-examples-dubbo/shenyu-examples-apache-dubbo-service/src/main/java/org/apache/shenyu/examples/apache/dubbo/service/impl/DubboProtobufServiceImpl.java @@ -18,6 +18,8 @@ package org.apache.shenyu.examples.apache.dubbo.service.impl; import com.google.protobuf.Empty; +import org.apache.shenyu.client.apidocs.annotations.ApiDoc; +import org.apache.shenyu.client.apidocs.annotations.ApiModule; import org.apache.shenyu.client.dubbo.common.annotation.ShenyuDubboClient; import org.apache.shenyu.examples.dubbo.api.service.DubboProtobufService; import org.apache.shenyu.examples.dubbo.api.service.DubboTestProtobuf; @@ -25,21 +27,25 @@ @ShenyuDubboClient(value = "/protobufSerialization") @Service("dubboProtobufService") +@ApiModule(value = "dubboProtobufService") public class DubboProtobufServiceImpl implements DubboProtobufService { @ShenyuDubboClient("/insert") + @ApiDoc(desc = "insert") @Override public DubboTestProtobuf insert(final DubboTestProtobuf request) { return request; } @ShenyuDubboClient("/update") + @ApiDoc(desc = "update") @Override public Empty update(final DubboTestProtobuf request) { return Empty.getDefaultInstance(); } @ShenyuDubboClient("/findOne") + @ApiDoc(desc = "findOne") @Override public DubboTestProtobuf findOne(final Empty request) { return DubboTestProtobuf.newBuilder().setId("1").setName("test1").build();