diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java index 948e04c57..f0e5eff42 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java @@ -188,7 +188,7 @@ public T build(Class 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) { @@ -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 diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/MethodInvocation.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/MethodInvocation.java index b4e2f8485..32be96d21 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/MethodInvocation.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/MethodInvocation.java @@ -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; @@ -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; diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/TypeInfo.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/TypeInfo.java index c35d7a42b..08acf7947 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/TypeInfo.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/TypeInfo.java @@ -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; @@ -100,6 +103,9 @@ 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]; @@ -107,11 +113,33 @@ private Class resolveTypeVariable() { 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 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 } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/AnimalApi.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/AnimalApi.java new file mode 100644 index 000000000..0bc8ccea8 --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/AnimalApi.java @@ -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 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 allAnimals() { + return new ArrayList<>(animals.values()); + } + + @Query + public Animal animalWithName(String name) { + return animals.get(name); + } + + @Subscription() + public Multi animalsSubscription() { + return Multi.createFrom().iterable(animals.values()); + } +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/AnimalClientApi.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/AnimalClientApi.java new file mode 100644 index 000000000..6e22b4192 --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/AnimalClientApi.java @@ -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 { + @Query + List allAnimals(); + + @Query("allAnimals") + Uni> allAnimalsUni(); + + @Query + T animalWithName(String name); + + @Query("animalWithName") + Uni animalWithNameUni(String name); + + @Subscription() + Multi animalsSubscription(); +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/TypesafeClientGenericsTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/TypesafeClientGenericsTest.java new file mode 100644 index 000000000..cf3eecf5e --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/TypesafeClientGenericsTest.java @@ -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 createClient(Class clientClass) { + return new VertxTypesafeGraphQLClientBuilder() + .endpoint(testingURL.toString() + "graphql") + .build(clientClass); + } + + @GraphQLClientApi + interface SimpleAnimalClientApi extends AnimalClientApi { + + } + + @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 response = client.allAnimals(); + + MatcherAssert.assertThat(response, Matchers.hasItems(elephantSimple, catSimple)); + } + + @Test + public void uniListReturnedSimple() { + SimpleAnimalClientApi client = createClient(SimpleAnimalClientApi.class); + + List 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 response = client.animalsSubscription() + .collect().asList() + .await().atMost(Duration.ofSeconds(10)); + + MatcherAssert.assertThat(response, + Matchers.hasItems(elephantSimple, elephantSimple)); + } + + @GraphQLClientApi + interface FullAnimalClientApi extends AnimalClientApi { + + } + + @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 response = client.allAnimals(); + + MatcherAssert.assertThat(response, Matchers.hasItems(elephantFull, catFull)); + } + + @Test + public void uniListReturnedFull() { + FullAnimalClientApi client = createClient(FullAnimalClientApi.class); + + List 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 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"); +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/clientmodels/FullAnimal.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/clientmodels/FullAnimal.java new file mode 100644 index 000000000..b9ac30dd1 --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/clientmodels/FullAnimal.java @@ -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 + '\'' + + '}'; + } +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/clientmodels/SimpleAnimal.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/clientmodels/SimpleAnimal.java new file mode 100644 index 000000000..282e97eb7 --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/clientmodels/SimpleAnimal.java @@ -0,0 +1,37 @@ +package io.smallrye.graphql.tests.client.typesafe.generics.clientmodels; + +import java.util.Objects; + +public class SimpleAnimal { + public String name; + + public SimpleAnimal() { + + } + + public SimpleAnimal(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + SimpleAnimal that = (SimpleAnimal) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + + @Override + public String toString() { + return "SimpleAnimal{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/servermodels/Animal.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/servermodels/Animal.java new file mode 100644 index 000000000..eb5cb87e0 --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/generics/servermodels/Animal.java @@ -0,0 +1,15 @@ +package io.smallrye.graphql.tests.client.typesafe.generics.servermodels; + +public class Animal { + public String name; + public int age; + public int weight; + public String description; + + public Animal(String name, int age, int weight, String description) { + this.name = name; + this.age = age; + this.weight = weight; + this.description = description; + } +}