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 Jackson enum naming support via @EnumNaming + @JsonProperty #1589

Merged
merged 1 commit into from
Oct 3, 2023
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
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