diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/insensitive/InsensitiveEnum.java b/conjure-java-core/src/integrationInput/java/com/palantir/insensitive/InsensitiveEnum.java new file mode 100644 index 000000000..34c087da5 --- /dev/null +++ b/conjure-java-core/src/integrationInput/java/com/palantir/insensitive/InsensitiveEnum.java @@ -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. + * + *

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}. + * + *

For example, {@code InsensitiveEnum.valueOf("corrupted value").get()} will return {@link + * InsensitiveEnum.Value#UNKNOWN}, but {@link InsensitiveEnum#toString} will return "corrupted + * value". + * + *

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 accept(Visitor 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 visitOne(); + + T visitTwo(); + + /** Value of 100. */ + T visitOneHundred(); + + T visitUnknown(String unknownValue); + } +} diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/FeatureFlags.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/FeatureFlags.java index 042656fe9..9947903df 100644 --- a/conjure-java-core/src/main/java/com/palantir/conjure/java/FeatureFlags.java +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/FeatureFlags.java @@ -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, } diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/types/EnumGenerator.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/types/EnumGenerator.java index 12db2513a..965439285 100644 --- a/conjure-java-core/src/main/java/com/palantir/conjure/java/types/EnumGenerator.java +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/types/EnumGenerator.java @@ -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; @@ -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; @@ -49,20 +52,24 @@ public final class EnumGenerator { private EnumGenerator() {} - public static JavaFile generateEnumType(EnumDefinition typeDef) { + public static JavaFile generateEnumType(EnumDefinition typeDef, Set 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) { TypeSpec.Builder wrapper = TypeSpec.classBuilder(typeDef.getTypeName().getName()) .addAnnotation(ConjureAnnotations.getConjureGeneratedAnnotation(EnumGenerator.class)) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) @@ -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( @@ -201,11 +208,16 @@ private static MethodSpec createConstructor(ClassName enumClass) { .build(); } - private static MethodSpec createValueOf(ClassName thisClass, Iterable values) { + private static MethodSpec createValueOf( + ClassName thisClass, + Iterable values, + Set 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() diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/types/ObjectGenerator.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/types/ObjectGenerator.java index ae1875b36..41592b765 100644 --- a/conjure-java-core/src/main/java/com/palantir/conjure/java/types/ObjectGenerator.java +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/types/ObjectGenerator.java @@ -46,8 +46,7 @@ public Set generateTypes(List 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)); diff --git a/conjure-java-core/src/test/java/com/palantir/conjure/java/types/EnumTests.java b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/EnumTests.java index 48732bcec..a4268d4c3 100644 --- a/conjure-java-core/src/test/java/com/palantir/conjure/java/types/EnumTests.java +++ b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/EnumTests.java @@ -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; @@ -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"); diff --git a/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ObjectGeneratorTests.java b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ObjectGeneratorTests.java index 45b33ab8a..a525616d5 100644 --- a/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ObjectGeneratorTests.java +++ b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ObjectGeneratorTests.java @@ -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 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( diff --git a/conjure-java-core/src/test/resources/example-compat-enum.yml b/conjure-java-core/src/test/resources/example-compat-enum.yml new file mode 100644 index 000000000..1b4454bbc --- /dev/null +++ b/conjure-java-core/src/test/resources/example-compat-enum.yml @@ -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. diff --git a/conjure-java/src/main/java/com/palantir/conjure/java/cli/CliConfiguration.java b/conjure-java/src/main/java/com/palantir/conjure/java/cli/CliConfiguration.java index 08a9f1008..fcc34af6d 100644 --- a/conjure-java/src/main/java/com/palantir/conjure/java/cli/CliConfiguration.java +++ b/conjure-java/src/main/java/com/palantir/conjure/java/cli/CliConfiguration.java @@ -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; + } } } diff --git a/conjure-java/src/main/java/com/palantir/conjure/java/cli/ConjureJavaCli.java b/conjure-java/src/main/java/com/palantir/conjure/java/cli/ConjureJavaCli.java index e5da51c3e..6898ede35 100644 --- a/conjure-java/src/main/java/com/palantir/conjure/java/cli/ConjureJavaCli.java +++ b/conjure-java/src/main/java/com/palantir/conjure/java/cli/ConjureJavaCli.java @@ -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 unmatchedOptions; @@ -169,6 +175,7 @@ CliConfiguration getConfiguration() { .notNullAuthAndBody(notNullAuthAndBody) .undertowServicePrefix(undertowServicePrefix) .useImmutableBytes(useImmutableBytes) + .useInsensitiveEnums(useInsensitiveEnums) .build(); } diff --git a/conjure-java/src/test/java/com/palantir/conjure/java/cli/ConjureJavaCliTest.java b/conjure-java/src/test/java/com/palantir/conjure/java/cli/ConjureJavaCliTest.java index 06584da93..ff13aa456 100644 --- a/conjure-java/src/test/java/com/palantir/conjure/java/cli/ConjureJavaCliTest.java +++ b/conjure-java/src/test/java/com/palantir/conjure/java/cli/ConjureJavaCliTest.java @@ -72,7 +72,8 @@ public void parseFeatureFlags() { "--retrofitCompletableFutures", "--jerseyBinaryAsResponse", "--requireNotNullAuthAndBodyParams", - "--useImmutableBytes" + "--useImmutableBytes", + "--useInsensitiveEnums" }; CliConfiguration expectedConfiguration = CliConfiguration.builder() .input(targetFile) @@ -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);