Skip to content

Commit

Permalink
Supporting other primitives as backing types for enums (#612)
Browse files Browse the repository at this point in the history
* Supporting other primitives as backing types for enums

* Add IT to test serializing int enum to json

* Reverting version bump

* Add serialization test to match deserialization test

* Missing file
  • Loading branch information
jcogilvie authored and joelittlejohn committed Aug 26, 2016
1 parent 0c2de26 commit 528eb22
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.jsonschema2pojo.rules;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.sun.codemodel.ClassType;
import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JBlock;
Expand All @@ -27,6 +28,7 @@
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JEnumConstant;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JForEach;
import com.sun.codemodel.JInvocation;
Expand Down Expand Up @@ -72,7 +74,7 @@ public class EnumRule implements Rule<JClassContainer, JType> {
private static final String VALUE_FIELD_NAME = "value";

private final RuleFactory ruleFactory;

protected EnumRule(RuleFactory ruleFactory) {
this.ruleFactory = ruleFactory;
}
Expand Down Expand Up @@ -118,11 +120,28 @@ public JType apply(String nodeName, JsonNode node, JClassContainer container, Sc
addInterfaces(_enum, node.get("javaInterfaces"));
}
addGeneratedAnnotation(_enum);

JFieldVar valueField = addValueField(_enum);
addToString(_enum, valueField);
addEnumConstants(node.path("enum"), _enum, node.path("javaEnumNames"));
addFactoryMethod(_enum);

// copy our node; remove the javaType as it will throw off the TypeRule for our case
ObjectNode typeNode = (ObjectNode)node.deepCopy();
typeNode.remove("javaType");

// If type is specified on the enum, get a type rule for it. Otherwise, we're a string.
// (This is different from the default of Object, which is why we don't do this for every case.)
JType backingType = node.has("type") ?
ruleFactory.getTypeRule().apply(nodeName, typeNode, container, schema) :
container.owner().ref(String.class);

JFieldVar valueField = addValueField(_enum, backingType);

// override toString only if we have a sensible string to return
if(isString(backingType)){
addToString(_enum, valueField);
}

addValueMethod(_enum, valueField);

addEnumConstants(node.path("enum"), _enum, node.path("javaEnumNames"), backingType);
addFactoryMethod(_enum, backingType);

return _enum;
}
Expand Down Expand Up @@ -157,11 +176,11 @@ private JDefinedClass createEnum(JsonNode node, String nodeName, JClassContainer
}
}

private void addFactoryMethod(JDefinedClass _enum) {
JFieldVar quickLookupMap = addQuickLookupMap(_enum);
private void addFactoryMethod(JDefinedClass _enum, JType backingType) {
JFieldVar quickLookupMap = addQuickLookupMap(_enum, backingType);

JMethod fromValue = _enum.method(JMod.PUBLIC | JMod.STATIC, _enum, "fromValue");
JVar valueParam = fromValue.param(String.class, "value");
JVar valueParam = fromValue.param(backingType, "value");

JBlock body = fromValue.body();
JVar constant = body.decl(_enum, "constant");
Expand All @@ -170,19 +189,26 @@ private void addFactoryMethod(JDefinedClass _enum) {
JConditional _if = body._if(constant.eq(JExpr._null()));

JInvocation illegalArgumentException = JExpr._new(_enum.owner().ref(IllegalArgumentException.class));
illegalArgumentException.arg(valueParam);
JExpression expr = valueParam;

// if string no need to add ""
if(!isString(backingType)){
expr = expr.plus(JExpr.lit(""));
}

illegalArgumentException.arg(expr);
_if._then()._throw(illegalArgumentException);
_if._else()._return(constant);

ruleFactory.getAnnotator().enumCreatorMethod(fromValue);
}

private JFieldVar addQuickLookupMap(JDefinedClass _enum) {
private JFieldVar addQuickLookupMap(JDefinedClass _enum, JType backingType) {

JClass lookupType = _enum.owner().ref(Map.class).narrow(_enum.owner().ref(String.class), _enum);
JClass lookupType = _enum.owner().ref(Map.class).narrow(backingType.boxify(), _enum);
JFieldVar lookupMap = _enum.field(JMod.PRIVATE | JMod.STATIC | JMod.FINAL, lookupType, "CONSTANTS");

JClass lookupImplType = _enum.owner().ref(HashMap.class).narrow(_enum.owner().ref(String.class), _enum);
JClass lookupImplType = _enum.owner().ref(HashMap.class).narrow(backingType.boxify(), _enum);
lookupMap.init(JExpr._new(lookupImplType));

JForEach forEach = _enum.init().forEach(_enum, "c", JExpr.invoke("values"));
Expand All @@ -193,11 +219,11 @@ private JFieldVar addQuickLookupMap(JDefinedClass _enum) {
return lookupMap;
}

private JFieldVar addValueField(JDefinedClass _enum) {
JFieldVar valueField = _enum.field(JMod.PRIVATE | JMod.FINAL, String.class, VALUE_FIELD_NAME);
private JFieldVar addValueField(JDefinedClass _enum, JType type) {
JFieldVar valueField = _enum.field(JMod.PRIVATE | JMod.FINAL, type, VALUE_FIELD_NAME);

JMethod constructor = _enum.constructor(JMod.PRIVATE);
JVar valueParam = constructor.param(String.class, VALUE_FIELD_NAME);
JVar valueParam = constructor.param(type, VALUE_FIELD_NAME);
JBlock body = constructor.body();
body.assign(JExpr._this().ref(valueField), valueParam);

Expand All @@ -208,13 +234,30 @@ private void addToString(JDefinedClass _enum, JFieldVar valueField) {
JMethod toString = _enum.method(JMod.PUBLIC, String.class, "toString");
JBlock body = toString.body();

body._return(JExpr._this().ref(valueField));
JExpression toReturn = JExpr._this().ref(valueField);
if(!isString(valueField.type())){
toReturn = toReturn.plus(JExpr.lit(""));
}

body._return(toReturn);

ruleFactory.getAnnotator().enumValueMethod(toString);
toString.annotate(Override.class);
}

private void addValueMethod(JDefinedClass _enum, JFieldVar valueField) {
JMethod fromValue = _enum.method(JMod.PUBLIC, valueField.type(), "value");

private void addEnumConstants(JsonNode node, JDefinedClass _enum, JsonNode customNames) {
JBlock body = fromValue.body();
body._return(JExpr._this().ref(valueField));

ruleFactory.getAnnotator().enumValueMethod(fromValue);
}

private boolean isString(JType type){
return type.fullName().equals(String.class.getName());
}

private void addEnumConstants(JsonNode node, JDefinedClass _enum, JsonNode customNames, JType type) {
Collection<String> existingConstantNames = new ArrayList<String>();
for (int i = 0; i < node.size(); i++) {
JsonNode value = node.path(i);
Expand All @@ -225,7 +268,20 @@ private void addEnumConstants(JsonNode node, JDefinedClass _enum, JsonNode custo
existingConstantNames.add(constantName);

JEnumConstant constant = _enum.enumConstant(constantName);
constant.arg(JExpr.lit(value.asText()));

String typeName = type.unboxify().fullName();
if(typeName.equals("int")){ // integer
constant.arg(JExpr.lit(value.intValue()));
} else if(typeName.equals("long")){ // integer-as-long
constant.arg(JExpr.lit(value.longValue()));
} else if(typeName.equals("double")){ // number
constant.arg(JExpr.lit(value.doubleValue()));
} else if(typeName.equals("boolean")){ // boolean
constant.arg(JExpr.lit(value.booleanValue()));
} else { // string, null, array, object?
// only string should really be valid here... TODO throw error?
constant.arg(JExpr.lit(value.asText()));
}
ruleFactory.getAnnotator().enumConstant(constant, value.asText());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ public class EnumRuleTest {
private NameHelper nameHelper = mock(NameHelper.class);
private Annotator annotator = mock(Annotator.class);
private RuleFactory ruleFactory = mock(RuleFactory.class);
private TypeRule typeRule = mock(TypeRule.class);

private EnumRule rule = new EnumRule(ruleFactory);

@Before
public void wireUpConfig() {
when(ruleFactory.getNameHelper()).thenReturn(nameHelper);
when(ruleFactory.getAnnotator()).thenReturn(annotator);
when(ruleFactory.getTypeRule()).thenReturn(typeRule);
}

@Test
Expand All @@ -71,6 +73,10 @@ public void applyGeneratesUniqueEnumNamesForMultipleEnumNodesWithSameName() {
ObjectNode enumNode = objectMapper.createObjectNode();
enumNode.put("type", "string");
enumNode.put("enum", arrayNode);

// We're always a string for the purposes of this test
when(typeRule.apply("status", enumNode, jpackage, schema))
.thenReturn(jpackage.owner()._ref(String.class));

JType result1 = rule.apply("status", enumNode, jpackage, schema);
JType result2 = rule.apply("status", enumNode, jpackage, schema);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.hamcrest.core.IsInstanceOf;
import org.jsonschema2pojo.integration.util.Jsonschema2PojoRule;
import org.junit.BeforeClass;
import org.junit.ClassRule;
Expand All @@ -33,6 +34,8 @@

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

Expand Down Expand Up @@ -79,7 +82,7 @@ public void enumClassIncludesCorrectlyNamedConstants() {
@Test
public void enumContainsWorkingAnnotatedSerializationMethod() throws NoSuchMethodException {

Method toString = enumClass.getMethod("toString");
Method toString = enumClass.getMethod("value");

assertThat(enumClass.getEnumConstants()[0].toString(), is("one"));
assertThat(enumClass.getEnumConstants()[1].toString(), is("secondOne"));
Expand Down Expand Up @@ -128,7 +131,33 @@ public void enumAtRootCreatesATopLevelType() throws ClassNotFoundException, NoSu
assertThat(isPublic(rootEnumClass.getModifiers()), is(true));

}

@Test
@SuppressWarnings("unchecked")
public void intEnumAtRootCreatesIntBackedEnum() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {

ClassLoader resultsClassLoader = schemaRule.generateAndCompile("/schema/enum/integerEnumAsRoot.json", "com.example");

Class<Enum> rootEnumClass = (Class<Enum>) resultsClassLoader.loadClass("com.example.enums.IntegerEnumAsRoot");

assertThat(rootEnumClass.isEnum(), is(true));
assertThat(rootEnumClass.getDeclaredMethod("fromValue", Integer.class), is(notNullValue()));
assertThat(isPublic(rootEnumClass.getModifiers()), is(true));
}

@Test
@SuppressWarnings("unchecked")
public void doubleEnumAtRootCreatesIntBackedEnum() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {

ClassLoader resultsClassLoader = schemaRule.generateAndCompile("/schema/enum/doubleEnumAsRoot.json", "com.example");

Class<Enum> rootEnumClass = (Class<Enum>) resultsClassLoader.loadClass("com.example.enums.DoubleEnumAsRoot");

assertThat(rootEnumClass.isEnum(), is(true));
assertThat(rootEnumClass.getDeclaredMethod("fromValue", Double.class), is(notNullValue()));
assertThat(isPublic(rootEnumClass.getModifiers()), is(true));
}

@Test
@SuppressWarnings("unchecked")
public void enumWithEmptyStringAsValue() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Expand Down Expand Up @@ -226,7 +255,65 @@ public void enumWithCustomJavaNames() throws ClassNotFoundException, NoSuchMetho
assertThat(jsonTree.get("enum_Property").isTextual(), is(true));
assertThat(jsonTree.get("enum_Property").asText(), is("3"));
}

@Test
@SuppressWarnings("unchecked")
public void intEnumIsDeserializedCorrectly() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, JsonParseException, JsonMappingException, IOException {

ClassLoader resultsClassLoader = schemaRule.generateAndCompile("/schema/enum/integerEnumToSerialize.json", "com.example");

// the schema for a valid instance
Class<?> typeWithEnumProperty = resultsClassLoader.loadClass("com.example.enums.IntegerEnumToSerialize");
Class<Enum> enumClass = (Class<Enum>) resultsClassLoader.loadClass("com.example.enums.IntegerEnumToSerialize$TestEnum");

// read the instance into the type
ObjectMapper objectMapper = new ObjectMapper();
Object valueWithEnumProperty = objectMapper.readValue("{\"testEnum\" : 2}", typeWithEnumProperty);

Method getEnumMethod = typeWithEnumProperty.getDeclaredMethod("getTestEnum");
Method getValueMethod = enumClass.getDeclaredMethod("value");

// call getTestEnum on the value
assertThat(getEnumMethod, is(notNullValue()));
Object enumObject = getEnumMethod.invoke(valueWithEnumProperty);

// assert that the object returned is a) a TestEnum, and b) calling .value() on it returns 2
// as per the json snippet above
assertThat(enumObject, IsInstanceOf.instanceOf(enumClass));
assertThat(getValueMethod, is(notNullValue()));
assertThat((Integer)getValueMethod.invoke(enumObject), is(2));
}

@Test
@SuppressWarnings("unchecked")
public void intEnumIsSerializedCorrectly() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, JsonParseException, JsonMappingException, IOException, InstantiationException {

ClassLoader resultsClassLoader = schemaRule.generateAndCompile("/schema/enum/integerEnumToSerialize.json", "com.example");

// the schema for a valid instance
Class<?> typeWithEnumProperty = resultsClassLoader.loadClass("com.example.enums.IntegerEnumToSerialize");
Class<Enum> enumClass = (Class<Enum>) resultsClassLoader.loadClass("com.example.enums.IntegerEnumToSerialize$TestEnum");

// create an instance
Object valueWithEnumProperty = typeWithEnumProperty.newInstance();
Method enumSetter = typeWithEnumProperty.getMethod("setTestEnum", enumClass);

// call setTestEnum(TestEnum.ONE)
enumSetter.invoke(valueWithEnumProperty, enumClass.getEnumConstants()[0]);

ObjectMapper objectMapper = new ObjectMapper();

// write our instance out to json
String jsonString = objectMapper.writeValueAsString(valueWithEnumProperty);
JsonNode jsonTree = objectMapper.readTree(jsonString);

assertThat(jsonTree.size(), is(1));
assertThat(jsonTree.has("testEnum"), is(true));
assertThat(jsonTree.get("testEnum").isIntegralNumber(), is(true));
assertThat(jsonTree.get("testEnum").asInt(), is(1));
}


@Test
@SuppressWarnings("unchecked")
public void jacksonCanMarshalEnums() throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, IOException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"javaType" : "com.example.enums.DoubleEnumAsRoot",
"type" : "number",
"enum" : [1.0, 2.5, 3],
"javaEnumNames" : ["One", "TwoAndAHalf", "Three"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"javaType" : "com.example.enums.IntegerEnumAsRoot",
"type" : "integer",
"enum" : [1, 2, 3],
"javaEnumNames" : ["One", "Two", "Three"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"javaType" : "com.example.enums.IntegerEnumToSerialize",
"properties" : {
"testEnum" : {
"type" : "integer",
"enum" : [1, 2, 3],
"javaEnumNames" : ["One", "Two", "Three"]
}
}
}

0 comments on commit 528eb22

Please sign in to comment.