Skip to content

Commit 58c5782

Browse files
authored
Merge pull request #47047 from Postremus/issue/36343-rest-client-type-variable
Support type variables on sub resource interfaces for the rest client
2 parents da88557 + b793849 commit 58c5782

File tree

2 files changed

+268
-8
lines changed

2 files changed

+268
-8
lines changed

extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.jboss.jandex.Type.Kind.CLASS;
66
import static org.jboss.jandex.Type.Kind.PARAMETERIZED_TYPE;
77
import static org.jboss.jandex.Type.Kind.PRIMITIVE;
8+
import static org.jboss.jandex.Type.Kind.TYPE_VARIABLE;
89
import static org.jboss.resteasy.reactive.client.impl.RestClientRequestContext.DEFAULT_CONTENT_TYPE_PROP;
910
import static org.jboss.resteasy.reactive.common.processor.EndpointIndexer.extractProducesConsumesValues;
1011
import static org.jboss.resteasy.reactive.common.processor.JandexUtil.isAssignableFrom;
@@ -83,6 +84,7 @@
8384
import org.jboss.jandex.ParameterizedType;
8485
import org.jboss.jandex.PrimitiveType;
8586
import org.jboss.jandex.Type;
87+
import org.jboss.jandex.TypeVariable;
8688
import org.jboss.logging.Logger;
8789
import org.jboss.resteasy.reactive.client.api.ClientMultipartForm;
8890
import org.jboss.resteasy.reactive.client.handlers.ClientObservabilityHandler;
@@ -930,7 +932,7 @@ A more full example of generated client (with sub-resource) can is at the bottom
930932
handleSubResourceMethod(enrichers, generatedClasses, interfaceClass, index, defaultMediaType,
931933
httpAnnotationToMethod, name, classContext, baseTarget, methodIndex, method,
932934
javaMethodParameters, jandexMethod, multipartResponseTypes, Collections.emptyList(),
933-
generatedSubResources);
935+
generatedSubResources, new HashMap<>());
934936
} else {
935937
FieldDescriptor methodField = classContext.createJavaMethodField(interfaceClass, jandexMethod,
936938
methodIndex);
@@ -1228,7 +1230,7 @@ A more full example of generated client (with sub-resource) can is at the bottom
12281230
handleReturn(interfaceClass, defaultMediaType, method.getHttpMethod(),
12291231
method.getConsumes(), jandexMethod, methodCreator, formParams,
12301232
bodyParameterIdx == null ? null : methodCreator.getMethodParam(bodyParameterIdx), builder,
1231-
multipart);
1233+
multipart, Collections.emptyMap());
12321234
}
12331235
}
12341236

@@ -1399,9 +1401,48 @@ private void handleSubResourceMethod(List<JaxrsClientReactiveEnricherBuildItem>
13991401
ClassRestClientContext ownerContext, ResultHandle ownerTarget, int methodIndex,
14001402
ResourceMethod method, String[] javaMethodParameters, MethodInfo jandexMethod,
14011403
Set<ClassInfo> multipartResponseTypes, List<SubResourceParameter> ownerSubResourceParameters,
1402-
Map<GeneratedSubResourceKey, String> generatedSubResources) {
1404+
Map<GeneratedSubResourceKey, String> generatedSubResources, Map<String, Type> ownerIdentifierToTypeVariable) {
1405+
1406+
Map<String, Type> identifierToTypeVariable = new HashMap<>();
14031407
Type returnType = jandexMethod.returnType();
1404-
if (returnType.kind() != CLASS) {
1408+
if (returnType.kind() == PARAMETERIZED_TYPE) {
1409+
1410+
ParameterizedType parameterizedReturnType = returnType.asParameterizedType();
1411+
ClassInfo returnClass = index.getClassByName(returnType.name());
1412+
ParameterizedType.Builder methodReturnTypeBuilder = ParameterizedType.builder(returnType.name());
1413+
for (int i = 0; i < parameterizedReturnType.arguments().size(); i++) {
1414+
Type paramReturnTypeArg = parameterizedReturnType.arguments().get(i);
1415+
Type resolvedType;
1416+
if (paramReturnTypeArg.kind() == TYPE_VARIABLE) {
1417+
// method returns another subresource, and one of the arguments is a type variable e.g. Wrapper<T>
1418+
resolvedType = ownerIdentifierToTypeVariable.get(paramReturnTypeArg.asTypeVariable().identifier());
1419+
if (resolvedType == null) {
1420+
throw new IllegalArgumentException(
1421+
"Type variable %s of the sub resource locator method's return type %s could not be resolved."
1422+
.formatted(paramReturnTypeArg.asTypeVariable().identifier(), jandexMethod));
1423+
}
1424+
} else {
1425+
// Subresource, but no type variable, e.g. Wrapper<String>
1426+
resolvedType = paramReturnTypeArg;
1427+
}
1428+
1429+
identifierToTypeVariable.put(returnClass.typeParameters().get(i).identifier(), resolvedType);
1430+
methodReturnTypeBuilder.addArgument(resolvedType);
1431+
}
1432+
1433+
// rewrite returnType to reflect the resolved type variable for the generatedSubResources cache
1434+
// i.e. Wrapper<String> instead of Wrapper<V>
1435+
returnType = methodReturnTypeBuilder.build();
1436+
} else if (returnType.kind() == TYPE_VARIABLE) {
1437+
TypeVariable typeVariable = returnType.asTypeVariable();
1438+
// rewrite returnType to reflect the resolved type variable for the generatedSubResources cache
1439+
// i.e. String instead of Type Variable V
1440+
returnType = identifierToTypeVariable.get(typeVariable.identifier());
1441+
if (returnType == null) {
1442+
return;
1443+
}
1444+
1445+
} else if (returnType.kind() != CLASS) {
14051446
// sort of sub-resource method that returns a thing that isn't a class
14061447
throw new IllegalArgumentException("Sub resource type is not a class: " + returnType.name().toString());
14071448
}
@@ -1854,7 +1895,7 @@ private void handleSubResourceMethod(List<JaxrsClientReactiveEnricherBuildItem>
18541895
handleReturn(subInterface, defaultMediaType,
18551896
getHttpMethod(jandexSubMethod, subMethod.getHttpMethod(), httpAnnotationToMethod),
18561897
consumes, jandexSubMethod, subMethodCreator, formParams, bodyParameterValue,
1857-
builder, multipart);
1898+
builder, multipart, identifierToTypeVariable);
18581899
} else {
18591900
// finding corresponding jandex method, used by enricher (MicroProfile enricher stores it in a field
18601901
// to later fill in context with corresponding java.lang.reflect.Method)
@@ -1867,7 +1908,7 @@ private void handleSubResourceMethod(List<JaxrsClientReactiveEnricherBuildItem>
18671908
handleSubResourceMethod(enrichers, generatedClasses, subInterface, index,
18681909
defaultMediaType, httpAnnotationToMethod, subName, subContext, subMethodTarget,
18691910
subMethodIndex, subMethod, subJavaMethodParameters, jandexSubMethod,
1870-
multipartResponseTypes, subParamFields, generatedSubResources);
1911+
multipartResponseTypes, subParamFields, generatedSubResources, identifierToTypeVariable);
18711912
}
18721913

18731914
}
@@ -2295,13 +2336,24 @@ private String getHttpMethod(MethodInfo subMethod, String defaultMethod, Map<Dot
22952336

22962337
private void handleReturn(ClassInfo restClientInterface, String defaultMediaType, String httpMethod, String[] consumes,
22972338
MethodInfo jandexMethod, MethodCreator methodCreator, ResultHandle formParams,
2298-
ResultHandle bodyValue, AssignableResultHandle builder, boolean multipart) {
2339+
ResultHandle bodyValue, AssignableResultHandle builder, boolean multipart,
2340+
Map<String, Type> identifierToTypeVariable) {
22992341
Type returnType = jandexMethod.returnType();
23002342
ReturnCategory returnCategory = ReturnCategory.BLOCKING;
23012343

2344+
if (returnType.kind() == TYPE_VARIABLE) {
2345+
TypeVariable typeVariable = returnType.asTypeVariable();
2346+
Type resolvedTypeVariable = identifierToTypeVariable.get(typeVariable.identifier());
2347+
if (resolvedTypeVariable != null) {
2348+
returnType = resolvedTypeVariable;
2349+
} else {
2350+
throw new RuntimeException("Type variable %s of the return type of method %s could not be resolved."
2351+
.formatted(typeVariable.identifier(), jandexMethod));
2352+
}
2353+
}
2354+
23022355
String simpleReturnType = returnType.name().toString();
23032356
ResultHandle genericReturnType = null;
2304-
23052357
if (returnType.kind() == PARAMETERIZED_TYPE) {
23062358
ParameterizedType paramType = returnType.asParameterizedType();
23072359
if (ASYNC_RETURN_TYPES.contains(paramType.name())) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package io.quarkus.rest.client.reactive.subresource;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.lang.annotation.Annotation;
8+
import java.lang.reflect.Type;
9+
import java.net.URI;
10+
import java.nio.charset.StandardCharsets;
11+
import java.util.Arrays;
12+
import java.util.List;
13+
import java.util.Map;
14+
15+
import jakarta.ws.rs.GET;
16+
import jakarta.ws.rs.Path;
17+
import jakarta.ws.rs.WebApplicationException;
18+
import jakarta.ws.rs.core.MediaType;
19+
import jakarta.ws.rs.core.MultivaluedMap;
20+
import jakarta.ws.rs.ext.Provider;
21+
22+
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
23+
import org.eclipse.microprofile.rest.client.inject.RestClient;
24+
import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext;
25+
import org.jboss.resteasy.reactive.client.spi.ClientMessageBodyReader;
26+
import org.junit.jupiter.api.Assertions;
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.api.extension.RegisterExtension;
29+
30+
import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
31+
import io.quarkus.test.QuarkusUnitTest;
32+
33+
public class SubResourceGenericsTest {
34+
@RegisterExtension
35+
static final QuarkusUnitTest TEST = new QuarkusUnitTest()
36+
.withApplicationRoot((jar) -> jar
37+
.addClass(Resource.class)
38+
.addClass(ClientLocator.class)
39+
.addClass(TranslationSubResource.class)
40+
.addClass(EnglishSubResource.class)
41+
.addClass(ShortsSubResource.class)
42+
.addClass(HelloSubResource.class)
43+
.addClass(EachCellAsListElementClientMessageBodyReader.class)
44+
.addClass(CellsAsMapEntryClientMessageBodyReader.class));
45+
46+
@RestClient
47+
ClientLocator client;
48+
49+
@Test
50+
void testRestCalls() {
51+
{
52+
String result = client.string().subResource().subResource().subResource().get();
53+
assertThat(result).isEqualTo("Hello,World");
54+
}
55+
56+
{
57+
Long result = client.number().subResource().subResource().subResource().get();
58+
assertThat(result).isEqualTo(42L);
59+
}
60+
61+
{
62+
List<String> result = client.testListOnRoot();
63+
assertThat(result).contains("Hello", "World");
64+
}
65+
66+
{
67+
List<String> result = client.list().subResource().subResource().subResource().get();
68+
assertThat(result).contains("Hello", "World");
69+
}
70+
71+
{
72+
Map<String, String> result = client.map().subResource().subResource().subResource().get();
73+
assertThat(result).containsEntry("Hello", "World");
74+
}
75+
}
76+
77+
@Test
78+
void testFailureSubResourceLocatorMethodWithUnresolvedTypeVariable() {
79+
80+
try {
81+
QuarkusRestClientBuilder.newBuilder().baseUri(URI.create("http://localhost:8081"))
82+
.build(TranslationSubResource.class);
83+
} catch (Exception e) {
84+
assertThat(e.getMessage()).endsWith(
85+
"Failed to generate client for class interface io.quarkus.rest.client.reactive.subresource.SubResourceGenericsTest$TranslationSubResource : Type variable R of the sub resource locator method's return type io.quarkus.rest.client.reactive.subresource.SubResourceGenericsTest$EnglishSubResource<R> subResource() could not be resolved.");
86+
return;
87+
}
88+
Assertions.fail("Should have thrown an exception");
89+
}
90+
91+
@Test
92+
void testFailureRestClientMethodWithUnresolvedTypeVariable() {
93+
94+
try {
95+
QuarkusRestClientBuilder.newBuilder().baseUri(URI.create("http://localhost:8081")).build(HelloSubResource.class);
96+
} catch (Exception e) {
97+
assertThat(e.getMessage()).endsWith(
98+
"Failed to generate client for class interface io.quarkus.rest.client.reactive.subresource.SubResourceGenericsTest$HelloSubResource : Type variable V of the return type of method V get() could not be resolved.");
99+
return;
100+
}
101+
Assertions.fail("Should have thrown an exception");
102+
}
103+
104+
@Path("")
105+
public static class Resource {
106+
@Path("greetings/translations/english/shorts/hello")
107+
@GET
108+
public String greet() {
109+
return "Hello,World";
110+
}
111+
112+
@Path("greetings-count/translations/english/shorts/hello")
113+
@GET
114+
public Long greetingCount() {
115+
return 42L;
116+
}
117+
}
118+
119+
@RegisterRestClient(baseUri = "http://localhost:8081")
120+
@Path("")
121+
public interface ClientLocator {
122+
123+
@Path("greetings/translations")
124+
TranslationSubResource<String> string();
125+
126+
@Path("greetings-count/translations")
127+
TranslationSubResource<Long> number();
128+
129+
@Path("greetings/translations")
130+
TranslationSubResource<List<String>> list();
131+
132+
@Path("greetings/translations")
133+
TranslationSubResource<Map<String, String>> map();
134+
135+
@Path("greetings/translations/english/shorts/hello")
136+
@GET
137+
List<String> testListOnRoot();
138+
}
139+
140+
public interface TranslationSubResource<R> {
141+
@Path("english")
142+
EnglishSubResource<R> subResource();
143+
}
144+
145+
public interface EnglishSubResource<Z> {
146+
@Path("shorts")
147+
ShortsSubResource<Z> subResource();
148+
}
149+
150+
public interface ShortsSubResource<Y> {
151+
@Path("hello")
152+
HelloSubResource<Y> subResource();
153+
}
154+
155+
public interface HelloSubResource<V> {
156+
@GET
157+
V get();
158+
}
159+
160+
@Provider
161+
public static class EachCellAsListElementClientMessageBodyReader implements ClientMessageBodyReader<List<String>> {
162+
163+
@Override
164+
public List<String> readFrom(Class<List<String>> type, Type genericType, Annotation[] annotations, MediaType mediaType,
165+
MultivaluedMap<String, String> httpHeaders, InputStream entityStream, RestClientRequestContext context)
166+
throws IOException, WebApplicationException {
167+
return readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream);
168+
}
169+
170+
@Override
171+
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
172+
return genericType.getTypeName().equals("java.util.List<java.lang.String>");
173+
}
174+
175+
@Override
176+
public List<String> readFrom(Class<List<String>> type, Type genericType, Annotation[] annotations, MediaType mediaType,
177+
MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
178+
throws IOException, WebApplicationException {
179+
String body = new String(entityStream.readAllBytes(), StandardCharsets.UTF_8);
180+
return Arrays.asList(body.split(","));
181+
}
182+
}
183+
184+
@Provider
185+
public static class CellsAsMapEntryClientMessageBodyReader implements ClientMessageBodyReader<Map<String, String>> {
186+
187+
@Override
188+
public Map<String, String> readFrom(Class<Map<String, String>> type, Type genericType, Annotation[] annotations,
189+
MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream,
190+
RestClientRequestContext context) throws IOException, WebApplicationException {
191+
return readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream);
192+
}
193+
194+
@Override
195+
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
196+
return genericType.getTypeName().equals("java.util.Map<java.lang.String, java.lang.String>");
197+
}
198+
199+
@Override
200+
public Map<String, String> readFrom(Class<Map<String, String>> type, Type genericType, Annotation[] annotations,
201+
MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
202+
throws IOException, WebApplicationException {
203+
String body = new String(entityStream.readAllBytes(), StandardCharsets.UTF_8);
204+
String[] split = body.split(",");
205+
return Map.of(split[0], split[1]);
206+
}
207+
}
208+
}

0 commit comments

Comments
 (0)