Skip to content

Commit cc1aab6

Browse files
committed
graphql-java#296 - now with an AST from object support and tests
1 parent 0fb1e21 commit cc1aab6

File tree

4 files changed

+396
-7
lines changed

4 files changed

+396
-7
lines changed

src/main/java/graphql/introspection/Introspection.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package graphql.introspection;
22

33

4+
import graphql.language.AstHelper;
5+
import graphql.language.AstPrinter;
46
import graphql.schema.DataFetcher;
57
import graphql.schema.DataFetchingEnvironment;
68
import graphql.schema.GraphQLArgument;
@@ -11,6 +13,7 @@
1113
import graphql.schema.GraphQLFieldsContainer;
1214
import graphql.schema.GraphQLInputObjectField;
1315
import graphql.schema.GraphQLInputObjectType;
16+
import graphql.schema.GraphQLInputType;
1417
import graphql.schema.GraphQLInterfaceType;
1518
import graphql.schema.GraphQLList;
1619
import graphql.schema.GraphQLNonNull;
@@ -103,16 +106,20 @@ public Object get(DataFetchingEnvironment environment) {
103106
public Object get(DataFetchingEnvironment environment) {
104107
if (environment.getSource() instanceof GraphQLArgument) {
105108
GraphQLArgument inputField = environment.getSource();
106-
return inputField.getDefaultValue() != null ? inputField.getDefaultValue().toString() : null;
109+
return inputField.getDefaultValue() != null ? print(inputField.getDefaultValue(), inputField.getType()) : null;
107110
} else if (environment.getSource() instanceof GraphQLInputObjectField) {
108111
GraphQLInputObjectField inputField = environment.getSource();
109-
return inputField.getDefaultValue() != null ? inputField.getDefaultValue().toString() : null;
112+
return inputField.getDefaultValue() != null ? print(inputField.getDefaultValue(), inputField.getType()) : null;
110113
}
111114
return null;
112115
}
113116
}))
114117
.build();
115118

119+
private static String print(Object value, GraphQLInputType type) {
120+
return new AstPrinter().printAst(AstHelper.astFromValue(value, type));
121+
}
122+
116123

117124
public static GraphQLObjectType __Field = newObject()
118125
.name("__Field")
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package graphql.language;
2+
3+
import graphql.AssertException;
4+
import graphql.Scalars;
5+
import graphql.schema.GraphQLEnumType;
6+
import graphql.schema.GraphQLInputObjectField;
7+
import graphql.schema.GraphQLInputObjectType;
8+
import graphql.schema.GraphQLInputType;
9+
import graphql.schema.GraphQLList;
10+
import graphql.schema.GraphQLNonNull;
11+
import graphql.schema.GraphQLScalarType;
12+
import graphql.schema.GraphQLType;
13+
14+
import java.beans.BeanInfo;
15+
import java.beans.IntrospectionException;
16+
import java.beans.Introspector;
17+
import java.beans.PropertyDescriptor;
18+
import java.lang.reflect.InvocationTargetException;
19+
import java.lang.reflect.Method;
20+
import java.math.BigDecimal;
21+
import java.math.BigInteger;
22+
import java.util.ArrayList;
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
27+
public class AstHelper {
28+
29+
/*
30+
* Produces a GraphQL Value AST given a Java value.
31+
*
32+
* A GraphQL type must be provided, which will be used to interpret different
33+
* Java values.
34+
*
35+
* | Value | GraphQL Value |
36+
* | ------------- | -------------------- |
37+
* | Object | Input Object |
38+
* | Array | List |
39+
* | Boolean | Boolean |
40+
* | String | String / Enum Value |
41+
* | Number | Int / Float |
42+
* | Mixed | Enum Value |
43+
*/
44+
public static Value astFromValue(Object _value, GraphQLType type) {
45+
if (_value == null) {
46+
return null;
47+
}
48+
49+
if (type instanceof GraphQLNonNull) {
50+
GraphQLType wrappedType = ((GraphQLNonNull) type).getWrappedType();
51+
return astFromValue(_value, wrappedType);
52+
}
53+
54+
// Convert JavaScript array to GraphQL list. If the GraphQLType is a list, but
55+
// the value is not an array, convert the value using the list's item type.
56+
if (type instanceof GraphQLList) {
57+
GraphQLType itemType = ((GraphQLList) type).getWrappedType();
58+
if (_value instanceof Iterable) {
59+
Iterable iterable = (Iterable) _value;
60+
List<Value> valuesNodes = new ArrayList<>();
61+
for (Object item : iterable) {
62+
Value itemNode = astFromValue(item, itemType);
63+
if (itemNode != null) {
64+
valuesNodes.add(itemNode);
65+
}
66+
67+
}
68+
return new ArrayValue(valuesNodes);
69+
}
70+
return astFromValue(_value, itemType);
71+
}
72+
73+
// Populate the fields of the input object by creating ASTs from each value
74+
// in the JavaScript object according to the fields in the input type.
75+
if (type instanceof GraphQLInputObjectType) {
76+
Map mapValue = objToMap(_value);
77+
GraphQLInputObjectType objectType = (GraphQLInputObjectType) type;
78+
List<GraphQLInputObjectField> fields = objectType.getFields();
79+
List<ObjectField> fieldNodes = new ArrayList<>();
80+
fields.forEach(field -> {
81+
GraphQLInputType fieldType = field.getType();
82+
Value nodeValue = astFromValue(mapValue.get(field.getName()), fieldType);
83+
if (nodeValue != null) {
84+
85+
fieldNodes.add(new ObjectField(field.getName(), nodeValue));
86+
}
87+
});
88+
return new ObjectValue(fieldNodes);
89+
}
90+
91+
if (!(type instanceof GraphQLScalarType || type instanceof GraphQLEnumType)) {
92+
throw new AssertException("Must provide Input Type, cannot use: " + type.getClass());
93+
}
94+
95+
// Since value is an internally represented value, it must be serialized
96+
// to an externally represented value before converting into an AST.
97+
final Object serialized = serialize(type, _value);
98+
if (isNullish(serialized)) {
99+
return null;
100+
}
101+
102+
// Others serialize based on their corresponding JavaScript scalar types.
103+
if (serialized instanceof Boolean) {
104+
return new BooleanValue((Boolean) serialized);
105+
}
106+
107+
String stringValue = serialized.toString();
108+
// numbers can be Int or Float values.
109+
if (serialized instanceof Number) {
110+
if (stringValue.matches("^[0-9]+$")) {
111+
return new IntValue(new BigInteger(stringValue));
112+
} else {
113+
return new FloatValue(new BigDecimal(stringValue));
114+
}
115+
}
116+
117+
if (serialized instanceof String) {
118+
// Enum types use Enum literals.
119+
if (type instanceof GraphQLEnumType) {
120+
return new EnumValue(stringValue);
121+
}
122+
123+
// ID types can use Int literals.
124+
if (type == Scalars.GraphQLID && stringValue.matches("^[0-9]+$")) {
125+
return new IntValue(new BigInteger(stringValue));
126+
}
127+
128+
return new StringValue(jsonStringify(stringValue));
129+
}
130+
131+
throw new AssertException("'Cannot convert value to AST: " + serialized);
132+
}
133+
134+
private static String jsonStringify(String stringValue) {
135+
stringValue = stringValue.replace("\"", "\\\"");
136+
stringValue = stringValue.replace("\\", "\\\\");
137+
stringValue = stringValue.replace("/", "\\/");
138+
stringValue = stringValue.replace("\f", "\\f");
139+
stringValue = stringValue.replace("\n", "\\n");
140+
stringValue = stringValue.replace("\r", "\\r");
141+
stringValue = stringValue.replace("\t", "\\t");
142+
return stringValue;
143+
}
144+
145+
private static Object serialize(GraphQLType type, Object value) {
146+
if (type instanceof GraphQLScalarType) {
147+
return ((GraphQLScalarType) type).getCoercing().serialize(value);
148+
} else {
149+
return ((GraphQLEnumType) type).getCoercing().serialize(value);
150+
}
151+
}
152+
153+
private static boolean isNullish(Object serialized) {
154+
if (serialized instanceof Number) {
155+
return Double.isNaN(((Number) serialized).doubleValue());
156+
}
157+
return serialized == null;
158+
}
159+
160+
private static Map objToMap(Object value) {
161+
if (value instanceof Map) {
162+
return (Map) value;
163+
}
164+
// java bean inspector
165+
Map<String, Object> result = new HashMap<>();
166+
try {
167+
BeanInfo info = Introspector.getBeanInfo(value.getClass());
168+
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
169+
Method reader = pd.getReadMethod();
170+
if (reader != null)
171+
result.put(pd.getName(), reader.invoke(value));
172+
}
173+
} catch (IntrospectionException | InvocationTargetException | IllegalAccessException e) {
174+
throw new RuntimeException(e);
175+
}
176+
return result;
177+
}
178+
}

src/test/groovy/graphql/Issue296.groovy

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import static graphql.schema.GraphQLSchema.newSchema
1212

1313
class Issue296 extends Specification {
1414

15-
def "test introspection for #296"() {
15+
def "test introspection for #296 with map"() {
1616

1717
def graphql = GraphQL.newGraphQL(newSchema()
1818
.query(newObject()
@@ -28,14 +28,57 @@ class Issue296 extends Specification {
2828
.name("inputField")
2929
.type(GraphQLString))
3030
.build())
31-
.defaultValue([field1:'value1']))))
31+
.defaultValue([inputField: 'value1']))))
3232
.build())
3333
.build()
3434

3535
def query = '{ __type(name: "Query") { fields { args { defaultValue } } } }'
3636

37-
// Instead of `'{field: "value"}'` you get '{field1=value1}' (#toString())
3837
expect:
39-
graphql.execute(query).data == [ __type: [ fields: [ [ args: [ [ defaultValue: '{field: "value"}' ] ] ] ] ] ]
38+
// converts the default object value to AST, then graphql pretty prints that as the value
39+
graphql.execute(query).data ==
40+
[__type: [fields: [[args: [[defaultValue: '{inputField : "value1"}']]]]]]
4041
}
41-
}
42+
43+
class FooBar {
44+
final String inputField = "foo"
45+
final String bar = "bar"
46+
47+
String getInputField() {
48+
return inputField
49+
}
50+
51+
String getBar() {
52+
return bar
53+
}
54+
}
55+
56+
def "test introspection for #296 with some object"() {
57+
58+
def graphql = GraphQL.newGraphQL(newSchema()
59+
.query(newObject()
60+
.name("Query")
61+
.field(newFieldDefinition()
62+
.name("field")
63+
.type(GraphQLString)
64+
.argument(newArgument()
65+
.name("argument")
66+
.type(newInputObject()
67+
.name("InputObjectType")
68+
.field(newInputObjectField()
69+
.name("inputField")
70+
.type(GraphQLString))
71+
.build())
72+
.defaultValue(new FooBar()))))
73+
.build())
74+
.build()
75+
76+
def query = '{ __type(name: "Query") { fields { args { defaultValue } } } }'
77+
78+
expect:
79+
// converts the default object value to AST, then graphql pretty prints that as the value
80+
graphql.execute(query).data ==
81+
[__type: [fields: [[args: [[defaultValue: '{inputField : "foo"}']]]]]]
82+
}
83+
}
84+

0 commit comments

Comments
 (0)