This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new ca83c6974d Fix RocketMQ logging e2e bootstrap startup (#6379)
ca83c6974d is described below
commit ca83c6974d69e96c63f5a1d42272e9dcc738fd2b
Author: aias00 <[email protected]>
AuthorDate: Fri Jun 12 14:53:29 2026 +0800
Fix RocketMQ logging e2e bootstrap startup (#6379)
* feat(api-doc): support request parameter parsing for RPC types
(Dubbo/SOFA/TARS/gRPC)
* fix license
* fix checkstyle exclude generated source
* fix checkstyle
* fix review
* Fix Protobuf field schema parsing logic
* fix use common version
* chore: trigger CI retest
* Recover RocketMQ e2e bootstrap after delayed admin health
The RocketMQ logging e2e script runs three sync modes in sequence. In the
zookeeper round, Docker Compose can return before shenyu-admin has recovered
from an initial unhealthy state, leaving shenyu-bootstrap created but not
started. The script previously ignored both that compose failure and later
healthcheck failures, so Maven reported the gateway as unavailable instead of
recovering the service stack first.
Constraint: CI builds the required latest Docker images before this script
runs
Rejected: Increase fixed sleeps only | it would hide the dependency failure
without ensuring bootstrap starts
Confidence: medium
Scope-risk: narrow
Directive: Keep bootstrap startup gated on admin health for this e2e script
Tested: bash -n e2e-logging-rocketmq-compose.sh; e2e RocketMQ test-compile;
RocketMQ logging plugin test-compile; docker compose config for zookeeper,
RocketMQ, and HTTP example compose files; git diff --check
Not-tested: Full Docker Compose e2e locally because CI-built
apache/shenyu-admin:latest, apache/shenyu-bootstrap:latest, and
shenyu-examples-http:latest images are not present
---------
Co-authored-by: eye-gu <[email protected]>
---
pom.xml | 4 +-
.../shenyu/admin/service/impl/ApiServiceImpl.java | 14 +-
.../shenyu/admin/service/ApiServiceTest.java | 54 ++
shenyu-client/shenyu-client-core/pom.xml | 50 ++
.../AbstractContextRefreshedEventListener.java | 14 +-
.../registrar/AbstractApiDocRegistrar.java | 12 +-
.../register/registrar/ApiDocRegistrarImpl.java | 13 +-
.../shenyu/client/core/utils/OpenApiUtils.java | 834 ++++++++++++++++++---
.../registrar/ApiDocRegistrarImplTest.java | 263 +++++++
.../registrar/NoHttpApiDocRegistrarTest.java | 276 ++++++-
.../shenyu/client/core/utils/OpenApiUtilsTest.java | 519 +++++++++++++
.../shenyu-client-core/src/test/proto/test.proto | 41 +
.../client/sofa/SofaServiceEventListener.java | 77 +-
.../client/sofa/SofaServiceEventListenerTest.java | 3 +-
.../compose/script/e2e-logging-rocketmq-compose.sh | 12 +-
.../annotation/impl/DubboProtobufServiceImpl.java | 6 +
.../service/xml/impl/DubboProtobufServiceImpl.java | 6 +
.../service/impl/DubboProtobufServiceImpl.java | 6 +
18 files changed, 2002 insertions(+), 202 deletions(-)
diff --git a/pom.xml b/pom.xml
index 7fdbee3836..fb743a5a6e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -103,6 +103,8 @@
<maven-shade-plugin.version>3.5.1</maven-shade-plugin.version>
<docker-maven-plugin.version>0.40.1</docker-maven-plugin.version>
<maven-assembly-plugin.version>3.5.0</maven-assembly-plugin.version>
+ <protobuf-maven-plugin.version>0.6.1</protobuf-maven-plugin.version>
+ <os-maven-plugin.version>1.6.2</os-maven-plugin.version>
<!-- maven plugin version end -->
<!-- dependency version start -->
@@ -735,7 +737,7 @@
<configLocation>/script/shenyu_checkstyle.xml</configLocation>
<headerLocation>/script/checkstyle-header.txt</headerLocation>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
- <excludes>**/transfer/**/*</excludes>
+ <excludes>**/transfer/**/*,**/generated*/**/*</excludes>
</configuration>
<executions>
<execution>
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 979e6d7e1a..ad05dbc788 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 com.google.common.collect.Lists;
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.ApiVO;
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 class ApiServiceImpl implements ApiService {
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 dd7b719e76..7ffc2c87b0 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 final class ApiServiceTest {
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 683afdcc7c..c6e70b6d32 100644
--- a/shenyu-client/shenyu-client-core/pom.xml
+++ b/shenyu-client/shenyu-client-core/pom.xml
@@ -81,5 +81,55 @@
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-java</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-stub</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
+
+ <build>
+ <extensions>
+ <extension>
+ <groupId>kr.motd.maven</groupId>
+ <artifactId>os-maven-plugin</artifactId>
+ <version>${os-maven-plugin.version}</version>
+ </extension>
+ </extensions>
+ <plugins>
+ <plugin>
+ <groupId>org.xolstice.maven.plugins</groupId>
+ <artifactId>protobuf-maven-plugin</artifactId>
+ <version>${protobuf-maven-plugin.version}</version>
+ <extensions>true</extensions>
+ <configuration>
+
<protocArtifact>com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier}</protocArtifact>
+
<protoSourceRoot>${project.basedir}/src/test/proto</protoSourceRoot>
+
<outputDirectory>${project.build.directory}/generated-test-sources/protobuf/java</outputDirectory>
+ <clearOutputDirectory>false</clearOutputDirectory>
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>test-compile</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <configuration>
+ <testSourceDirectories>
+
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
+ </testSourceDirectories>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
</project>
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 32ef07fcda..1aef7389b7 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 @@ public abstract class
AbstractContextRefreshedEventListener<T, A extends Annotat
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 @@ public abstract class
AbstractContextRefreshedEventListener<T, A extends Annotat
return ext;
}
- private String buildDocumentJson(final List<String> tags, final String
path, final Method method) {
- Map<String, Object> documentMap = ImmutableMap.<String,
Object>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<String> tags, final String
path, final Method method, final RpcTypeEnum rpcTypeEnum) {
+ return OpenApiUtils.buildDocumentJson(tags, path, method, rpcTypeEnum);
}
protected abstract Sextet<String[], String, String, ApiHttpMethodEnum[],
RpcTypeEnum, String> buildApiDocSextet(Method method, Annotation annotation,
Map<String, T> 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 6d2e5358b3..30ddb88286 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 org.apache.shenyu.register.common.enums.EventType;
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<ApiDocRegisterDTO> {
@@ -128,14 +125,7 @@ public abstract class AbstractApiDocRegistrar extends
AbstractApiRegistrar<ApiDo
}
private String buildDocumentJson(final List<String> tags, final String
path, final Method method) {
- Map<String, Object> documentMap = ImmutableMap.<String,
Object>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 91b842f97f..10d07877a4 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 org.jetbrains.annotations.NotNull;
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 @@ public class ApiDocRegistrarImpl extends
BaseApiRegistrarImpl {
return document;
}
final String path = getPath(api);
- final Map<String, Object> documentMap = ImmutableMap.<String,
Object>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 5affbce77a..d74ba59b69 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 com.google.common.collect.ImmutableMap;
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.LocalDate;
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<String> tags, final
String path,
+ final Method method, final
RpcTypeEnum rpcTypeEnum) {
+ boolean useSpringMvcParamParsing =
useSpringMvcParamParsing(rpcTypeEnum);
+ Map<String, Object> documentMap;
+ if (useSpringMvcParamParsing) {
+ documentMap = ImmutableMap.<String, Object>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.<String, Object>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.<String, Object>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<Parameter> generateDocumentParameters(final String
path, final Method method) {
- ArrayList<Parameter> list = new ArrayList<>();
+ public static List<Parameter> generateRequestDocParameters(final String
path, final Method method) {
+ List<Parameter> list = new ArrayList<>();
Pair<Boolean, Annotation[][]> query = isQuery(method);
if (query.getLeft()) {
for (Annotation[] annotations : query.getRight()) {
@@ -89,34 +149,182 @@ public class OpenApiUtils {
parameter.setIn("query");
parameter.setRequired(required);
parameter.setName(name);
- parameter.setSchema(new Schema("string", null));
+ parameter.setType("string");
list.add(parameter);
}
}
}
- } else {
- List<String> 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<String> 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<Parameter> generateRpcRequestDocParameters(final Method
method) {
+ List<Parameter> 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<Parameter> generateGrpcRequestDocParameters(final
Method method) {
+ List<Parameter> 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<Parameter> 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<String, Object> 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<Object, Object> contentMap = ImmutableMap.builder()
+ .put(ShenyuClientConstants.MEDIA_TYPE_ALL_VALUE,
ImmutableMap.of("schema", ImmutableMap.of("type", returnTypeStr)))
+ .build();
+ ImmutableMap<Object, Object> successMap = ImmutableMap.builder()
+ .put("description", path)
+ .put("content", contentMap).build();
+ ImmutableMap<Object, Object> 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.<String, Object>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<Boolean, Annotation[][]> isQuery(final Method method) {
@@ -125,7 +333,6 @@ public class OpenApiUtils {
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 @@ public class OpenApiUtils {
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<String, Object> generateRpcDocumentResponse(final String
path, final Method method) {
+ Type returnType = method.getGenericReturnType();
+ String returnTypeStr = "void".equals(returnType.getTypeName()) ?
"void" : resolveTypeName(returnType);
+ ImmutableMap<Object, Object> contentMap = ImmutableMap.builder()
+ .put(ShenyuClientConstants.MEDIA_TYPE_ALL_VALUE,
ImmutableMap.of("schema", ImmutableMap.of("type", returnTypeStr)))
+ .build();
+ ImmutableMap<Object, Object> successMap = ImmutableMap.builder()
+ .put("description", path)
+ .put("content", contentMap).build();
+ ImmutableMap<Object, Object> 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.<String, Object>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<TypeVariable<?>, 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<ResponseType> refs = parseProtobufFields(fields, clazz,
depth, typeVariableMap);
+ responseType.setType("object");
+ responseType.setRefs(refs);
+ } catch (Exception e) {
+ responseType.setType("object");
+ }
+ return responseType;
+ }
+
+ private static List<ResponseType> parseProtobufFields(final List<?>
fields, final Class<?> clazz,
+ final int depth,
final Map<TypeVariable<?>, Type> typeVariableMap) throws Exception {
+ List<ResponseType> 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<TypeVariable<?>,
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<TypeVariable<?>,
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<ResponseType> 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 @@ public class OpenApiUtils {
} else if (isDateType(clazz)) {
responseType.setType("date");
return responseType;
+ } else if (isProtobufMessage(clazz)) {
+ return parseProtobufClass(responseType, clazz, depth,
typeVariableMap);
} else {
List<ResponseType> refs = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
@@ -296,6 +747,228 @@ public class OpenApiUtils {
return responseType;
}
+ private static Schema parseSchema(final Type type, final int depth, final
Map<TypeVariable<?>, 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<TypeVariable<?>, 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<Schema> 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<TypeVariable<?>, Type> typeVariableMap) {
+ Class<?> rawType = (Class<?>) type.getRawType();
+ Type[] actualTypeArguments = type.getActualTypeArguments();
+ TypeVariable<?>[] typeVariables = rawType.getTypeParameters();
+ Map<TypeVariable<?>, 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<Schema> 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<TypeVariable<?>, 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<Schema> 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<Schema> parseProtobufFieldsSchema(final List<?>
fields, final Class<?> clazz,
+ final int depth,
final Map<TypeVariable<?>, Type> typeVariableMap) throws Exception {
+ List<Schema> 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<TypeVariable<?>,
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<TypeVariable<?>, 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<TypeVariable<?>, 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<Schema> 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 class OpenApiUtils {
}
}
-
public static class Parameter {
private String name;
@@ -379,145 +1051,105 @@ public class OpenApiUtils {
private boolean required;
- private Schema schema;
+ private String type;
+
+ private List<Parameter> 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<Parameter> getRefs() {
+ return refs;
+ }
+
+ public void setRefs(final List<Parameter> refs) {
+ this.refs = refs;
}
}
public static class Schema {
+ private String name;
+
private String type;
private String format;
+ private List<Schema> 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<Schema> getRefs() {
+ return refs;
+ }
+
+ public void setRefs(final List<Schema> 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 0000000000..d3b3857de0
--- /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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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 835c57a635..e9760c6dc9 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 @@ package org.apache.shenyu.client.core.register.registrar;
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<DubboTest> 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 0000000000..350969927f
--- /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<String, Object> 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<String, Object> 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<String, Object> successResponse = (Map<String, Object>)
response.get("200");
+ Map<String, Object> content = (Map<String, Object>)
successResponse.get("content");
+ Map<String, Object> schema = (Map<String, Object>) ((Map<String,
Object>) 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<Parameter> 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<String, Object> 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<Parameter> 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<Parameter> 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<Parameter> 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<Parameter> params =
OpenApiUtils.generateRequestDocParameters("/dubbo/findById", method);
+ assertThat(params, hasSize(0));
+ }
+
+ @Test
+ void testGenerateRpcRequestDocParametersSimpleType() throws Exception {
+ Method method = DubboTestService.class.getMethod("findById",
String.class);
+ List<Parameter> 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<Parameter> 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<Parameter> 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<Parameter> 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<Parameter> 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<Parameter> 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<Parameter> 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<String, Object> 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<String, Object> 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<DubboTest> 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<GrpcTestClass> responseObserver) {
+ }
+
+ public void unaryCallComplex(final GrpcTestClass request, final
io.grpc.stub.StreamObserver<GrpcTestClass> responseObserver) {
+ }
+
+ public String noStreamObserver(final String request) {
+ return null;
+ }
+ }
+
+ public static class GrpcClientStreamingService {
+
+ public io.grpc.stub.StreamObserver<String> clientStreaming(
+ final io.grpc.stub.StreamObserver<GrpcTestClass>
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<String> ids) {
+ return null;
+ }
+
+ public String searchByMap(final Map<String, String> 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 0000000000..fc3e0d305d
--- /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 167c580da6..bc3d40ac1c 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.Logger;
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 @@ public class SofaServiceEventListener extends
AbstractContextRefreshedEventListe
return ShenyuSofaClient.class;
}
+ @Override
+ protected void handleClass(final Class<?> clazz,
+ final ServiceFactoryBean bean,
+ @NonNull final ShenyuSofaClient
beanShenyuClient,
+ final String superPath) {
+ List<String> 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 @@ public class SofaServiceEventListener extends
AbstractContextRefreshedEventListe
}
@Override
- public void onApplicationEvent(final ContextRefreshedEvent
contextRefreshedEvent) {
- Map<String, ServiceFactoryBean> serviceBean =
contextRefreshedEvent.getApplicationContext().getBeansOfType(ServiceFactoryBean.class);
- for (Map.Entry<String, ServiceFactoryBean> 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 @@ public class SofaServiceEventListener extends
AbstractContextRefreshedEventListe
return Sextet.with(values, consume, produce, apiHttpMethodEnums,
RpcTypeEnum.SOFA, version);
}
- private void handler(final ServiceFactoryBean serviceBean) {
- Class<?> 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<String> 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 a38615c987..5e0bc07b6e 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 class SofaServiceEventListenerTest {
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-e2e/shenyu-e2e-case/shenyu-e2e-case-logging-rocketmq/compose/script/e2e-logging-rocketmq-compose.sh
b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-logging-rocketmq/compose/script/e2e-logging-rocketmq-compose.sh
index 4049b338fd..1967c16f0d 100644
---
a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-logging-rocketmq/compose/script/e2e-logging-rocketmq-compose.sh
+++
b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-logging-rocketmq/compose/script/e2e-logging-rocketmq-compose.sh
@@ -31,16 +31,18 @@ SYNC_ARRAY=("websocket" "http" "zookeeper")
docker network create -d bridge shenyu
for sync in "${SYNC_ARRAY[@]}"; do
+
sync_compose_file="$SHENYU_TESTCASE_DIR"/compose/sync/shenyu-sync-"${sync}".yml
echo -e "------------------\n"
echo "[Start ${sync} synchronous] create shenyu-admin-${sync}.yml
shenyu-bootstrap-${sync}.yml "
- docker compose -f
"$SHENYU_TESTCASE_DIR"/compose/sync/shenyu-sync-"${sync}".yml up -d --quiet-pull
+ docker compose -f "${sync_compose_file}" up -d --quiet-pull || true
sleep 30s
- sh "$SHENYU_TESTCASE_DIR"/k8s/script/healthcheck.sh
http://localhost:31095/actuator/health
- sh "$SHENYU_TESTCASE_DIR"/k8s/script/healthcheck.sh
http://localhost:31195/actuator/health
+ sh "$SHENYU_TESTCASE_DIR"/k8s/script/healthcheck.sh
http://localhost:31095/actuator/health || exit 1
+ docker compose -f "${sync_compose_file}" up -d shenyu-bootstrap
+ sh "$SHENYU_TESTCASE_DIR"/k8s/script/healthcheck.sh
http://localhost:31195/actuator/health || exit 1
docker compose -f "${PRGDIR}"/shenyu-rocketmq-compose.yml up -d --quiet-pull
docker compose -f "${PRGDIR}"/shenyu-examples-http-compose.yml up -d
--quiet-pull
sleep 30s
- sh "$SHENYU_TESTCASE_DIR"/k8s/script/healthcheck.sh
http://localhost:31189/actuator/health
+ sh "$SHENYU_TESTCASE_DIR"/k8s/script/healthcheck.sh
http://localhost:31189/actuator/health || exit 1
sleep 10s
docker ps -a
## run e2e-test
@@ -60,7 +62,7 @@ for sync in "${SYNC_ARRAY[@]}"; do
docker compose -f "${PRGDIR}"/shenyu-rocketmq-compose.yml logs
exit 1
fi
- docker compose -f
"$SHENYU_TESTCASE_DIR"/compose/sync/shenyu-sync-"${sync}".yml down
+ docker compose -f "${sync_compose_file}" down
docker compose -f "${PRGDIR}"/shenyu-rocketmq-compose.yml down
docker compose -f "${PRGDIR}"/shenyu-examples-http-compose.yml down
echo "[Remove ${sync} synchronous] delete shenyu-admin-${sync}.yml
shenyu-bootstrap-${sync}.yml "
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 e589bd0c53..8d8a93f345 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 @@ package
org.apache.shenyu.examples.apache.dubbo.service.annotation.impl;
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 6084bc2470..c8f5135e14 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 @@ import org.springframework.stereotype.Service;
@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 da96e87072..d2efd24320 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 @@ import org.springframework.stereotype.Service;
@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();