strictMode for RuntimeWiring and TypeRuntimeWiring by bbakerman · Pull Request #3565 · graphql-java/graphql-java · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions src/main/java/graphql/schema/idl/RuntimeWiring.java
51 changes: 51 additions & 0 deletions src/main/java/graphql/schema/idl/TypeRuntimeWiring.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
import graphql.schema.DataFetcher;
import graphql.schema.GraphQLSchema;
import graphql.schema.TypeResolver;
import graphql.schema.idl.errors.StrictModeWiringException;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.UnaryOperator;

import static graphql.Assert.assertNotNull;
import static java.lang.String.format;

/**
* A type runtime wiring is a specification of the data fetchers and possible type resolver for a given type name.
Expand All @@ -18,6 +21,28 @@
*/
@PublicApi
public class TypeRuntimeWiring {

private final static AtomicBoolean DEFAULT_STRICT_MODE = new AtomicBoolean(false);

/**
* By default {@link TypeRuntimeWiring} builders are not in strict mode, but you can set a JVM wide value
* so that any created will be.
*
* @param strictMode the desired strict mode state
*
* @see Builder#strictMode()
*/
public static void setStrictModeJvmWide(boolean strictMode) {
DEFAULT_STRICT_MODE.set(strictMode);
}

/**
* @return the current JVM wide state of strict mode
*/
public static boolean getStrictModeJvmWide() {
return DEFAULT_STRICT_MODE.get();
}

private final String typeName;
private final DataFetcher defaultDataFetcher;
private final Map<String, DataFetcher> fieldDataFetchers;
Expand Down Expand Up @@ -82,6 +107,7 @@ public static class Builder {
private DataFetcher defaultDataFetcher;
private TypeResolver typeResolver;
private EnumValuesProvider enumValuesProvider;
private boolean strictMode = DEFAULT_STRICT_MODE.get();

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

/**
* This puts the builder into strict mode, so if things get defined twice, for example, it
* will throw a {@link StrictModeWiringException}.
*
* @return this builder
*/
public Builder strictMode() {
this.strictMode = true;
return this;
}

/**
* Adds a data fetcher for the current type to the specified field
*
Expand All @@ -106,6 +143,9 @@ public Builder typeName(String typeName) {
public Builder dataFetcher(String fieldName, DataFetcher dataFetcher) {
assertNotNull(dataFetcher, () -> "you must provide a data fetcher");
assertNotNull(fieldName, () -> "you must tell us what field");
if (strictMode) {
assertFieldStrictly(fieldName);
}
fieldDataFetchers.put(fieldName, dataFetcher);
return this;
}
Expand All @@ -119,10 +159,21 @@ public Builder dataFetcher(String fieldName, DataFetcher dataFetcher) {
*/
public Builder dataFetchers(Map<String, DataFetcher> dataFetchersMap) {
assertNotNull(dataFetchersMap, () -> "you must provide a data fetchers map");
if (strictMode) {
dataFetchersMap.forEach((fieldName, df) -> {
assertFieldStrictly(fieldName);
});
}
fieldDataFetchers.putAll(dataFetchersMap);
return this;
}

private void assertFieldStrictly(String fieldName) {
if (fieldDataFetchers.containsKey(fieldName)) {
throw new StrictModeWiringException(format("The field %s already has a data fetcher defined", fieldName));
}
}

/**
* All fields in a type need a data fetcher of some sort and this method is called to provide the default data fetcher
* that will be used for this type if no specific one has been provided per field.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package graphql.schema.idl.errors;

import graphql.GraphQLException;
import graphql.PublicApi;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.TypeRuntimeWiring;

/**
* An exception that is throw when {@link RuntimeWiring.Builder#strictMode()} or {@link TypeRuntimeWiring.Builder#strictMode()} is true and
* something gets redefined.
*/
@PublicApi
public class StrictModeWiringException extends GraphQLException {
public StrictModeWiringException(String msg) {
super(msg);
}
}
75 changes: 61 additions & 14 deletions src/test/groovy/graphql/schema/idl/RuntimeWiringTest.groovy
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package graphql.schema.idl

import graphql.Scalars
import graphql.TypeResolutionEnvironment
import graphql.schema.Coercing
import graphql.schema.DataFetcher
Expand All @@ -9,6 +10,7 @@ import graphql.schema.GraphQLFieldsContainer
import graphql.schema.GraphQLObjectType
import graphql.schema.GraphQLScalarType
import graphql.schema.TypeResolver
import graphql.schema.idl.errors.StrictModeWiringException
import graphql.schema.visibility.GraphqlFieldVisibility
import spock.lang.Specification

Expand Down Expand Up @@ -62,22 +64,22 @@ class RuntimeWiringTest extends Specification {
def "basic call structure"() {
def wiring = RuntimeWiring.newRuntimeWiring()
.type("Query", { type ->
type
.dataFetcher("fieldX", new NamedDF("fieldX"))
.dataFetcher("fieldY", new NamedDF("fieldY"))
.dataFetcher("fieldZ", new NamedDF("fieldZ"))
.defaultDataFetcher(new NamedDF("defaultQueryDF"))
.typeResolver(new NamedTR("typeResolver4Query"))
} as UnaryOperator<TypeRuntimeWiring.Builder>)
type
.dataFetcher("fieldX", new NamedDF("fieldX"))
.dataFetcher("fieldY", new NamedDF("fieldY"))
.dataFetcher("fieldZ", new NamedDF("fieldZ"))
.defaultDataFetcher(new NamedDF("defaultQueryDF"))
.typeResolver(new NamedTR("typeResolver4Query"))
} as UnaryOperator<TypeRuntimeWiring.Builder>)

.type("Mutation", { type ->
type
.dataFetcher("fieldX", new NamedDF("mfieldX"))
.dataFetcher("fieldY", new NamedDF("mfieldY"))
.dataFetcher("fieldZ", new NamedDF("mfieldZ"))
.defaultDataFetcher(new NamedDF("defaultMutationDF"))
.typeResolver(new NamedTR("typeResolver4Mutation"))
} as UnaryOperator<TypeRuntimeWiring.Builder>)
type
.dataFetcher("fieldX", new NamedDF("mfieldX"))
.dataFetcher("fieldY", new NamedDF("mfieldY"))
.dataFetcher("fieldZ", new NamedDF("mfieldZ"))
.defaultDataFetcher(new NamedDF("defaultMutationDF"))
.typeResolver(new NamedTR("typeResolver4Mutation"))
} as UnaryOperator<TypeRuntimeWiring.Builder>)
.build()


Expand Down Expand Up @@ -190,4 +192,49 @@ class RuntimeWiringTest extends Specification {
newWiring.scalars["Custom2"] == customScalar2
newWiring.fieldVisibility == fieldVisibility
}

def "strict mode can stop certain redefinitions"() {
DataFetcher DF1 = env -> "x"
TypeResolver TR1 = env -> null
EnumValuesProvider EVP1 = name -> null

when:
RuntimeWiring.newRuntimeWiring()
.strictMode()
.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("foo", DF1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("bar", DF1))


then:
def e1 = thrown(StrictModeWiringException)
e1.message == "The type Foo has already been defined"

when:
RuntimeWiring.newRuntimeWiring()
.strictMode()
.type(TypeRuntimeWiring.newTypeWiring("Foo").typeResolver(TR1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").typeResolver(TR1))

then:
def e2 = thrown(StrictModeWiringException)
e2.message == "The type Foo already has a type resolver defined"

when:
RuntimeWiring.newRuntimeWiring()
.strictMode()
.type(TypeRuntimeWiring.newTypeWiring("Foo").enumValues(EVP1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").enumValues(EVP1))
then:
def e3 = thrown(StrictModeWiringException)
e3.message == "The type Foo already has a enum provider defined"

when:
RuntimeWiring.newRuntimeWiring()
.strictMode()
.scalar(Scalars.GraphQLString)
then:
def e4 = thrown(StrictModeWiringException)
e4.message == "The scalar String is already defined"

}
}
88 changes: 88 additions & 0 deletions src/test/groovy/graphql/schema/idl/TypeRuntimeWiringTest.groovy