package io.leangen.graphql; import com.fasterxml.jackson.annotation.JsonCreator; import io.leangen.geantyref.GenericTypeReflector; import io.leangen.graphql.annotations.GraphQLId; import io.leangen.graphql.annotations.GraphQLIgnore; import io.leangen.graphql.annotations.GraphQLInputField; import io.leangen.graphql.annotations.GraphQLNonNull; import io.leangen.graphql.annotations.GraphQLQuery; import io.leangen.graphql.annotations.GraphQLScalar; import io.leangen.graphql.execution.GlobalEnvironment; import io.leangen.graphql.metadata.InputField; import io.leangen.graphql.metadata.TypedElement; import io.leangen.graphql.metadata.strategy.value.InputFieldBuilder; import io.leangen.graphql.metadata.strategy.value.InputFieldBuilderParams; import io.leangen.graphql.metadata.strategy.value.gson.GsonValueMapper; import io.leangen.graphql.metadata.strategy.value.gson.GsonValueMapperFactory; import io.leangen.graphql.metadata.strategy.value.jackson.JacksonValueMapper; import io.leangen.graphql.metadata.strategy.value.jackson.JacksonValueMapperFactory; import org.junit.Test; import java.beans.ConstructorProperties; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedParameterizedType; import java.lang.reflect.AnnotatedType; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; public class InputFieldDiscoveryTest { private JacksonValueMapper jackson = new JacksonValueMapperFactory().getValueMapper(Collections.emptyMap(), ENVIRONMENT); private GsonValueMapper gson = new GsonValueMapperFactory().getValueMapper(Collections.emptyMap(), ENVIRONMENT); private static final TypedElement IGNORED_TYPE = new TypedElement(GenericTypeReflector.annotate(Object.class), (AnnotatedElement) null); private static final GlobalEnvironment ENVIRONMENT = new TestGlobalEnvironment(); private static final InputField[] expectedDefaultFields = new InputField[] { new InputField("field1", null, IGNORED_TYPE, null, null), new InputField("field2", null, IGNORED_TYPE, null, null), new InputField("field3", null, IGNORED_TYPE, null, null) }; private static final InputField[] expectedFilteredDefaultFields = new InputField[] {expectedDefaultFields[0], expectedDefaultFields[2]}; private static final InputField[] expectedExplicitFields = new InputField[] { new InputField("aaa", "AAA", IGNORED_TYPE, null, "AAAA"), new InputField("bbb", "BBB", IGNORED_TYPE, null, 2222), new InputField("ccc", "CCC", IGNORED_TYPE, null, 3333) }; private static final InputField[] expectedQueryFields = new InputField[] { new InputField("aaa", null, IGNORED_TYPE, null, null), new InputField("bbb", null, IGNORED_TYPE, null, null), new InputField("ccc", null, IGNORED_TYPE, null, null) }; @Test public void basicFieldsTest() { assertFieldNamesEqual(FieldsOnly.class, expectedDefaultFields); } @Test public void basicGettersTest() { assertFieldNamesEqual(GettersOnly.class, expectedDefaultFields); } @Test public void basicSettersTest() { assertFieldNamesEqual(SettersOnly.class, expectedDefaultFields); } @Test public void explicitFieldsTest() { assertFieldNamesEqual(ExplicitFields.class, expectedExplicitFields); } @Test public void explicitGettersTest() { assertFieldNamesEqual(ExplicitGetters.class, expectedExplicitFields); } @Test public void explicitSettersTest() { assertFieldNamesEqual(ExplicitSetters.class, expectedExplicitFields); } @Test public void queryFieldsTest() { assertFieldNamesEqual(QueryFields.class, expectedQueryFields); } @Test public void queryGettersTest() { assertFieldNamesEqual(QueryGetters.class, expectedQueryFields); } @Test public void querySettersTest() { assertFieldNamesEqual(QuerySetters.class, expectedQueryFields); } @Test public void mixedFieldsTest() { assertFieldNamesEqual(MixedFieldsWin.class, expectedExplicitFields); } @Test public void mixedGettersTest() { assertFieldNamesEqual(MixedGettersWin.class, expectedExplicitFields); } @Test public void mixedSettersTest() { assertFieldNamesEqual(MixedSettersWin.class, expectedExplicitFields); } @Test public void conflictingGettersTest() { assertFieldNamesEqual(ConflictingGettersWin.class, expectedExplicitFields); } @Test public void conflictingSettersTest() { assertFieldNamesEqual(ConflictingSettersWin.class, expectedExplicitFields); } @Test public void allConflictingSettersTest() { assertFieldNamesEqual(AllConflictingSettersWin.class, expectedExplicitFields); } @Test public void hiddenSettersTest() { assertFieldNamesEqual(HiddenSetters.class, expectedFilteredDefaultFields); } @Test public void hiddenCtorParamsTest() { assertFieldNamesEqual(jackson, HiddenCtorParams.class, expectedFilteredDefaultFields); } @Test public void mergedTypesTest() { Set<InputField> jFields = getInputFields(jackson, MergedTypes.class); Set<InputField> gFields = getInputFields(gson, MergedTypes.class); assertTypesMerged(jFields); assertTypesMerged(gFields); assertAllFieldsEqual(jFields, gFields); } @Test public void abstractInputTest() { assertFieldNamesEqual(Abstract.class, expectedDefaultFields); } @Test public void jacksonDelegatedConstructorTest() { assertFieldNamesEqual(jackson, Delegator.class, expectedDefaultFields); } private void assertFieldNamesEqual(Class typeToScan, InputField... expectedFields) { Set<InputField> jFields = assertFieldNamesEqual(jackson, typeToScan, expectedFields); Set<InputField> gFields = assertFieldNamesEqual(gson, typeToScan, expectedFields); assertAllFieldsEqual(jFields, gFields); } private Set<InputField> assertFieldNamesEqual(InputFieldBuilder mapper, Class typeToScan, InputField... templates) { Set<InputField> fields = getInputFields(mapper, typeToScan); assertEquals(templates.length, fields.size()); for (InputField template : templates) { Optional<InputField> field = fields.stream().filter(input -> input.getName().equals(template.getName())).findFirst(); assertTrue("Field '" + template.getName() + "' doesn't match between different strategies", field.isPresent()); assertEquals(template.getDescription(), field.get().getDescription()); assertEquals(template.getDefaultValue(), field.get().getDefaultValue()); } return fields; } private void assertTypesMerged(Set<InputField> fields) { Optional<InputField> field1 = fields.stream().filter(field -> field.getName().equals("field1")).findFirst(); Optional<InputField> field2 = fields.stream().filter(field -> field.getName().equals("field2")).findFirst(); Optional<InputField> field3 = fields.stream().filter(field -> field.getName().equals("field3")).findFirst(); assertTrue(field1.isPresent() && field2.isPresent() && field3.isPresent()); AnnotatedType type1 = field1.get().getTypedElement().getJavaType(); assertTrue(type1.isAnnotationPresent(GraphQLNonNull.class) && type1.isAnnotationPresent(GraphQLId.class)); AnnotatedType type2 = field2.get().getTypedElement().getJavaType(); assertTrue(type2.isAnnotationPresent(GraphQLNonNull.class) && type2.isAnnotationPresent(GraphQLId.class)); AnnotatedType type3 = field3.get().getTypedElement().getJavaType(); assertTrue(type3.isAnnotationPresent(GraphQLNonNull.class)); AnnotatedType type31 = ((AnnotatedParameterizedType) type3).getAnnotatedActualTypeArguments()[0]; assertTrue(type31.isAnnotationPresent(GraphQLNonNull.class) && type31.isAnnotationPresent(GraphQLScalar.class)); } private Set<InputField> getInputFields(InputFieldBuilder mapper, Class typeToScan) { return mapper.getInputFields( InputFieldBuilderParams.builder() .withType(GenericTypeReflector.annotate(typeToScan)) .withEnvironment(ENVIRONMENT) .withConcreteSubTypes(Arrays.asList(Concrete.class, Concrete2.class)) .build()); } private void assertAllFieldsEqual(Set<InputField> fields1, Set<InputField> fields2) { assertEquals(fields1.size(), fields2.size()); fields1.forEach(f1 -> assertTrue(fields2.stream().anyMatch(f2 -> f1.getName().equals(f2.getName()) && Objects.equals(f1.getDescription(), f2.getDescription()) && GenericTypeReflector.equals(f1.getJavaType(), f2.getJavaType()) && Objects.equals(f1.getDefaultValue(), f2.getDefaultValue())))); } private class FieldsOnly { public String field1; public int field2; public Object field3; } private class GettersOnly { private String field1; private int field2; private Object field3; public String getField1() { return field1; } public int getField2() { return field2; } public Object getField3() { return field3; } } private class SettersOnly { private String field1; private int field2; private Object field3; public void setField1(String field1) { this.field1 = field1; } public void setField2(int field2) { this.field2 = field2; } public void setField3(Object field3) { this.field3 = field3; } } private class ExplicitFields { @GraphQLInputField(name = "aaa", description = "AAA", defaultValue = "AAAA") public String field1; @GraphQLInputField(name = "bbb", description = "BBB", defaultValue = "2222") public int field2; @GraphQLInputField(name = "ccc", description = "CCC", defaultValue = "3333") public Object field3; } private class ExplicitGetters { private String field1; private int field2; private Object field3; @GraphQLInputField(name = "aaa", description = "AAA", defaultValue = "AAAA") public String getField1() { return field1; } @GraphQLInputField(name = "bbb", description = "BBB", defaultValue = "2222") public int getField2() { return field2; } @GraphQLInputField(name = "ccc", description = "CCC", defaultValue = "3333") public Object getField3() { return field3; } } private class ExplicitSetters { private String field1; private int field2; private Object field3; @GraphQLInputField(name = "aaa", description = "AAA", defaultValue = "AAAA") public void setField1(String field1) { this.field1 = field1; } @GraphQLInputField(name = "bbb", description = "BBB", defaultValue = "2222") public void setField2(int field2) { this.field2 = field2; } @GraphQLInputField(name = "ccc", description = "CCC", defaultValue = "3333") public void setField3(Object field3) { this.field3 = field3; } } private class QueryFields { @GraphQLQuery(name = "aaa") public String field1; @GraphQLQuery(name = "bbb") public int field2; @GraphQLQuery(name = "ccc") public Object field3; } private class QueryGetters { private String field1; private int field2; private Object field3; @GraphQLQuery(name = "aaa") public String getField1() { return field1; } @GraphQLQuery(name = "bbb") public int getField2() { return field2; } @GraphQLQuery(name = "ccc") public Object getField3() { return field3; } } private class QuerySetters { private String field1; private int field2; private Object field3; @GraphQLQuery(name = "aaa") public void setField1(String field1) { this.field1 = field1; } @GraphQLQuery(name = "bbb") public void setField2(int field2) { this.field2 = field2; } @GraphQLQuery(name = "ccc") public void setField3(Object field3) { this.field3 = field3; } } private class MixedFieldsWin { @GraphQLInputField(name = "aaa", description = "AAA", defaultValue = "AAAA") private String field1; @GraphQLInputField(name = "bbb", description = "BBB", defaultValue = "2222") private int field2; @GraphQLInputField(name = "ccc", description = "CCC", defaultValue = "3333") private Object field3; @GraphQLQuery(name = "xxx") public String getField1() { return field1; } @GraphQLQuery(name = "yyy") public int getField2() { return field2; } @GraphQLQuery(name = "zzz") public Object getField3() { return field3; } } private class MixedGettersWin { @GraphQLQuery(name = "xxx") private String field1; @GraphQLQuery(name = "yyy") private int field2; @GraphQLQuery(name = "zzz") private Object field3; @GraphQLInputField(name = "aaa", description = "AAA", defaultValue = "AAAA") public String getField1() { return field1; } @GraphQLInputField(name = "bbb", description = "BBB", defaultValue = "2222") public int getField2() { return field2; } @GraphQLInputField(name = "ccc", description = "CCC", defaultValue = "3333") public Object getField3() { return field3; } } private class MixedSettersWin { @GraphQLQuery(name = "xxx") private String field1; @GraphQLQuery(name = "yyy") private int field2; @GraphQLQuery(name = "zzz") private Object field3; @GraphQLInputField(name = "aaa", description = "AAA", defaultValue = "AAAA") public void setField1(String field1) { this.field1 = field1; } @GraphQLInputField(name = "bbb", description = "BBB", defaultValue = "2222") public void setField2(int field2) { this.field2 = field2; } @GraphQLInputField(name = "ccc", description = "CCC", defaultValue = "3333") public void setField3(Object field3) { this.field3 = field3; } } private class ConflictingGettersWin { @GraphQLInputField(name = "xxx", description = "XXX", defaultValue = "XXXX") private String field1; @GraphQLInputField(name = "yyy", description = "YYY", defaultValue = "-1") private int field2; @GraphQLInputField(name = "zzz", description = "ZZZ", defaultValue = "-1") private Object field3; @GraphQLInputField(name = "aaa", description = "AAA", defaultValue = "AAAA") public String getField1() { return field1; } @GraphQLInputField(name = "bbb", description = "BBB", defaultValue = "2222") public int getField2() { return field2; } @GraphQLInputField(name = "ccc", description = "CCC", defaultValue = "3333") public Object getField3() { return field3; } } private class ConflictingSettersWin { @GraphQLInputField(name = "xxx", description = "XXX", defaultValue = "XXXX") private String field1; @GraphQLInputField(name = "yyy", description = "YYY", defaultValue = "-1") private int field2; @GraphQLInputField(name = "zzz", description = "ZZZ", defaultValue = "-1") private Object field3; @GraphQLInputField(name = "aaa", description = "AAA", defaultValue = "AAAA") public void setField1(String field1) { this.field1 = field1; } @GraphQLInputField(name = "bbb", description = "BBB", defaultValue = "2222") public void setField2(int field2) { this.field2 = field2; } @GraphQLInputField(name = "ccc", description = "CCC", defaultValue = "3333") public void setField3(Object field3) { this.field3 = field3; } } private class AllConflictingSettersWin { @GraphQLInputField(name = "xxx", description = "XXX", defaultValue = "XXXX") private String field1; @GraphQLInputField(name = "yyy", description = "YYY", defaultValue = "-1") private int field2; @GraphQLInputField(name = "zzz", description = "ZZZ", defaultValue = "-1") private Object field3; @GraphQLInputField(name = "111", description = "1111", defaultValue = "XXXX") public String getField1() { return field1; } @GraphQLInputField(name = "222", description = "2222", defaultValue = "-1") public int getField2() { return field2; } @GraphQLInputField(name = "333", description = "3333", defaultValue = "-1") public Object getField3() { return field3; } @GraphQLInputField(name = "aaa", description = "AAA", defaultValue = "AAAA") public void setField1(String field1) { this.field1 = field1; } @GraphQLInputField(name = "bbb", description = "BBB", defaultValue = "2222") public void setField2(int field2) { this.field2 = field2; } @GraphQLInputField(name = "ccc", description = "CCC", defaultValue = "3333") public void setField3(Object field3) { this.field3 = field3; } } private class HiddenSetters { private String field1; private int field2; private Object field3; public String getField1() { return field1; } public void setField1(String field1) { this.field1 = field1; } @GraphQLQuery(name = "ignored") @GraphQLInputField(name = "ignored") public int getField2() { return field2; } @GraphQLIgnore @GraphQLInputField(name = "ignored") public void setField2(int field2) { this.field2 = field2; } public Object getField3() { return field3; } @GraphQLInputField public void setField3(Object field3) { this.field3 = field3; } } public static class HiddenCtorParams { private String field1; private int field2; private Object field3; @JsonCreator public HiddenCtorParams(String field1, @GraphQLIgnore int field2, Object field3) { this.field1 = field1; this.field2 = field2; this.field3 = field3; } } private static class MergedTypes { private @GraphQLNonNull String field1; private RelayTest.Book field2; private List<RelayTest.@GraphQLNonNull Book> field3; @JsonCreator // Only Jackson will pick this up. Gson does not use the constructor. public MergedTypes(@GraphQLId String field1, @GraphQLId RelayTest.Book field2, List<RelayTest.@GraphQLScalar Book> field3) { this.field1 = field1; this.field2 = field2; this.field3 = field3; } public String getField1() { return field1; } // Only Gson will pick this up. In Jackson, the constructor parameter shadows the unannotated setter. public void setField1(@GraphQLId String field1) { this.field1 = field1; } @GraphQLInputField public RelayTest.@GraphQLNonNull Book getField2() { return field2; } // Only Gson will pick this up. In Jackson, the constructor parameter shadows the unannotated setter. public void setField2(RelayTest.@GraphQLId Book field2) { this.field2 = field2; } @GraphQLInputField public @GraphQLNonNull List<RelayTest.Book> getField3() { return field3; } // Only Gson will pick this up. In Jackson, the constructor parameter shadows the unannotated setter. public void setField3(List<RelayTest.@GraphQLScalar Book> field3) { this.field3 = field3; } } private interface Abstract { @GraphQLInputField(name = "field1") String getFieldX(); int getField2(); String getField3(); String getField4(); } private static class Concrete implements Abstract { private String fieldX; private int field2; @JsonCreator @ConstructorProperties({"fieldX", "field2"}) public Concrete(String model, int price) { this.fieldX = model; this.field2 = price; } @Override @GraphQLInputField(name = "field1") public String getFieldX() { return fieldX; } @Override public int getField2() { return field2; } @Override public String getField3() { return null; } @Override public String getField4() { return null; } } private static class Concrete2 implements Abstract { private String field3; private String field5; @Override public String getFieldX() { return null; } @Override public int getField2() { return 0; } @Override public String getField3() { return field3; } public void setField3(String field3) { this.field3 = field3; } @Override public String getField4() { return null; } public void setField5(String field5) { this.field5 = field5; } } private static class Delegate { private final String field1; private final int field2; private final Object field3; @JsonCreator public Delegate(String field1, int field2, Object field3) { this.field1 = field1; this.field2 = field2; this.field3 = field3; } public String getField1() { return field1; } public int getField2() { return field2; } public Object getField3() { return field3; } } private static class Delegator { private final String field4; private final String field5; @JsonCreator(mode = JsonCreator.Mode.DELEGATING) public Delegator(Delegate delegate) { this.field4 = delegate.field1; this.field5 = delegate.field2 + delegate.field3.toString(); } public String getField4() { return field4; } public String getField5() { return field5; } } }