Skip to content

Instantly share code, notes, and snippets.

@jkuipers
Last active November 29, 2024 15:31
Show Gist options
  • Save jkuipers/e6a5a1fc0ca1c921fa2b3b6b933eda9c to your computer and use it in GitHub Desktop.
Save jkuipers/e6a5a1fc0ca1c921fa2b3b6b933eda9c to your computer and use it in GitHub Desktop.
MapStruct EnumMappingStrategy to deal with enums generated by the OpenAPI Tools
package nl.trifork.mapstruct;
import org.mapstruct.ap.spi.DefaultEnumMappingStrategy;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Recognizes enums that are generated by the OpenAPI tools and uses
* the constants' constructor argument rather than their name for mapping
* if that String argument represents a valid Java identifier.
* We do this because the tools do things like stripping common prefixes
* and inserting underscores for digits, which we don't want in the
* enum constants for our corresponding domain enums.
* When that constructor orgument cannot be determined, simply falls back
* to the given {@code enumConstant}.
*/
public class OpenApiToolEnumMappingStrategy extends DefaultEnumMappingStrategy {
private static final Pattern VALID_IDENTIFIER = Pattern.compile("^[a-zA-Z_]\\w*$");
private static boolean isGenerated(TypeElement enumType) {
TypeElement toplevelClass = enumType;
while (toplevelClass.getEnclosingElement() instanceof TypeElement owningType) {
toplevelClass = owningType;
}
for (AnnotationMirror annotation : toplevelClass.getAnnotationMirrors()) {
Name annotationName = annotation.getAnnotationType().asElement().getSimpleName();
if (annotationName.contentEquals("Generated")) {
for (var entry : annotation.getElementValues().entrySet()) {
if (hasOpenApiCodeGenValue(entry)) return true;
}
}
}
return false;
}
private static boolean hasOpenApiCodeGenValue(Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry) {
return entry.getKey().getSimpleName().contentEquals("value") &&
entry.getValue().getValue() instanceof List values &&
!values.isEmpty() && values.getFirst() instanceof AnnotationValue annValue &&
annValue.getValue() instanceof String attrValue &&
attrValue.startsWith("org.openapitools.codegen");
}
@Override
public String getEnumConstant(TypeElement enumType, String enumConstant) {
if (isGenerated(enumType)) {
return constantForGeneratedEnum(enumType, enumConstant);
}
return super.getEnumConstant(enumType, enumConstant);
}
private String constantForGeneratedEnum(TypeElement enumType, String enumConstant) {
// we can't obtain the string arg of the enum instances programmatically,
// so we're going to parse the source file to obtain those as we know the format
try {
var content = elementUtils.getFileObjectOf(enumType).getCharContent(true).toString();
String prefix = enumConstant + "(\"";
int i = content.indexOf(prefix);
if (i != -1) {
int start = i + prefix.length();
int end = content.indexOf('"', start);
String openApiEnum = content.substring(start, end);
if (VALID_IDENTIFIER.matcher(openApiEnum).matches()) return openApiEnum;
}
} catch (IOException e) {
System.out.println("Couldn't read source file for " + enumType + ", mapping '" + enumConstant + "' as-is");
}
return enumConstant;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment