Skip to content

Commit

Permalink
Merge pull request #24 from nfl/map_support
Browse files Browse the repository at this point in the history
Added Map type support as a scalar
  • Loading branch information
vaant authored Nov 16, 2017
2 parents f380abf + 9a0bcbf commit 923209f
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 43 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ dependencies {
// Misc dependencies
compile("io.reactivex:rxjava:1.1.3")
compile("javax.validation:validation-api:1.1.0.Final")
compile("com.fasterxml.jackson.core:jackson-databind:2.6.2")

// Spock test dependencies
testCompile("junit:junit:4.12")
testCompile("cglib:cglib-nodep:3.2.4") // for spock mocking, need later version for Java 8
testCompile("org.spockframework:spock-core:1.0-groovy-2.4")
testCompile("org.codehaus.groovy:groovy-all:2.4.4")
testCompile("com.fasterxml.jackson.core:jackson-databind:2.6.2")
}

task testSpock(type: Test) {
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/nfl/glitr/GlitrBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ private Glitr buildGlitrWithRelaySupport() {
.withAnnotationToGraphQLOutputTypeMap(annotationToGraphQLOutputTypeMap)
.withAnnotationToDataFetcherFactoryMap(annotationToDataFetcherFactoryMap)
.withAnnotationToDataFetcherMap(annotationToDataFetcherMap)
.withJavaTypesDeclaredAsScalarMap(javaTypeDeclaredAsScalarMap)
.withOverrides(overrides)
// add the relay extra features
.withExplicitRelayNodeScan(relayConfig.isExplicitRelayNodeScanEnabled())
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/nfl/glitr/registry/TypeRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ private GraphQLArgument getGraphQLArgument(GlitrArgument arg) {
.name(arg.name())
.description(arg.description())
.type(inputType)
.defaultValue(arg.defaultValue().equalsIgnoreCase(GlitrArgument.NO_DEFAULT_VALUE) ? null : arg.defaultValue())
.build();
}

Expand Down Expand Up @@ -622,6 +623,10 @@ private GraphQLType getGraphQLTypeForOutputParameterizedType(Type type, String n

public Optional<GraphQLType> detectScalar(Type type, String name) {
// users can register their own GraphQLScalarTypes for given Java types
if(type instanceof ParameterizedType) {
type = ((ParameterizedType)type).getRawType();
}

if (javaTypeDeclaredAsScalarMap.containsKey(type)) {
return Optional.of(javaTypeDeclaredAsScalarMap.get(type));
}
Expand All @@ -643,6 +648,8 @@ else if (name != null && name.equals("id")) {
return Optional.of(Scalars.GraphQLDate);
} else if (type == ZonedDateTime.class || type == Instant.class) {
return Optional.of(Scalars.GraphQLDateTime);
} else if (type == Map.class) {
return Optional.of(Scalars.GraphQLMap);
}
// not a scalar
return Optional.empty();
Expand Down
76 changes: 75 additions & 1 deletion src/main/java/com/nfl/glitr/registry/type/Scalars.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
package com.nfl.glitr.registry.type;

import graphql.language.StringValue;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.language.*;
import graphql.schema.Coercing;
import graphql.schema.GraphQLScalarType;

import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

public class Scalars {

private static final String UTC = "UTC";
private static final String SHIELD_DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSX";
private static final String SHIELD_LOCAL_DATE_PATTERN = "yyyy-MM-dd";

private static final ObjectMapper om = new ObjectMapper();

/**
* {@code GraphQLDateTime} represents a date time as `2014-08-20T18:00:00.000Z`
Expand Down Expand Up @@ -141,6 +146,75 @@ public Object parseLiteral(Object input) {
}
});

public static final GraphQLScalarType GraphQLMap = new GraphQLScalarType("Map", "Object represented by map, " +
"where key is a property and value is a value of this property.", new Coercing() {

@Override
public Object serialize(Object input) {
if (input == null) {
return null;
}

if (input instanceof Map) {
return input;
}

if (input instanceof String) {
try {
return om.readValue((String) input, Map.class);
} catch (IOException e) {
throw new IllegalArgumentException("Can't serialize type "+input.getClass()+" with value "+input.toString());
}
}

throw new IllegalArgumentException("Can't serialize type "+input.getClass()+" with value "+input.toString());
}

@Override
public Object parseValue(Object input) {
return serialize(input);
}

/**
* Always parse to {@code Map}.
*
* @param input object string representation
* @return object as a {@code Map}
*/
@Override
public Object parseLiteral(Object input) {
if (!(input instanceof ObjectValue)) return null;
ObjectValue obj = (ObjectValue) input;
Map<String, Object> map = new HashMap<>();
for (ObjectField of : obj.getObjectFields()) {
map.put(of.getName(), parseVariableValue(of.getValue()));
}
return map;
}
});

private static Object parseVariableValue(Value val) {
if (val instanceof StringValue) {
return ((StringValue) val).getValue();
} else if (val instanceof IntValue) {
return ((IntValue) val).getValue();
} else if (val instanceof FloatValue) {
return ((FloatValue) val).getValue();
} else if (val instanceof BooleanValue) {
return ((BooleanValue) val).isValue();
} else if (val instanceof NullValue) {
return null;
} else if (val instanceof ObjectValue) {
Map<String, Object> map = new HashMap<>();
for (ObjectField of : ((ObjectValue) val).getObjectFields()) {
map.put(of.getName(), parseVariableValue(of.getValue()));
}
return map;
}

throw new IllegalArgumentException("Can't serialize value " + val);
}

private static Object formatAsLocalDate(Object input) {
try {
String time = (String) input;
Expand Down
3 changes: 1 addition & 2 deletions src/main/java/com/nfl/glitr/util/ReflectionUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ public static Boolean eligibleMethod(Method method) {
}

return (methodName.startsWith("is") || methodName.startsWith("get"))
&& method.getDeclaringClass() != Object.class
&& (!Map.class.isAssignableFrom(method.getReturnType()));
&& method.getDeclaringClass() != Object.class;
}

/**
Expand Down
122 changes: 85 additions & 37 deletions src/test/groovy/com/nfl/glitr/registry/type/ScalarsTest.groovy
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package com.nfl.glitr.registry.type

import graphql.language.BooleanValue
import graphql.language.FloatValue
import graphql.language.IntValue
import graphql.language.NullValue
import graphql.language.ObjectField
import graphql.language.ObjectValue
import graphql.language.StringValue
import spock.lang.Specification
import spock.lang.Unroll

import java.time.Instant
import java.time.LocalDate
Expand All @@ -11,77 +18,118 @@ public class ScalarsTest extends Specification {

def "DateTime parse literal"() {
expect:
Scalars.GraphQLDateTime.getCoercing().parseLiteral(literal) == result
Scalars.GraphQLDateTime.getCoercing().parseLiteral(literal) == result

where:
literal | result
new StringValue("2016-01-08T00:32:09.132Z") | Instant.parse("2016-01-08T00:32:09.132Z")
new StringValue("2016-01-08T00:32:09.132Z") | ZonedDateTime.parse("2016-01-08T00:32:09.132Z").toInstant()
null | null
literal | result
new StringValue("2016-01-08T00:32:09.132Z") | Instant.parse("2016-01-08T00:32:09.132Z")
new StringValue("2016-01-08T00:32:09.132Z") | ZonedDateTime.parse("2016-01-08T00:32:09.132Z").toInstant()
null | null
}

def "DateTime serialize/parseValue object"() {
expect:
Scalars.GraphQLDateTime.getCoercing().serialize(value) == result
Scalars.GraphQLDateTime.getCoercing().parseValue(value) == result
Scalars.GraphQLDateTime.getCoercing().serialize(value) == result
Scalars.GraphQLDateTime.getCoercing().parseValue(value) == result

where:
value | result
Instant.parse("2016-01-08T00:32:09.132Z") | "2016-01-08T00:32:09.132Z"
Instant.ofEpochMilli(1454362550000l) | "2016-02-01T21:35:50.000Z"
ZonedDateTime.parse("2016-01-08T00:32:09.132Z") | "2016-01-08T00:32:09.132Z"
"2016-01-08T00:32:09.132Z" | "2016-01-08T00:32:09.132Z"
null | null
value | result
Instant.parse("2016-01-08T00:32:09.132Z") | "2016-01-08T00:32:09.132Z"
Instant.ofEpochMilli(1454362550000l) | "2016-02-01T21:35:50.000Z"
ZonedDateTime.parse("2016-01-08T00:32:09.132Z") | "2016-01-08T00:32:09.132Z"
"2016-01-08T00:32:09.132Z" | "2016-01-08T00:32:09.132Z"
null | null
}

def "DateTime serialize/parseValue object unknown type"() {
setup:
def input = "Hello"
def input = "Hello"
when:
Scalars.GraphQLDateTime.getCoercing().serialize(input)
Scalars.GraphQLDateTime.getCoercing().parseValue(input)
Scalars.GraphQLDateTime.getCoercing().serialize(input)
Scalars.GraphQLDateTime.getCoercing().parseValue(input)

then:
def e = thrown(IllegalArgumentException)
e.getMessage() == "Failed to parse/serialize GraphQLDateTime with value "+input+". Value likely of an unsupported format."
def e = thrown(IllegalArgumentException)
e.getMessage() == "Failed to parse/serialize GraphQLDateTime with value " + input + ". Value likely of an unsupported format."
}


def "Date parse literal"() {
expect:
Scalars.GraphQLDate.getCoercing().parseLiteral(literal) == result
Scalars.GraphQLDate.getCoercing().parseLiteral(literal) == result

where:
literal | result
new StringValue("2016-01-08") | LocalDate.parse("2016-01-08")
null | null
literal | result
new StringValue("2016-01-08") | LocalDate.parse("2016-01-08")
null | null
}

def "Date serialize/parseValue object"() {
expect:
Scalars.GraphQLDate.getCoercing().serialize(value) == result
Scalars.GraphQLDate.getCoercing().parseValue(value) == result
Scalars.GraphQLDate.getCoercing().serialize(value) == result
Scalars.GraphQLDate.getCoercing().parseValue(value) == result

where:
value | result
Instant.parse("2016-01-08T00:32:09.132Z") | "2016-01-08"
Instant.ofEpochMilli(1454362550000l) | "2016-02-01"
ZonedDateTime.parse("2016-01-08T00:32:09.132Z") | "2016-01-08"
LocalDate.of(2015, 01, 01) | "2015-01-01"
"2016-01-08T00:32:09.132Z" | "2016-01-08"
"2016-01-08" | "2016-01-08"
null | null
value | result
Instant.parse("2016-01-08T00:32:09.132Z") | "2016-01-08"
Instant.ofEpochMilli(1454362550000l) | "2016-02-01"
ZonedDateTime.parse("2016-01-08T00:32:09.132Z") | "2016-01-08"
LocalDate.of(2015, 01, 01) | "2015-01-01"
"2016-01-08T00:32:09.132Z" | "2016-01-08"
"2016-01-08" | "2016-01-08"
null | null
}

def "Date serialize/parseValue object unknown type"() {
setup:
def input = "Hello"
def input = "Hello"
when:
Scalars.GraphQLDate.getCoercing().serialize(input)
Scalars.GraphQLDate.getCoercing().parseValue(input)
Scalars.GraphQLDate.getCoercing().serialize(input)
Scalars.GraphQLDate.getCoercing().parseValue(input)

then:
def e = thrown(IllegalArgumentException)
e.getMessage() == "Failed to parse/serialize GraphQLDate with value "+input+". Value likely of an unsupported format."
def e = thrown(IllegalArgumentException)
e.getMessage() == "Failed to parse/serialize GraphQLDate with value " + input + ". Value likely of an unsupported format."
}

@Unroll
def "Map parse literal"() {
expect:
Scalars.GraphQLMap.getCoercing().parseLiteral(literal) == result

where:
literal | result
new ObjectValue([new ObjectField("test1", new StringValue("test1")), new ObjectField("test2", new StringValue("test2"))]) | [test1: "test1", test2: "test2"]
new ObjectValue([new ObjectField("test1", new IntValue(BigInteger.ZERO)), new ObjectField("test2", new IntValue(BigInteger.ONE))]) | [test1: 0, test2: 1]
new ObjectValue([new ObjectField("test1", new IntValue(BigInteger.ZERO)), new ObjectField("test2", new IntValue(BigInteger.ONE))]) | [test1: 0, test2: 1]
new ObjectValue([new ObjectField("test1", new FloatValue(BigDecimal.ZERO)), new ObjectField("test2", new FloatValue(BigDecimal.ONE))]) | [test1: 0F, test2: 1F]
new ObjectValue([new ObjectField("test1", new BooleanValue(true)), new ObjectField("test2", new BooleanValue(false))]) | [test1: true, test2: false]
new ObjectValue([new ObjectField("test1", NullValue.Null), new ObjectField("test2", NullValue.Null)]) | [test1: null, test2: null]
null | null
}

@Unroll
def "Map serialize/parseValue object"() {
expect:
Scalars.GraphQLMap.getCoercing().serialize(value) == result
Scalars.GraphQLMap.getCoercing().parseValue(value) == result

where:
value | result
[test1: "test1", test2: "test2"] | [test1: "test1", test2: "test2"]
'{"test1": "test1", "test2": "test2"}' | [test1: "test1", test2: "test2"]
null | null
}

def "Map serialize/parseValue object unknown type"() {
setup:
def input = "Hello"
when:
Scalars.GraphQLMap.getCoercing().serialize(input)
Scalars.GraphQLMap.getCoercing().parseValue(input)

then:
def e = thrown(IllegalArgumentException)
e.getMessage() == "Can't serialize type class java.lang.String with value $input"
}
}
4 changes: 2 additions & 2 deletions src/test/groovy/com/nfl/glitr/util/ReflectionUtilTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ class ReflectionUtilTest extends Specification {
"getTitle" || true
"getId" || true
"isValid" || true
"getMap" || true
"nonGetterMethod" || false
"getTitleWithGlitrIgnoreOnField" || false
"getTitleWithGlitrIgnoreOnGetter" || false
"getTitleWithGlitrIgnoreOnBothFieldAndGetter" || false
"getMap" || false
}

@SuppressWarnings("GroovyPointlessBoolean")
Expand All @@ -31,11 +31,11 @@ class ReflectionUtilTest extends Specification {
"getTitle" || true
"getId" || true
"isValid" || true
"getMap" || true
"nonGetterMethod" || false
"getTitleWithGlitrIgnoreOnField" || false
"getTitleWithGlitrIgnoreOnGetter" || false
"getTitleWithGlitrIgnoreOnBothFieldAndGetter" || false
"getMap" || false
}

class Video {
Expand Down

0 comments on commit 923209f

Please sign in to comment.