From 23aa62a7667726f62d5e2dc0fbf2f52b122b9da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Marti=C5=A1ka?= Date: Wed, 15 Sep 2021 09:25:32 +0200 Subject: [PATCH] Support for records as input on the server side --- .../schema/creator/type/InputTypeCreator.java | 15 +++ server/integration-tests-jdk16/pom.xml | 3 + .../graphql/tests/records/RecordTest.java | 120 ++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/records/RecordTest.java diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/InputTypeCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/InputTypeCreator.java index 45109ea9e..dca17c3e2 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/InputTypeCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/InputTypeCreator.java @@ -8,6 +8,7 @@ import java.util.Optional; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; import org.jboss.jandex.MethodInfo; import org.jboss.logging.Logger; @@ -39,6 +40,8 @@ public class InputTypeCreator implements Creator { private final FieldCreator fieldCreator; private final TypeAutoNameStrategy autoNameStrategy; + private final DotName RECORD = DotName.createSimple("java.lang.Record"); + public InputTypeCreator(FieldCreator fieldCreator, TypeAutoNameStrategy autoNameStrategy) { this.fieldCreator = fieldCreator; this.autoNameStrategy = autoNameStrategy; @@ -84,6 +87,18 @@ public boolean hasUseableConstructor(ClassInfo classInfo) { * @return the creator, null, if no public constructor or factory method is found */ public MethodInfo findCreator(ClassInfo classInfo) { + if (classInfo.superName().equals(RECORD)) { + // records should always have a canonical constructor + // the creator will be picked by the JSONB impl at runtime anyway, so + // just make sure we can find a public constructor and move on + for (MethodInfo constructor : classInfo.constructors()) { + if (!Modifier.isPublic(constructor.flags())) + continue; + return constructor; + } + return null; + } + for (final MethodInfo constructor : classInfo.constructors()) { if (!Modifier.isPublic(constructor.flags())) continue; diff --git a/server/integration-tests-jdk16/pom.xml b/server/integration-tests-jdk16/pom.xml index 16854f66f..9bd478f0c 100644 --- a/server/integration-tests-jdk16/pom.xml +++ b/server/integration-tests-jdk16/pom.xml @@ -212,6 +212,9 @@ maven-compiler-plugin 16 + + true diff --git a/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/records/RecordTest.java b/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/records/RecordTest.java new file mode 100644 index 000000000..225198730 --- /dev/null +++ b/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/records/RecordTest.java @@ -0,0 +1,120 @@ +package io.smallrye.graphql.tests.records; + +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.core.Document; +import io.smallrye.graphql.client.core.InputObject; +import io.smallrye.graphql.client.core.InputObjectField; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.dynamic.vertx.VertxDynamicGraphQLClientBuilder; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +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.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.json.bind.annotation.JsonbCreator; +import java.net.URL; + +import static io.smallrye.graphql.client.core.Argument.arg; +import static io.smallrye.graphql.client.core.Argument.args; +import static io.smallrye.graphql.client.core.Document.document; +import static io.smallrye.graphql.client.core.Field.field; +import static io.smallrye.graphql.client.core.InputObject.inputObject; +import static io.smallrye.graphql.client.core.InputObjectField.prop; +import static io.smallrye.graphql.client.core.Operation.operation; +import static org.junit.Assert.assertEquals; + +/** + * This test verifies that the server side can handle Java records in GraphQL apis, both as input and as output types. + */ +@RunWith(Arquillian.class) +@RunAsClient +public class RecordTest { + + @Deployment + public static WebArchive deployment() { + return ShrinkWrap.create(WebArchive.class) + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml") + .addClasses(SimpleRecord.class); + } + + @ArquillianResource + URL testingURL; + + @Test + public void testSimpleRecord() throws Exception { + try (DynamicGraphQLClient client = new VertxDynamicGraphQLClientBuilder() + .url(testingURL.toString() + "graphql").build()) { + Document query = document(operation( + field("simple", + args(arg("input", + inputObject(prop("a", "a"), prop("b", "b")))), + field("a"), + field("b")))); + Response response = client.executeSync(query); + assertEquals("a", response.getData().getJsonObject("simple").getString("a")); + assertEquals("b", response.getData().getJsonObject("simple").getString("b")); + } + } + + @Test + public void testSimpleRecordWithFactory() throws Exception { + try (DynamicGraphQLClient client = new VertxDynamicGraphQLClientBuilder() + .url(testingURL.toString() + "graphql").build()) { + Document query = document(operation( + field("simpleWithFactory", + args(arg("input", + inputObject(prop("a", "a"), prop("b", "b")))), + field("a"), + field("b")))); + Response response = client.executeSync(query); + System.out.println(response); + System.out.println("query.build() = " + query.build()); + assertEquals("a", response.getData().getJsonObject("simpleWithFactory").getString("a")); + assertEquals("b", response.getData().getJsonObject("simpleWithFactory").getString("b")); + } + } + + @GraphQLApi + public static class Api { + + @Query + public SimpleRecord simple(SimpleRecord input) { + return input; + } + + @Query + public SimpleRecordWithFactory simpleWithFactory(SimpleRecordWithFactory input) { + return input; + } + + } + + public record SimpleRecord(String a, String b) { + + // FIXME: until Yasson receives proper support for records, we have + // to use this hack to tell Yasson to use the canonical constructor when + // deserializing from JSON. Otherwise it will try to find and use a no-arg constructor, and + // that does not exist. + @JsonbCreator + public SimpleRecord { + } + + } + + public record SimpleRecordWithFactory(String a, String b) { + + @JsonbCreator + public static SimpleRecordWithFactory build(String a, String b) { + return new SimpleRecordWithFactory(a, b); + } + + } + +}