Skip to content

Generate Test Clients during test compilation #585

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

Merged
merged 9 commits into from
Mar 26, 2025
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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@ build/
*.Module
dependency-reduced-pom.xml
.DS_Store
tests/test-sigma/avaje-processors.txt
*avaje-processors.txt
*controllers.txt
tests/test-sigma/io.avaje.jsonb.spi.JsonbExtension
tests/test-sigma/*.txt
tests/test-javalin-jsonb/*.txt
tests/test-nima-jsonb/*.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package io.avaje.http.generator.core;

import static java.util.stream.Collectors.toList;

import java.util.List;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;

final class AnnotationCopier {
private AnnotationCopier() {}

private static final Pattern ANNOTATION_TYPE_PATTERN = Pattern.compile("@([\\w.]+)\\.");

static String trimAnnotationString(String input) {
return ANNOTATION_TYPE_PATTERN.matcher(input).replaceAll("@");
}

static void copyAnnotations(Append writer, Element element, boolean newLines) {
copyAnnotations(writer, element, "", newLines);
}

static void copyAnnotations(Append writer, Element element, String indent, boolean newLines) {
for (final AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
final var type = annotationMirror.getAnnotationType().asElement().asType().toString();
if (!type.contains("io.avaje.http.api.")
|| type.contains("Produces")
|| type.contains("Consumes")
|| type.contains("InstrumentServerContext")
|| type.contains("Default")
|| type.contains("OpenAPI")
|| type.contains("Valid")) {
continue;
}

String annotationString = toAnnotationString(indent, annotationMirror, false);

annotationString =
annotationString
.replace("io.avaje.http.api.", "")
.replace("value=", "")
.replace("(\"\")", "");

writer.append(annotationString);

if (newLines) {
writer.eol();
} else {
writer.append(" ");
}
}
}

static String toSimpleAnnotationString(AnnotationMirror annotationMirror) {
return trimAnnotationString(toAnnotationString("", annotationMirror, true)).substring(1);
}

static String toAnnotationString(
String indent, AnnotationMirror annotationMirror, boolean simpleEnums) {
final String annotationName = annotationMirror.getAnnotationType().toString();

final StringBuilder sb =
new StringBuilder(indent).append("@").append(annotationName).append("(");
boolean first = true;

for (final var entry : sortedValues(annotationMirror)) {
if (!first) {
sb.append(", ");
}
sb.append(entry.getKey().getSimpleName()).append("=");
writeVal(sb, entry.getValue(), simpleEnums);
first = false;
}

return sb.append(")").toString().replace("()", "");
}

private static List<Entry<? extends ExecutableElement, ? extends AnnotationValue>> sortedValues(
AnnotationMirror annotationMirror) {
return APContext.elements().getElementValuesWithDefaults(annotationMirror).entrySet().stream()
.sorted(AnnotationCopier::compareBySimpleName)
.collect(toList());
}

private static int compareBySimpleName(
Entry<? extends ExecutableElement, ? extends AnnotationValue> entry1,
Entry<? extends ExecutableElement, ? extends AnnotationValue> entry2) {
return entry1
.getKey()
.getSimpleName()
.toString()
.compareTo(entry2.getKey().getSimpleName().toString());
}

@SuppressWarnings("unchecked")
private static void writeVal(
final StringBuilder sb, final AnnotationValue annotationValue, boolean simpleEnums) {
final var value = annotationValue.getValue();
if (value instanceof List) {
// handle array values
sb.append("{");
boolean first = true;
for (final AnnotationValue listValue : (List<AnnotationValue>) value) {
if (!first) {
sb.append(", ");
}
writeVal(sb, listValue, simpleEnums);
first = false;
}
sb.append("}");

} else if (value instanceof VariableElement) {
// Handle enum values
final var element = (VariableElement) value;
final var type = element.asType();
final var str = simpleEnums ? element : type.toString() + "." + element;
sb.append(str);

} else if (value instanceof AnnotationMirror) {
// handle annotation values
final var mirror = (AnnotationMirror) value;
final String annotationName = mirror.getAnnotationType().toString();
sb.append("@").append(annotationName).append("(");
boolean first = true;

for (final var entry : sortedValues(mirror)) {
if (!first) {
sb.append(", ");
}
sb.append(entry.getKey().getSimpleName()).append("=");
writeVal(sb, entry.getValue(), simpleEnums);
first = false;
}
sb.append(")");

} else {
sb.append(annotationValue);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
import static java.util.stream.Collectors.toMap;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
Expand All @@ -27,28 +30,54 @@

@GenerateAPContext
@GenerateModuleInfoReader
@SupportedOptions({"useJavax", "useSingleton", "instrumentRequests","disableDirectWrites","disableJsonB"})
@SupportedOptions({
"useJavax",
"useSingleton",
"instrumentRequests",
"disableDirectWrites",
"disableJsonB"
})
public abstract class BaseProcessor extends AbstractProcessor {

private static final String HTTP_CONTROLLERS_TXT = "testAPI/controllers.txt";
protected String contextPathString;

protected Map<String, String> packagePaths = new HashMap<>();

private final Set<String> clientFQNs = new HashSet<>();

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of(PathPrism.PRISM_TYPE, ControllerPrism.PRISM_TYPE, OpenAPIDefinitionPrism.PRISM_TYPE);
return Set.of(
PathPrism.PRISM_TYPE, ControllerPrism.PRISM_TYPE, OpenAPIDefinitionPrism.PRISM_TYPE);
}

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
APContext.init(processingEnv);
ProcessingContext.init(processingEnv, providePlatformAdapter());

try {
var txtFilePath = APContext.getBuildResource(HTTP_CONTROLLERS_TXT);

if (txtFilePath.toFile().exists()) {
Files.lines(txtFilePath).forEach(clientFQNs::add);
}
if (APContext.isTestCompilation()) {
for (var path : clientFQNs) {
TestClientWriter.writeActual(path);
}
}
} catch (IOException e) {
e.printStackTrace();
// not worth failing over
}
}

/** Provide the platform specific adapter to use for Javalin, Helidon etc. */
Expand Down Expand Up @@ -82,19 +111,34 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
readSecuritySchemes(round);
}

for (final Element controller : round.getElementsAnnotatedWith(typeElement(ControllerPrism.PRISM_TYPE))) {
for (final var controller :
ElementFilter.typesIn(
round.getElementsAnnotatedWith(typeElement(ControllerPrism.PRISM_TYPE)))) {
writeAdapter(controller);
}

if (round.processingOver()) {
writeOpenAPI();
ProcessingContext.validateModule();

if (!APContext.isTestCompilation()) {
try {
Files.write(
APContext.getBuildResource(HTTP_CONTROLLERS_TXT),
clientFQNs,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE);
} catch (IOException e) {
// not worth failing over
}
}
}
return false;
}

private void readOpenApiDefinition(RoundEnvironment round) {
for (final Element element : round.getElementsAnnotatedWith(typeElement(OpenAPIDefinitionPrism.PRISM_TYPE))) {
for (final Element element :
round.getElementsAnnotatedWith(typeElement(OpenAPIDefinitionPrism.PRISM_TYPE))) {
doc().readApiDefinition(element);
}
}
Expand All @@ -103,16 +147,19 @@ private void readTagDefinitions(RoundEnvironment round) {
for (final Element element : round.getElementsAnnotatedWith(typeElement(TagPrism.PRISM_TYPE))) {
doc().addTagDefinition(element);
}
for (final Element element : round.getElementsAnnotatedWith(typeElement(TagsPrism.PRISM_TYPE))) {
for (final Element element :
round.getElementsAnnotatedWith(typeElement(TagsPrism.PRISM_TYPE))) {
doc().addTagsDefinition(element);
}
}

private void readSecuritySchemes(RoundEnvironment round) {
for (final Element element : round.getElementsAnnotatedWith(typeElement(SecuritySchemePrism.PRISM_TYPE))) {
for (final Element element :
round.getElementsAnnotatedWith(typeElement(SecuritySchemePrism.PRISM_TYPE))) {
doc().addSecurityScheme(element);
}
for (final Element element : round.getElementsAnnotatedWith(typeElement(SecuritySchemesPrism.PRISM_TYPE))) {
for (final Element element :
round.getElementsAnnotatedWith(typeElement(SecuritySchemesPrism.PRISM_TYPE))) {
doc().addSecuritySchemes(element);
}
}
Expand All @@ -121,31 +168,42 @@ private void writeOpenAPI() {
doc().writeApi();
}

private void writeAdapter(Element controller) {
if (controller instanceof TypeElement) {
final var packageFQN = elements().getPackageOf(controller).getQualifiedName().toString();
final var contextPath = Util.combinePath(contextPathString, packagePath(packageFQN));
final var reader = new ControllerReader((TypeElement) controller, contextPath);
reader.read(true);
try {
writeControllerAdapter(reader);
} catch (final Throwable e) {
logError(reader.beanType(), "Failed to write $Route class " + e);
private void writeAdapter(TypeElement controller) {
final var packageFQN = elements().getPackageOf(controller).getQualifiedName().toString();
final var contextPath = Util.combinePath(contextPathString, packagePath(packageFQN));
final var reader = new ControllerReader(controller, contextPath);
reader.read(true);
try {

writeControllerAdapter(reader);
writeClientAdapter(reader);

} catch (final Throwable e) {
logError(reader.beanType(), "Failed to write $Route class " + e);
}
}

private void writeClientAdapter(ControllerReader reader) {

try {
if (reader.beanType().getInterfaces().isEmpty()
&& "java.lang.Object".equals(reader.beanType().getSuperclass().toString())) {
new TestClientWriter(reader).write();
clientFQNs.add(reader.beanType().getQualifiedName().toString() + "TestAPI");
}
} catch (final IOException e) {
logError(reader.beanType(), "Failed to write $Route class " + e);
}
}

private String packagePath(String packageFQN) {
return packagePaths.entrySet().stream()
.filter(k -> packageFQN.startsWith(k.getKey()))
.map(Entry::getValue)
.reduce(Util::combinePath)
.orElse(null);
.filter(k -> packageFQN.startsWith(k.getKey()))
.map(Entry::getValue)
.reduce(Util::combinePath)
.orElse(null);
}

/**
* Write the adapter code for the given controller.
*/
/** Write the adapter code for the given controller. */
public abstract void writeControllerAdapter(ControllerReader reader) throws IOException;

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ private PrimitiveUtil() {}
"short", "Short",
"double", "Double",
"float", "Float",
"boolean", "Boolean");
"boolean", "Boolean",
"void", "Void");

public static String wrap(String shortName) {
final var wrapped = wrapperMap.get(shortName);
Expand Down
Loading