Skip to content

Commit

Permalink
Add Jackson enum naming support via @EnumNaming + @JsonProperty (#…
Browse files Browse the repository at this point in the history
…1589)

Signed-off-by: Michael Edgar <michael@xlate.io>
  • Loading branch information
MikeEdgar authored Oct 3, 2023
1 parent d0146c8 commit 0ad3014
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<Object> 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<Object> 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<Object> 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<String, Object> defaults = new HashMap<>(2);
defaults.put(SchemaConstant.PROP_TYPE, SchemaType.STRING);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TypeVariable> typeVariables, List<Type> 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);

}
Original file line number Diff line number Diff line change
@@ -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<Object> enumConstants(AnnotationScannerContext context, Type enumType) {
ClassInfo enumKlazz = context.getIndex().getClassByName(TypeUtil.getName(enumType));
Function<FieldInfo, String> 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<Object> 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<Object> 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<FieldInfo, String> nameTranslator(AnnotationScannerContext context, ClassInfo enumKlazz) {
return Optional.<Type> ofNullable(Annotations.getAnnotationValue(enumKlazz, ENUM_NAMING, PROP_VALUE))
.map(namingClass -> namingClass.name().toString())
.map(namingClass -> PropertyNamingStrategyFactory.getStrategy(namingClass, context.getClassLoader()))
.<Function<FieldInfo, String>> map(nameStrategy -> fieldInfo -> nameStrategy.apply(fieldInfo.name()))
.orElse(fieldInfo -> Optional
.<String> ofNullable(Annotations.getAnnotationValue(fieldInfo, JSON_PROPERTY, PROP_VALUE))
.orElseGet(fieldInfo::name));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> knownMethods = Arrays.asList(JSONB_TRANSLATE_NAME, JACKSON_TRANSLATE);
private static final String JACKSON_TRANSLATE_ENUM = "convertEnumToExternalName";
private static final List<String> knownMethods = Arrays.asList(JSONB_TRANSLATE_NAME, JACKSON_TRANSLATE,
JACKSON_TRANSLATE_ENUM);
private static final Map<String, UnaryOperator<String>> STRATEGY_CACHE = new ConcurrentHashMap<>();

private PropertyNamingStrategyFactory() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}

}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

<properties>
<version.buildhelper.plugin>3.3.0</version.buildhelper.plugin>
<jackson-bom.version>2.14.1</jackson-bom.version>
<jackson-bom.version>2.15.2</jackson-bom.version>
<version.eclipse.microprofile.config>3.0.3</version.eclipse.microprofile.config>
<version.io.smallrye.jandex>3.1.5</version.io.smallrye.jandex>
<version.io.smallrye.smallrye-config>3.2.1</version.io.smallrye.smallrye-config>
Expand Down

0 comments on commit 0ad3014

Please sign in to comment.