From a11d110556a167facc54aba0dce2ffc7160e93c2 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Mon, 2 Oct 2023 20:36:52 -0400 Subject: [PATCH] Add Jackson enum naming support via `@EnumNaming` + `@JsonProperty` Signed-off-by: Michael Edgar --- .../api/constants/JacksonConstants.java | 2 + .../openapi/runtime/io/IoLogging.java | 4 - .../runtime/io/schema/SchemaFactory.java | 44 +----- .../scanner/dataobject/DataObjectLogging.java | 5 + .../scanner/dataobject/EnumProcessor.java | 89 ++++++++++++ .../PropertyNamingStrategyFactory.java | 4 +- .../scanner/dataobject/EnumNamingTest.java | 137 ++++++++++++++++++ .../components.schemas.enum-naming.json | 19 +++ pom.xml | 2 +- 9 files changed, 258 insertions(+), 48 deletions(-) create mode 100644 core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/EnumProcessor.java create mode 100644 core/src/test/java/io/smallrye/openapi/runtime/scanner/dataobject/EnumNamingTest.java create mode 100644 core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.enum-naming.json diff --git a/core/src/main/java/io/smallrye/openapi/api/constants/JacksonConstants.java b/core/src/main/java/io/smallrye/openapi/api/constants/JacksonConstants.java index f2027d5a0..2aa496dd5 100644 --- a/core/src/main/java/io/smallrye/openapi/api/constants/JacksonConstants.java +++ b/core/src/main/java/io/smallrye/openapi/api/constants/JacksonConstants.java @@ -28,6 +28,8 @@ public class JacksonConstants { .createSimple("com.fasterxml.jackson.annotation.JsonView"); public static final DotName JSON_NAMING = DotName .createSimple("com.fasterxml.jackson.databind.annotation.JsonNaming"); + public static final DotName ENUM_NAMING = DotName + .createSimple("com.fasterxml.jackson.databind.annotation.EnumNaming"); public static final String PROP_VALUE = "value"; diff --git a/core/src/main/java/io/smallrye/openapi/runtime/io/IoLogging.java b/core/src/main/java/io/smallrye/openapi/runtime/io/IoLogging.java index a8e387bc0..7c0cbf11a 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/io/IoLogging.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/io/IoLogging.java @@ -76,8 +76,4 @@ public interface IoLogging extends BasicLogger { @Message(id = 2015, value = "Processing a json array of %s json nodes.") void jsonArray(String of); - @LogMessage(level = Logger.Level.WARN) - @Message(id = 2016, value = "Failed to read enumeration values from enum %s method %s with `@JsonValue`: %s") - void exceptionReadingEnumJsonValue(String enumName, String methodName, Exception exception); - } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java b/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java index 7c4b5f0f0..a2fc9e071 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java @@ -1,6 +1,5 @@ package io.smallrye.openapi.runtime.io.schema; -import java.lang.reflect.Method; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; @@ -9,7 +8,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -22,13 +20,11 @@ import org.jboss.jandex.ArrayType; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassType; -import org.jboss.jandex.FieldInfo; import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; import org.jboss.jandex.Type.Kind; import io.smallrye.openapi.api.constants.JDKConstants; -import io.smallrye.openapi.api.constants.JacksonConstants; import io.smallrye.openapi.api.constants.MutinyConstants; import io.smallrye.openapi.api.constants.OpenApiConstants; import io.smallrye.openapi.api.models.media.DiscriminatorImpl; @@ -41,6 +37,7 @@ import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension; import io.smallrye.openapi.runtime.scanner.OpenApiDataObjectScanner; import io.smallrye.openapi.runtime.scanner.SchemaRegistry; +import io.smallrye.openapi.runtime.scanner.dataobject.EnumProcessor; import io.smallrye.openapi.runtime.scanner.spi.AnnotationScanner; import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext; import io.smallrye.openapi.runtime.util.Annotations; @@ -550,48 +547,11 @@ public static Schema typeToSchema(final AnnotationScannerContext context, Type t */ public static Schema enumToSchema(final AnnotationScannerContext context, Type enumType) { IoLogging.logger.enumProcessing(enumType); - final int ENUM = 0x00004000; // see java.lang.reflect.Modifier#ENUM + List enumeration = EnumProcessor.enumConstants(context, enumType); ClassInfo enumKlazz = context.getIndex().getClassByName(TypeUtil.getName(enumType)); AnnotationInstance schemaAnnotation = Annotations.getAnnotation(enumKlazz, SchemaConstant.DOTNAME_SCHEMA); Schema enumSchema = new SchemaImpl(); - List enumeration = enumKlazz.annotationsMap() - .getOrDefault(JacksonConstants.JSON_VALUE, Collections.emptyList()) - .stream() - // @JsonValue#value (default = true) allows for the functionality to be disabled - .filter(atJsonValue -> Annotations.value(atJsonValue, JacksonConstants.PROP_VALUE, true)) - .map(AnnotationInstance::target) - .filter(JandexUtil::isSupplier) - .map(valueTarget -> { - String className = enumKlazz.name().toString(); - String methodName = valueTarget.asMethod().name(); - - try { - Class loadedEnum = Class.forName(className, false, context.getClassLoader()); - Method valueMethod = loadedEnum.getDeclaredMethod(methodName); - Object[] constants = loadedEnum.getEnumConstants(); - - List reflectedEnumeration = new ArrayList<>(constants.length); - - for (Object constant : constants) { - reflectedEnumeration.add(valueMethod.invoke(constant)); - } - - return reflectedEnumeration; - } catch (Exception e) { - IoLogging.logger.exceptionReadingEnumJsonValue(className, methodName, e); - } - - return null; - }) - .filter(Objects::nonNull) - .findFirst() - .orElseGet(() -> JandexUtil.fields(context, enumKlazz) - .stream() - .filter(field -> (field.flags() & ENUM) != 0) - .map(FieldInfo::name) - .collect(Collectors.toList())); - if (schemaAnnotation != null) { Map defaults = new HashMap<>(2); defaults.put(SchemaConstant.PROP_TYPE, SchemaType.STRING); diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/DataObjectLogging.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/DataObjectLogging.java index 5c7448600..e02172aed 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/DataObjectLogging.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/DataObjectLogging.java @@ -86,4 +86,9 @@ interface DataObjectLogging extends BasicLogger { @Message(id = 31016, value = "Unanticipated mismatch between type arguments and type variables \n" + "Args: %s\n Vars:%s") void classNotAvailable(List typeVariables, List arguments); + + @LogMessage(level = Logger.Level.WARN) + @Message(id = 31017, value = "Failed to read enumeration values from enum %s method %s with `@JsonValue`: %s") + void exceptionReadingEnumJsonValue(String enumName, String methodName, Exception exception); + } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/EnumProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/EnumProcessor.java new file mode 100644 index 000000000..dd262ad17 --- /dev/null +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/EnumProcessor.java @@ -0,0 +1,89 @@ +package io.smallrye.openapi.runtime.scanner.dataobject; + +import static io.smallrye.openapi.api.constants.JacksonConstants.ENUM_NAMING; +import static io.smallrye.openapi.api.constants.JacksonConstants.JSON_PROPERTY; +import static io.smallrye.openapi.api.constants.JacksonConstants.JSON_VALUE; +import static io.smallrye.openapi.api.constants.JacksonConstants.PROP_VALUE; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.Type; + +import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext; +import io.smallrye.openapi.runtime.util.Annotations; +import io.smallrye.openapi.runtime.util.JandexUtil; +import io.smallrye.openapi.runtime.util.TypeUtil; + +public class EnumProcessor { + + private static final int ENUM = 0x00004000; // see java.lang.reflect.Modifier#ENUM + + private EnumProcessor() { + } + + public static List enumConstants(AnnotationScannerContext context, Type enumType) { + ClassInfo enumKlazz = context.getIndex().getClassByName(TypeUtil.getName(enumType)); + Function nameTranslator = nameTranslator(context, enumKlazz); + + return enumKlazz.annotationsMap() + .getOrDefault(JSON_VALUE, Collections.emptyList()) + .stream() + // @JsonValue#value (default = true) allows for the functionality to be disabled + .filter(atJsonValue -> Annotations.value(atJsonValue, PROP_VALUE, true)) + .map(AnnotationInstance::target) + .filter(JandexUtil::isSupplier) + .map(valueTarget -> jacksonJsonValues(context, enumKlazz, valueTarget)) + .filter(Objects::nonNull) + .findFirst() + .orElseGet(() -> JandexUtil.fields(context, enumKlazz) + .stream() + .filter(field -> (field.flags() & ENUM) != 0) + .map(nameTranslator::apply) + .collect(Collectors.toList())); + } + + private static List jacksonJsonValues(AnnotationScannerContext context, ClassInfo enumKlazz, + AnnotationTarget valueTarget) { + String className = enumKlazz.name().toString(); + String methodName = valueTarget.asMethod().name(); + + try { + Class loadedEnum = Class.forName(className, false, context.getClassLoader()); + Method valueMethod = loadedEnum.getDeclaredMethod(methodName); + Object[] constants = loadedEnum.getEnumConstants(); + + List reflectedEnumeration = new ArrayList<>(constants.length); + + for (Object constant : constants) { + reflectedEnumeration.add(valueMethod.invoke(constant)); + } + + return reflectedEnumeration; + } catch (Exception e) { + DataObjectLogging.logger.exceptionReadingEnumJsonValue(className, methodName, e); + } + + return null; // NOSONAR + } + + private static Function nameTranslator(AnnotationScannerContext context, ClassInfo enumKlazz) { + return Optional. ofNullable(Annotations.getAnnotationValue(enumKlazz, ENUM_NAMING, PROP_VALUE)) + .map(namingClass -> namingClass.name().toString()) + .map(namingClass -> PropertyNamingStrategyFactory.getStrategy(namingClass, context.getClassLoader())) + .> map(nameStrategy -> fieldInfo -> nameStrategy.apply(fieldInfo.name())) + .orElse(fieldInfo -> Optional + . ofNullable(Annotations.getAnnotationValue(fieldInfo, JSON_PROPERTY, PROP_VALUE)) + .orElseGet(fieldInfo::name)); + } +} diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/PropertyNamingStrategyFactory.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/PropertyNamingStrategyFactory.java index 42be4c516..bdbb46afe 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/PropertyNamingStrategyFactory.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/PropertyNamingStrategyFactory.java @@ -14,7 +14,9 @@ public class PropertyNamingStrategyFactory { private static final String JSONB_TRANSLATE_NAME = "translateName"; private static final String JACKSON_TRANSLATE = "translate"; - private static final List knownMethods = Arrays.asList(JSONB_TRANSLATE_NAME, JACKSON_TRANSLATE); + private static final String JACKSON_TRANSLATE_ENUM = "convertEnumToExternalName"; + private static final List knownMethods = Arrays.asList(JSONB_TRANSLATE_NAME, JACKSON_TRANSLATE, + JACKSON_TRANSLATE_ENUM); private static final Map> STRATEGY_CACHE = new ConcurrentHashMap<>(); private PropertyNamingStrategyFactory() { diff --git a/core/src/test/java/io/smallrye/openapi/runtime/scanner/dataobject/EnumNamingTest.java b/core/src/test/java/io/smallrye/openapi/runtime/scanner/dataobject/EnumNamingTest.java new file mode 100644 index 000000000..f881d7f50 --- /dev/null +++ b/core/src/test/java/io/smallrye/openapi/runtime/scanner/dataobject/EnumNamingTest.java @@ -0,0 +1,137 @@ +package io.smallrye.openapi.runtime.scanner.dataobject; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.jboss.jandex.Index; +import org.junit.jupiter.api.Test; + +import io.smallrye.openapi.runtime.scanner.IndexScannerTestBase; +import io.smallrye.openapi.runtime.scanner.OpenApiAnnotationScanner; + +class EnumNamingTest extends IndexScannerTestBase { + + static void test(Class... classes) throws Exception { + Index index = indexOf(classes); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("components.schemas.enum-naming.json", result); + } + + @Test + void testEnumNamingDefault() throws Exception { + @Schema(name = "Bean") + class Bean { + @SuppressWarnings("unused") + DaysOfWeekDefault days; + } + + test(Bean.class, DaysOfWeekDefault.class); + } + + @Test + void testEnumNamingValueMethod() throws Exception { + @Schema(name = "Bean") + class Bean { + @SuppressWarnings("unused") + DaysOfWeekValue days; + } + + test(Bean.class, DaysOfWeekValue.class); + } + + @Test + void testEnumNamingStrategy() throws Exception { + @Schema(name = "Bean") + class Bean { + @SuppressWarnings("unused") + DaysOfWeekStrategy days; + } + + test(Bean.class, DaysOfWeekStrategy.class); + } + + @Test + void testEnumNamingProperty() throws Exception { + @Schema(name = "Bean") + class Bean { + @SuppressWarnings("unused") + DaysOfWeekProperty days; + } + + test(Bean.class, DaysOfWeekProperty.class); + } + + @Schema(name = "DaysOfWeek") + enum DaysOfWeekDefault { + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday + } + + @Schema(name = "DaysOfWeek") + enum DaysOfWeekValue { + MONDAY, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, + SATURDAY, + SUNDAY; + + @com.fasterxml.jackson.annotation.JsonValue + public String toValue() { + String name = name(); + return name.charAt(0) + name.substring(1).toLowerCase(); + } + } + + @Schema(name = "DaysOfWeek") + @com.fasterxml.jackson.databind.annotation.EnumNaming(DaysOfWeekShortNaming.class) + enum DaysOfWeekStrategy { + MON("Monday"), + TUE("Tuesday"), + WED("Wednesday"), + THU("Thursday"), + FRI("Friday"), + SAT("Saturday"), + SUN("Sunday"); + + final String displayName; + + private DaysOfWeekStrategy(String displayName) { + this.displayName = displayName; + } + } + + public static class DaysOfWeekShortNaming implements com.fasterxml.jackson.databind.EnumNamingStrategy { + @Override + public String convertEnumToExternalName(String enumName) { + return DaysOfWeekStrategy.valueOf(enumName).displayName; + } + } + + @Schema(name = "DaysOfWeek") + enum DaysOfWeekProperty { + @com.fasterxml.jackson.annotation.JsonProperty("Monday") + MONDAY, + @com.fasterxml.jackson.annotation.JsonProperty("Tuesday") + TUESDAY, + @com.fasterxml.jackson.annotation.JsonProperty("Wednesday") + WEDNESDAY, + @com.fasterxml.jackson.annotation.JsonProperty("Thursday") + THURSDAY, + @com.fasterxml.jackson.annotation.JsonProperty("Friday") + FRIDAY, + @com.fasterxml.jackson.annotation.JsonProperty("Saturday") + SATURDAY, + @com.fasterxml.jackson.annotation.JsonProperty("Sunday") + SUNDAY + } + +} diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.enum-naming.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.enum-naming.json new file mode 100644 index 000000000..18200f1ac --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.enum-naming.json @@ -0,0 +1,19 @@ +{ + "openapi" : "3.0.3", + "components" : { + "schemas" : { + "Bean" : { + "type" : "object", + "properties" : { + "days" : { + "$ref" : "#/components/schemas/DaysOfWeek" + } + } + }, + "DaysOfWeek" : { + "enum" : [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" ], + "type" : "string" + } + } + } +} diff --git a/pom.xml b/pom.xml index da27f47fe..ac19f0782 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 3.3.0 - 2.14.1 + 2.15.2 3.0.3 3.1.5 3.2.1