Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow using a sub-interface of a generic interface as API with Typesafe client #2151

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ public <T> T build(Class<T> apiClass) {
allowUnexpectedResponseFields);

return apiClass.cast(Proxy.newProxyInstance(getClassLoader(apiClass), new Class<?>[] { apiClass },
(proxy, method, args) -> invoke(graphQLClient, method, args)));
(proxy, method, args) -> invoke(apiClass, graphQLClient, method, args)));
}

private void applyConfigFor(Class<?> apiClass) {
Expand Down Expand Up @@ -221,9 +221,9 @@ private Vertx vertx() {
return vertx != null ? vertx : VertxManager.get();
}

private Object invoke(VertxTypesafeGraphQLClientProxy graphQlClient, java.lang.reflect.Method method,
private Object invoke(Class<?> apiClass, VertxTypesafeGraphQLClientProxy graphQlClient, java.lang.reflect.Method method,
Object... args) {
MethodInvocation methodInvocation = MethodInvocation.of(method, args);
MethodInvocation methodInvocation = MethodInvocation.of(apiClass, method, args);
if (methodInvocation.isDeclaredInCloseable()) {
graphQlClient.close();
return null; // void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.List;
Expand All @@ -33,6 +34,10 @@ public static MethodInvocation of(Method method, Object... args) {
return new MethodInvocation(new TypeInfo(null, method.getDeclaringClass()), method, args);
}

public static MethodInvocation of(Type apiInterface, Method method, Object... args) {
return new MethodInvocation(new TypeInfo(null, apiInterface), method, args);
}

private final TypeInfo type;
private final Method method;
private final Object[] parameterValues;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.Stream.Builder;

Expand Down Expand Up @@ -100,18 +103,43 @@ public String getTypeName() {

private Class<?> resolveTypeVariable() {
// TODO this is not generally correct
if (!(container.type instanceof ParameterizedType)) {
return resolveGenericParameter(type);
}
ParameterizedType parameterizedType = (ParameterizedType) container.type;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
Type actualTypeArgument = actualTypeArguments[0];
if (actualTypeArgument instanceof Class) {
return (Class<?>) actualTypeArgument;
} else if (actualTypeArgument instanceof ParameterizedType) {
return Object.class;
} else if (actualTypeArgument instanceof TypeVariable) {
return resolveGenericParameter(actualTypeArgument);
} else {
throw new UnsupportedOperationException("can't resolve type variable of a " + actualTypeArgument.getTypeName());
}
}

private Class<?> resolveGenericParameter(Type actualTypeArgument) {
TypeVariable<?> typeVariable = (TypeVariable<?>) actualTypeArgument;
TypeInfo concreteInterface = this.container;
while (concreteInterface.type instanceof ParameterizedType) {
concreteInterface = concreteInterface.container;
}
Set<ParameterizedType> genericInterfaces = Arrays.stream((concreteInterface.getRawType()).getGenericInterfaces())
.map(ParameterizedType.class::cast)
.collect(Collectors.toSet());
ParameterizedType paramType = genericInterfaces.stream()
.filter(i -> i.getRawType().equals(typeVariable.getGenericDeclaration()))
.findFirst()
.orElseThrow(() -> new UnsupportedOperationException(
"can't resolve type variable of a " + actualTypeArgument.getTypeName()));
var typeParameters = List.of(((Class<?>) paramType.getRawType()).getTypeParameters());
var actual = typeParameters.stream().filter(p -> p.getName().equals(typeVariable.getName())).findFirst().orElseThrow();
int index = typeParameters.indexOf(actual);
return (Class<?>) paramType.getActualTypeArguments()[index];
}

public String getPackage() {
return ((Class<?>) type).getPackage().getName(); // TODO may throw Class Cast or NPE
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.smallrye.graphql.tests.client.typesafe.generics;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Query;

import io.smallrye.graphql.api.Subscription;
import io.smallrye.graphql.tests.client.typesafe.generics.servermodels.Animal;
import io.smallrye.mutiny.Multi;

@GraphQLApi
public class AnimalApi {

final Map<String, Animal> animals = Map.of("elephant", new Animal("elephant", 34, 5000, "A very big animal"),
"cat", new Animal("cat", 3, 4, "A very cute animal"));

@Query
public List<Animal> allAnimals() {
return new ArrayList<>(animals.values());
}

@Query
public Animal animalWithName(String name) {
return animals.get(name);
}

@Subscription()
public Multi<Animal> animalsSubscription() {
return Multi.createFrom().iterable(animals.values());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.smallrye.graphql.tests.client.typesafe.generics;

import java.util.List;

import org.eclipse.microprofile.graphql.Query;

import io.smallrye.graphql.api.Subscription;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;

public interface AnimalClientApi<T> {
@Query
List<T> allAnimals();

@Query("allAnimals")
Uni<List<T>> allAnimalsUni();

@Query
T animalWithName(String name);

@Query("animalWithName")
Uni<T> animalWithNameUni(String name);

@Subscription()
Multi<T> animalsSubscription();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package io.smallrye.graphql.tests.client.typesafe.generics;

import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi;
import io.smallrye.graphql.client.vertx.typesafe.VertxTypesafeGraphQLClientBuilder;
import io.smallrye.graphql.tests.client.typesafe.generics.clientmodels.FullAnimal;
import io.smallrye.graphql.tests.client.typesafe.generics.clientmodels.SimpleAnimal;
import io.smallrye.graphql.tests.client.typesafe.generics.servermodels.Animal;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.net.URL;
import java.time.Duration;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;

@RunWith(Arquillian.class)
@RunAsClient
public class TypesafeClientGenericsTest {

@Deployment
public static WebArchive deployment() {
return ShrinkWrap.create(WebArchive.class, "annotationIgnore.war")
.addClasses(AnimalApi.class, Animal.class);
}

@ArquillianResource
URL testingURL;

private <T> T createClient(Class<T> clientClass) {
return new VertxTypesafeGraphQLClientBuilder()
.endpoint(testingURL.toString() + "graphql")
.build(clientClass);
}

@GraphQLClientApi
interface SimpleAnimalClientApi extends AnimalClientApi<SimpleAnimal> {

}

@Test
public void singleObjectReturnedSimple() {
SimpleAnimalClientApi client = createClient(SimpleAnimalClientApi.class);

SimpleAnimal response = client.animalWithName("elephant");

assertEquals(elephantSimple, response);
}

@Test
public void uniReturnedSimple() {
SimpleAnimalClientApi client = createClient(SimpleAnimalClientApi.class);

SimpleAnimal response = client.animalWithNameUni("elephant").await().atMost(Duration.ofSeconds(10));

assertEquals(elephantSimple, response);
}

@Test
public void listReturnedSimple() {
SimpleAnimalClientApi client = createClient(SimpleAnimalClientApi.class);

List<SimpleAnimal> response = client.allAnimals();

MatcherAssert.assertThat(response, Matchers.hasItems(elephantSimple, catSimple));
}

@Test
public void uniListReturnedSimple() {
SimpleAnimalClientApi client = createClient(SimpleAnimalClientApi.class);

List<SimpleAnimal> response = client.allAnimalsUni().await().atMost(Duration.ofSeconds(10));

MatcherAssert.assertThat(response,
Matchers.hasItems(elephantSimple, catSimple));
}

@Test
public void subscriptionSimple() {
SimpleAnimalClientApi client = createClient(SimpleAnimalClientApi.class);

List<SimpleAnimal> response = client.animalsSubscription()
.collect().asList()
.await().atMost(Duration.ofSeconds(10));

MatcherAssert.assertThat(response,
Matchers.hasItems(elephantSimple, elephantSimple));
}

@GraphQLClientApi
interface FullAnimalClientApi extends AnimalClientApi<FullAnimal> {

}

@Test
public void singleObjectReturnedFull() {
FullAnimalClientApi client = createClient(FullAnimalClientApi.class);

FullAnimal response = client.animalWithName("elephant");
assertEquals(elephantFull, response);
}

@Test
public void uniReturnedFull() {
FullAnimalClientApi client = createClient(FullAnimalClientApi.class);

FullAnimal response = client.animalWithNameUni("elephant").await().atMost(Duration.ofSeconds(10));

assertEquals(elephantFull, response);
}

@Test
public void listReturnedFull() {
FullAnimalClientApi client = createClient(FullAnimalClientApi.class);

List<FullAnimal> response = client.allAnimals();

MatcherAssert.assertThat(response, Matchers.hasItems(elephantFull, catFull));
}

@Test
public void uniListReturnedFull() {
FullAnimalClientApi client = createClient(FullAnimalClientApi.class);

List<FullAnimal> response = client.allAnimalsUni().await().atMost(Duration.ofSeconds(10));

MatcherAssert.assertThat(response,
Matchers.hasItems(elephantFull, catFull));
}

@Test
public void subscriptionFull() {
FullAnimalClientApi client = createClient(FullAnimalClientApi.class);

List<FullAnimal> response = client.animalsSubscription()
.collect().asList()
.await().atMost(Duration.ofSeconds(10));

MatcherAssert.assertThat(response,
Matchers.hasItems(elephantFull, catFull));
}

private final SimpleAnimal elephantSimple = new SimpleAnimal("elephant");
private final SimpleAnimal catSimple = new SimpleAnimal("cat");
private final FullAnimal elephantFull = new FullAnimal("elephant", 34, 5000, "A very big animal");
private final FullAnimal catFull = new FullAnimal("cat", 3, 4, "A very cute animal");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.smallrye.graphql.tests.client.typesafe.generics.clientmodels;

import java.util.Objects;

public class FullAnimal {
public String name;
public int age;
public int weight;
public String description;

public FullAnimal(String name, int age, int weight, String description) {
this.name = name;
this.age = age;
this.weight = weight;
this.description = description;
}

public FullAnimal() {
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
FullAnimal that = (FullAnimal) o;
return age == that.age && weight == that.weight && Objects.equals(name, that.name)
&& Objects.equals(description, that.description);
}

@Override
public int hashCode() {
return Objects.hash(name, age, weight, description);
}

@Override
public String toString() {
return "FullAnimal{" +
"name='" + name + '\'' +
", age=" + age +
", weight=" + weight +
", description='" + description + '\'' +
'}';
}
}
Loading
Loading