Skip to content

Commit 1d78326

Browse files
authored
Merge pull request #3565 from graphql-java/strict-mode-runtime-wiring
strictMode for RuntimeWiring and TypeRuntimeWiring
2 parents 7f13678 + bb82efb commit 1d78326

File tree

5 files changed

+243
-16
lines changed

5 files changed

+243
-16
lines changed

src/main/java/graphql/schema/idl/RuntimeWiring.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
import graphql.schema.GraphQLSchema;
88
import graphql.schema.GraphqlTypeComparatorRegistry;
99
import graphql.schema.TypeResolver;
10+
import graphql.schema.idl.errors.StrictModeWiringException;
1011
import graphql.schema.visibility.GraphqlFieldVisibility;
1112

1213
import java.util.ArrayList;
13-
import java.util.Collection;
1414
import java.util.LinkedHashMap;
1515
import java.util.List;
1616
import java.util.Map;
@@ -19,6 +19,7 @@
1919

2020
import static graphql.Assert.assertNotNull;
2121
import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY;
22+
import static java.lang.String.format;
2223

2324
/**
2425
* A runtime wiring is a specification of data fetchers, type resolvers and custom scalars that are needed
@@ -161,6 +162,7 @@ public static class Builder {
161162
private final Map<String, SchemaDirectiveWiring> registeredDirectiveWiring = new LinkedHashMap<>();
162163
private final List<SchemaDirectiveWiring> directiveWiring = new ArrayList<>();
163164
private WiringFactory wiringFactory = new NoopWiringFactory();
165+
private boolean strictMode = false;
164166
private GraphqlFieldVisibility fieldVisibility = DEFAULT_FIELD_VISIBILITY;
165167
private GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry().build();
166168
private GraphqlTypeComparatorRegistry comparatorRegistry = GraphqlTypeComparatorRegistry.AS_IS_REGISTRY;
@@ -169,6 +171,16 @@ private Builder() {
169171
ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS.forEach(this::scalar);
170172
}
171173

174+
/**
175+
* This puts the builder into strict mode, so if things get defined twice, for example, it will throw a {@link StrictModeWiringException}.
176+
*
177+
* @return this builder
178+
*/
179+
public Builder strictMode() {
180+
this.strictMode = true;
181+
return this;
182+
}
183+
172184
/**
173185
* Adds a wiring factory into the runtime wiring
174186
*
@@ -214,6 +226,9 @@ public Builder codeRegistry(GraphQLCodeRegistry.Builder codeRegistry) {
214226
* @return the runtime wiring builder
215227
*/
216228
public Builder scalar(GraphQLScalarType scalarType) {
229+
if (strictMode && scalars.containsKey(scalarType.getName())) {
230+
throw new StrictModeWiringException(format("The scalar %s is already defined", scalarType.getName()));
231+
}
217232
scalars.put(scalarType.getName(), scalarType);
218233
return this;
219234
}
@@ -264,17 +279,26 @@ public Builder type(String typeName, UnaryOperator<TypeRuntimeWiring.Builder> bu
264279
public Builder type(TypeRuntimeWiring typeRuntimeWiring) {
265280
String typeName = typeRuntimeWiring.getTypeName();
266281
Map<String, DataFetcher> typeDataFetchers = dataFetchers.computeIfAbsent(typeName, k -> new LinkedHashMap<>());
267-
typeRuntimeWiring.getFieldDataFetchers().forEach(typeDataFetchers::put);
282+
if (strictMode && !typeDataFetchers.isEmpty()) {
283+
throw new StrictModeWiringException(format("The type %s has already been defined", typeName));
284+
}
285+
typeDataFetchers.putAll(typeRuntimeWiring.getFieldDataFetchers());
268286

269287
defaultDataFetchers.put(typeName, typeRuntimeWiring.getDefaultDataFetcher());
270288

271289
TypeResolver typeResolver = typeRuntimeWiring.getTypeResolver();
272290
if (typeResolver != null) {
291+
if (strictMode && this.typeResolvers.containsKey(typeName)) {
292+
throw new StrictModeWiringException(format("The type %s already has a type resolver defined", typeName));
293+
}
273294
this.typeResolvers.put(typeName, typeResolver);
274295
}
275296

276297
EnumValuesProvider enumValuesProvider = typeRuntimeWiring.getEnumValuesProvider();
277298
if (enumValuesProvider != null) {
299+
if (strictMode && this.enumValuesProviders.containsKey(typeName)) {
300+
throw new StrictModeWiringException(format("The type %s already has a enum provider defined", typeName));
301+
}
278302
this.enumValuesProviders.put(typeName, enumValuesProvider);
279303
}
280304
return this;

src/main/java/graphql/schema/idl/TypeRuntimeWiring.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
import graphql.schema.DataFetcher;
55
import graphql.schema.GraphQLSchema;
66
import graphql.schema.TypeResolver;
7+
import graphql.schema.idl.errors.StrictModeWiringException;
78

89
import java.util.LinkedHashMap;
910
import java.util.Map;
11+
import java.util.concurrent.atomic.AtomicBoolean;
1012
import java.util.function.UnaryOperator;
1113

1214
import static graphql.Assert.assertNotNull;
15+
import static java.lang.String.format;
1316

1417
/**
1518
* A type runtime wiring is a specification of the data fetchers and possible type resolver for a given type name.
@@ -18,6 +21,28 @@
1821
*/
1922
@PublicApi
2023
public class TypeRuntimeWiring {
24+
25+
private final static AtomicBoolean DEFAULT_STRICT_MODE = new AtomicBoolean(false);
26+
27+
/**
28+
* By default {@link TypeRuntimeWiring} builders are not in strict mode, but you can set a JVM wide value
29+
* so that any created will be.
30+
*
31+
* @param strictMode the desired strict mode state
32+
*
33+
* @see Builder#strictMode()
34+
*/
35+
public static void setStrictModeJvmWide(boolean strictMode) {
36+
DEFAULT_STRICT_MODE.set(strictMode);
37+
}
38+
39+
/**
40+
* @return the current JVM wide state of strict mode
41+
*/
42+
public static boolean getStrictModeJvmWide() {
43+
return DEFAULT_STRICT_MODE.get();
44+
}
45+
2146
private final String typeName;
2247
private final DataFetcher defaultDataFetcher;
2348
private final Map<String, DataFetcher> fieldDataFetchers;
@@ -82,6 +107,7 @@ public static class Builder {
82107
private DataFetcher defaultDataFetcher;
83108
private TypeResolver typeResolver;
84109
private EnumValuesProvider enumValuesProvider;
110+
private boolean strictMode = DEFAULT_STRICT_MODE.get();
85111

86112
/**
87113
* Sets the type name for this type wiring. You MUST set this.
@@ -95,6 +121,17 @@ public Builder typeName(String typeName) {
95121
return this;
96122
}
97123

124+
/**
125+
* This puts the builder into strict mode, so if things get defined twice, for example, it
126+
* will throw a {@link StrictModeWiringException}.
127+
*
128+
* @return this builder
129+
*/
130+
public Builder strictMode() {
131+
this.strictMode = true;
132+
return this;
133+
}
134+
98135
/**
99136
* Adds a data fetcher for the current type to the specified field
100137
*
@@ -106,6 +143,9 @@ public Builder typeName(String typeName) {
106143
public Builder dataFetcher(String fieldName, DataFetcher dataFetcher) {
107144
assertNotNull(dataFetcher, () -> "you must provide a data fetcher");
108145
assertNotNull(fieldName, () -> "you must tell us what field");
146+
if (strictMode) {
147+
assertFieldStrictly(fieldName);
148+
}
109149
fieldDataFetchers.put(fieldName, dataFetcher);
110150
return this;
111151
}
@@ -119,10 +159,21 @@ public Builder dataFetcher(String fieldName, DataFetcher dataFetcher) {
119159
*/
120160
public Builder dataFetchers(Map<String, DataFetcher> dataFetchersMap) {
121161
assertNotNull(dataFetchersMap, () -> "you must provide a data fetchers map");
162+
if (strictMode) {
163+
dataFetchersMap.forEach((fieldName, df) -> {
164+
assertFieldStrictly(fieldName);
165+
});
166+
}
122167
fieldDataFetchers.putAll(dataFetchersMap);
123168
return this;
124169
}
125170

171+
private void assertFieldStrictly(String fieldName) {
172+
if (fieldDataFetchers.containsKey(fieldName)) {
173+
throw new StrictModeWiringException(format("The field %s already has a data fetcher defined", fieldName));
174+
}
175+
}
176+
126177
/**
127178
* All fields in a type need a data fetcher of some sort and this method is called to provide the default data fetcher
128179
* that will be used for this type if no specific one has been provided per field.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package graphql.schema.idl.errors;
2+
3+
import graphql.GraphQLException;
4+
import graphql.PublicApi;
5+
import graphql.schema.idl.RuntimeWiring;
6+
import graphql.schema.idl.TypeRuntimeWiring;
7+
8+
/**
9+
* An exception that is throw when {@link RuntimeWiring.Builder#strictMode()} or {@link TypeRuntimeWiring.Builder#strictMode()} is true and
10+
* something gets redefined.
11+
*/
12+
@PublicApi
13+
public class StrictModeWiringException extends GraphQLException {
14+
public StrictModeWiringException(String msg) {
15+
super(msg);
16+
}
17+
}

src/test/groovy/graphql/schema/idl/RuntimeWiringTest.groovy

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package graphql.schema.idl
22

3+
import graphql.Scalars
34
import graphql.TypeResolutionEnvironment
45
import graphql.schema.Coercing
56
import graphql.schema.DataFetcher
@@ -9,6 +10,7 @@ import graphql.schema.GraphQLFieldsContainer
910
import graphql.schema.GraphQLObjectType
1011
import graphql.schema.GraphQLScalarType
1112
import graphql.schema.TypeResolver
13+
import graphql.schema.idl.errors.StrictModeWiringException
1214
import graphql.schema.visibility.GraphqlFieldVisibility
1315
import spock.lang.Specification
1416

@@ -62,22 +64,22 @@ class RuntimeWiringTest extends Specification {
6264
def "basic call structure"() {
6365
def wiring = RuntimeWiring.newRuntimeWiring()
6466
.type("Query", { type ->
65-
type
66-
.dataFetcher("fieldX", new NamedDF("fieldX"))
67-
.dataFetcher("fieldY", new NamedDF("fieldY"))
68-
.dataFetcher("fieldZ", new NamedDF("fieldZ"))
69-
.defaultDataFetcher(new NamedDF("defaultQueryDF"))
70-
.typeResolver(new NamedTR("typeResolver4Query"))
71-
} as UnaryOperator<TypeRuntimeWiring.Builder>)
67+
type
68+
.dataFetcher("fieldX", new NamedDF("fieldX"))
69+
.dataFetcher("fieldY", new NamedDF("fieldY"))
70+
.dataFetcher("fieldZ", new NamedDF("fieldZ"))
71+
.defaultDataFetcher(new NamedDF("defaultQueryDF"))
72+
.typeResolver(new NamedTR("typeResolver4Query"))
73+
} as UnaryOperator<TypeRuntimeWiring.Builder>)
7274

7375
.type("Mutation", { type ->
74-
type
75-
.dataFetcher("fieldX", new NamedDF("mfieldX"))
76-
.dataFetcher("fieldY", new NamedDF("mfieldY"))
77-
.dataFetcher("fieldZ", new NamedDF("mfieldZ"))
78-
.defaultDataFetcher(new NamedDF("defaultMutationDF"))
79-
.typeResolver(new NamedTR("typeResolver4Mutation"))
80-
} as UnaryOperator<TypeRuntimeWiring.Builder>)
76+
type
77+
.dataFetcher("fieldX", new NamedDF("mfieldX"))
78+
.dataFetcher("fieldY", new NamedDF("mfieldY"))
79+
.dataFetcher("fieldZ", new NamedDF("mfieldZ"))
80+
.defaultDataFetcher(new NamedDF("defaultMutationDF"))
81+
.typeResolver(new NamedTR("typeResolver4Mutation"))
82+
} as UnaryOperator<TypeRuntimeWiring.Builder>)
8183
.build()
8284

8385

@@ -190,4 +192,49 @@ class RuntimeWiringTest extends Specification {
190192
newWiring.scalars["Custom2"] == customScalar2
191193
newWiring.fieldVisibility == fieldVisibility
192194
}
195+
196+
def "strict mode can stop certain redefinitions"() {
197+
DataFetcher DF1 = env -> "x"
198+
TypeResolver TR1 = env -> null
199+
EnumValuesProvider EVP1 = name -> null
200+
201+
when:
202+
RuntimeWiring.newRuntimeWiring()
203+
.strictMode()
204+
.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("foo", DF1))
205+
.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("bar", DF1))
206+
207+
208+
then:
209+
def e1 = thrown(StrictModeWiringException)
210+
e1.message == "The type Foo has already been defined"
211+
212+
when:
213+
RuntimeWiring.newRuntimeWiring()
214+
.strictMode()
215+
.type(TypeRuntimeWiring.newTypeWiring("Foo").typeResolver(TR1))
216+
.type(TypeRuntimeWiring.newTypeWiring("Foo").typeResolver(TR1))
217+
218+
then:
219+
def e2 = thrown(StrictModeWiringException)
220+
e2.message == "The type Foo already has a type resolver defined"
221+
222+
when:
223+
RuntimeWiring.newRuntimeWiring()
224+
.strictMode()
225+
.type(TypeRuntimeWiring.newTypeWiring("Foo").enumValues(EVP1))
226+
.type(TypeRuntimeWiring.newTypeWiring("Foo").enumValues(EVP1))
227+
then:
228+
def e3 = thrown(StrictModeWiringException)
229+
e3.message == "The type Foo already has a enum provider defined"
230+
231+
when:
232+
RuntimeWiring.newRuntimeWiring()
233+
.strictMode()
234+
.scalar(Scalars.GraphQLString)
235+
then:
236+
def e4 = thrown(StrictModeWiringException)
237+
e4.message == "The scalar String is already defined"
238+
239+
}
193240
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package graphql.schema.idl
2+
3+
import graphql.schema.DataFetcher
4+
import graphql.schema.idl.errors.StrictModeWiringException
5+
import spock.lang.Specification
6+
7+
class TypeRuntimeWiringTest extends Specification {
8+
9+
void setup() {
10+
TypeRuntimeWiring.setStrictModeJvmWide(false)
11+
}
12+
13+
void cleanup() {
14+
TypeRuntimeWiring.setStrictModeJvmWide(false)
15+
}
16+
17+
DataFetcher DF1 = env -> "x"
18+
DataFetcher DF2 = env -> "y"
19+
20+
def "strict mode is off by default"() {
21+
when:
22+
def typeRuntimeWiring = TypeRuntimeWiring.newTypeWiring("Foo")
23+
.dataFetcher("foo", DF1)
24+
.dataFetcher("foo", DF2)
25+
.build()
26+
then:
27+
typeRuntimeWiring.getFieldDataFetchers().get("foo") == DF2
28+
}
29+
30+
def "strict mode can be turned on"() {
31+
when:
32+
TypeRuntimeWiring.newTypeWiring("Foo")
33+
.strictMode()
34+
.dataFetcher("foo", DF1)
35+
.dataFetcher("foo", DF2)
36+
.build()
37+
then:
38+
def e = thrown(StrictModeWiringException)
39+
e.message == "The field foo already has a data fetcher defined"
40+
}
41+
42+
def "strict mode can be turned on for maps of fields"() {
43+
when:
44+
TypeRuntimeWiring.newTypeWiring("Foo")
45+
.strictMode()
46+
.dataFetcher("foo", DF1)
47+
.dataFetchers(["foo": DF2])
48+
.build()
49+
then:
50+
def e = thrown(StrictModeWiringException)
51+
e.message == "The field foo already has a data fetcher defined"
52+
}
53+
54+
def "strict mode can be turned on JVM wide"() {
55+
56+
57+
when:
58+
def inStrictMode = TypeRuntimeWiring.getStrictModeJvmWide()
59+
then:
60+
!inStrictMode
61+
62+
63+
when:
64+
TypeRuntimeWiring.setStrictModeJvmWide(true)
65+
inStrictMode = TypeRuntimeWiring.getStrictModeJvmWide()
66+
67+
TypeRuntimeWiring.newTypeWiring("Foo")
68+
.dataFetcher("foo", DF1)
69+
.dataFetcher("foo", DF2)
70+
.build()
71+
then:
72+
inStrictMode
73+
def e = thrown(StrictModeWiringException)
74+
e.message == "The field foo already has a data fetcher defined"
75+
76+
when:
77+
TypeRuntimeWiring.setStrictModeJvmWide(false)
78+
inStrictMode = TypeRuntimeWiring.getStrictModeJvmWide()
79+
80+
TypeRuntimeWiring.newTypeWiring("Foo")
81+
.dataFetcher("foo", DF1)
82+
.dataFetcher("foo", DF2)
83+
.build()
84+
then:
85+
!inStrictMode
86+
noExceptionThrown()
87+
}
88+
}

0 commit comments

Comments
 (0)