Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the '--useInsensitiveEnums' flag to allow case-insensitive enum values #232

Merged
merged 1 commit into from
Feb 13, 2019
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.palantir.insensitive;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.palantir.conjure.java.lib.internal.ConjureEnums;
import com.palantir.logsafe.Preconditions;
import java.util.Locale;
import javax.annotation.Generated;

/**
* This enumerates the numbers 1:2 also 100.
*
* <p>This class is used instead of a native enum to support unknown values. Rather than throw an
* exception, the {@link InsensitiveEnum#valueOf} method defaults to a new instantiation of {@link
* InsensitiveEnum} where {@link InsensitiveEnum#get} will return {@link
* InsensitiveEnum.Value#UNKNOWN}.
*
* <p>For example, {@code InsensitiveEnum.valueOf("corrupted value").get()} will return {@link
* InsensitiveEnum.Value#UNKNOWN}, but {@link InsensitiveEnum#toString} will return "corrupted
* value".
*
* <p>There is no method to access all instantiations of this class, since they cannot be known at
* compile time.
*/
@Generated("com.palantir.conjure.java.types.EnumGenerator")
public final class InsensitiveEnum {
public static final InsensitiveEnum ONE = new InsensitiveEnum(Value.ONE, "ONE");

public static final InsensitiveEnum TWO = new InsensitiveEnum(Value.TWO, "TWO");

/** Value of 100. */
public static final InsensitiveEnum ONE_HUNDRED =
new InsensitiveEnum(Value.ONE_HUNDRED, "ONE_HUNDRED");

private final Value value;

private final String string;

private InsensitiveEnum(Value value, String string) {
this.value = value;
this.string = string;
}

public Value get() {
return this.value;
}

@Override
@JsonValue
public String toString() {
return this.string;
}

@Override
public boolean equals(Object other) {
return (this == other)
|| (other instanceof InsensitiveEnum
&& this.string.equals(((InsensitiveEnum) other).string));
}

@Override
public int hashCode() {
return this.string.hashCode();
}

@JsonCreator
public static InsensitiveEnum valueOf(String value) {
Preconditions.checkNotNull(value, "value cannot be null");
value = value.toUpperCase(Locale.ENGLISH);
switch (value) {
case "ONE":
return ONE;
case "TWO":
return TWO;
case "ONE_HUNDRED":
return ONE_HUNDRED;
default:
ConjureEnums.validate(value);
return new InsensitiveEnum(Value.UNKNOWN, value);
}
}

public <T> T accept(Visitor<T> visitor) {
switch (value) {
case ONE:
return visitor.visitOne();
case TWO:
return visitor.visitTwo();
case ONE_HUNDRED:
return visitor.visitOneHundred();
default:
return visitor.visitUnknown(string);
}
}

@Generated("com.palantir.conjure.java.types.EnumGenerator")
public enum Value {
ONE,

TWO,

/** Value of 100. */
ONE_HUNDRED,

UNKNOWN
}

@Generated("com.palantir.conjure.java.types.EnumGenerator")
public interface Visitor<T> {
T visitOne();

T visitTwo();

/** Value of 100. */
T visitOneHundred();

T visitUnknown(String unknownValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,10 @@ public enum FeatureFlags {
* Use the conjure immutable "Bytes" class over ByteBuffer.
*/
UseImmutableBytes,

/**
* Enums valueOf function will use a case-insensitive lookup. Note that this is not allowed by the conjure
* specification, however may be enabled for backwards compatibility.
*/
CaseInsensitiveEnums,
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.palantir.conjure.java.ConjureAnnotations;
import com.palantir.conjure.java.FeatureFlags;
import com.palantir.conjure.java.lib.internal.ConjureEnums;
import com.palantir.conjure.spec.EnumDefinition;
import com.palantir.conjure.spec.EnumValueDefinition;
Expand All @@ -36,6 +37,8 @@
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.lang.model.element.Modifier;
import org.apache.commons.lang3.StringUtils;

Expand All @@ -49,20 +52,24 @@ public final class EnumGenerator {

private EnumGenerator() {}

public static JavaFile generateEnumType(EnumDefinition typeDef) {
public static JavaFile generateEnumType(EnumDefinition typeDef, Set<FeatureFlags> featureFlags) {
String typePackage = typeDef.getTypeName().getPackage();
ClassName thisClass = ClassName.get(typePackage, typeDef.getTypeName().getName());
ClassName enumClass = ClassName.get(typePackage, typeDef.getTypeName().getName(), "Value");
ClassName visitorClass = ClassName.get(typePackage, typeDef.getTypeName().getName(), "Visitor");

return JavaFile.builder(typePackage, createSafeEnum(typeDef, thisClass, enumClass, visitorClass))
return JavaFile.builder(typePackage, createSafeEnum(typeDef, thisClass, enumClass, visitorClass, featureFlags))
.skipJavaLangImports(true)
.indent(" ")
.build();
}

private static TypeSpec createSafeEnum(
EnumDefinition typeDef, ClassName thisClass, ClassName enumClass, ClassName visitorClass) {
EnumDefinition typeDef,
ClassName thisClass,
ClassName enumClass,
ClassName visitorClass,
Set<FeatureFlags> featureFlags) {
TypeSpec.Builder wrapper = TypeSpec.classBuilder(typeDef.getTypeName().getName())
.addAnnotation(ConjureAnnotations.getConjureGeneratedAnnotation(EnumGenerator.class))
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
Expand All @@ -86,7 +93,7 @@ private static TypeSpec createSafeEnum(
.build())
.addMethod(createEquals(thisClass))
.addMethod(createHashCode())
.addMethod(createValueOf(thisClass, typeDef.getValues()))
.addMethod(createValueOf(thisClass, typeDef.getValues(), featureFlags))
.addMethod(generateAcceptVisitMethod(visitorClass, typeDef.getValues()));

typeDef.getDocs().ifPresent(
Expand Down Expand Up @@ -201,11 +208,16 @@ private static MethodSpec createConstructor(ClassName enumClass) {
.build();
}

private static MethodSpec createValueOf(ClassName thisClass, Iterable<EnumValueDefinition> values) {
private static MethodSpec createValueOf(
ClassName thisClass,
Iterable<EnumValueDefinition> values,
Set<FeatureFlags> featureFlags) {
ParameterSpec param = ParameterSpec.builder(ClassName.get(String.class), "value").build();

CodeBlock.Builder parser = CodeBlock.builder()
.beginControlFlow("switch ($N)", param);
CodeBlock.Builder parser = CodeBlock.builder();
if (featureFlags.contains(FeatureFlags.CaseInsensitiveEnums)) {
parser.addStatement("value = value.toUpperCase($T.ENGLISH)", Locale.class);
}
parser.beginControlFlow("switch ($N)", param);
for (EnumValueDefinition value : values) {
parser.add("case $S:\n", value.getValue())
.indent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ public Set<JavaFile> generateTypes(List<TypeDefinition> types) {
return UnionGenerator.generateUnionType(
typeMapper, typeDef.accept(TypeDefinitionVisitor.UNION));
} else if (typeDef.accept(TypeDefinitionVisitor.IS_ENUM)) {
return EnumGenerator.generateEnumType(
typeDef.accept(TypeDefinitionVisitor.ENUM));
return EnumGenerator.generateEnumType(typeDef.accept(TypeDefinitionVisitor.ENUM), featureFlags);
} else if (typeDef.accept(TypeDefinitionVisitor.IS_ALIAS)) {
return AliasGenerator.generateAliasType(
typeMapper, typeDef.accept(TypeDefinitionVisitor.ALIAS));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static com.palantir.logsafe.testing.Assertions.assertThatLoggableExceptionThrownBy;
import static org.assertj.core.api.Assertions.assertThat;

import com.palantir.insensitive.InsensitiveEnum;
import com.palantir.product.EnumExample;
import org.junit.Test;

Expand Down Expand Up @@ -48,6 +49,17 @@ public void testVisitUnknown() {
assertThat(enumExample.accept(Visitor.INSTANCE)).isEqualTo("SOME_VALUE");
}

@Test
public void testInsensitiveEnum_lowerCase() {
assertThat(InsensitiveEnum.valueOf("one")).isEqualTo(InsensitiveEnum.ONE);
}

@Test
public void testInsensitiveEnum_lowerCaseUnknown() {
InsensitiveEnum value = InsensitiveEnum.valueOf("notknown");
assertThat(value.get()).isEqualTo(InsensitiveEnum.Value.UNKNOWN);
}

@Test
public void testNullValidationUsesSafeLoggable() {
assertThatLoggableExceptionThrownBy(() -> EnumExample.valueOf(null)).hasLogMessage("value cannot be null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ public void testObjectGenerator_byteBufferCompatibility() throws IOException {
assertThatFilesAreTheSame(files, REFERENCE_FILES_FOLDER);
}


@Test
public void testObjectGenerator_insensitiveEnum() throws IOException {
ConjureDefinition def = Conjure.parse(
ImmutableList.of(new File("src/test/resources/example-compat-enum.yml")));
List<Path> files = new ObjectGenerator(Collections.singleton(FeatureFlags.CaseInsensitiveEnums))
.emit(def, folder.getRoot());

assertThatFilesAreTheSame(files, REFERENCE_FILES_FOLDER);
}

@Test
public void testConjureImports() throws IOException {
ConjureDefinition conjure = Conjure.parse(
Expand Down
12 changes: 12 additions & 0 deletions conjure-java-core/src/test/resources/example-compat-enum.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
types:
definitions:
default-package: com.palantir.insensitive
objects:
InsensitiveEnum:
docs: |
This enumerates the numbers 1:2 also 100.
values:
- ONE
- TWO
- value: ONE_HUNDRED
docs: Value of 100.
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,9 @@ Builder undertowServicePrefix(boolean flag) {
Builder useImmutableBytes(boolean flag) {
return flag ? addFeatureFlags(FeatureFlags.UseImmutableBytes) : this;
}

Builder useInsensitiveEnums(boolean flag) {
return flag ? addFeatureFlags(FeatureFlags.CaseInsensitiveEnums) : this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ public static final class GenerateCommand implements Runnable {
description = "Generate binary fields using the immutable 'Bytes' type instead of 'ByteBuffer'")
private boolean useImmutableBytes;

@CommandLine.Option(names = "--useInsensitiveEnums",
defaultValue = "false",
description = "Enums valueOf function will use a case-insensitive lookup. Note that this is not "
+ "allowed by the conjure specification, however may be enabled for backwards compatibility.")
private boolean useInsensitiveEnums;

@CommandLine.Unmatched
private List<String> unmatchedOptions;

Expand Down Expand Up @@ -169,6 +175,7 @@ CliConfiguration getConfiguration() {
.notNullAuthAndBody(notNullAuthAndBody)
.undertowServicePrefix(undertowServicePrefix)
.useImmutableBytes(useImmutableBytes)
.useInsensitiveEnums(useInsensitiveEnums)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ public void parseFeatureFlags() {
"--retrofitCompletableFutures",
"--jerseyBinaryAsResponse",
"--requireNotNullAuthAndBodyParams",
"--useImmutableBytes"
"--useImmutableBytes",
"--useInsensitiveEnums"
};
CliConfiguration expectedConfiguration = CliConfiguration.builder()
.input(targetFile)
Expand All @@ -82,7 +83,8 @@ public void parseFeatureFlags() {
FeatureFlags.RetrofitCompletableFutures,
FeatureFlags.JerseyBinaryAsResponse,
FeatureFlags.RequireNotNullAuthAndBodyParams,
FeatureFlags.UseImmutableBytes))
FeatureFlags.UseImmutableBytes,
FeatureFlags.CaseInsensitiveEnums))
.build();
ConjureJavaCli.GenerateCommand cmd = new CommandLine(new ConjureJavaCli()).parse(args).get(1).getCommand();
assertThat(cmd.getConfiguration()).isEqualTo(expectedConfiguration);
Expand Down