Skip to content

Commit 6571d95

Browse files
authored
Merge pull request #585 from SentryMan/test-client
Generate Test Clients during test compilation
2 parents b970e0d + ee29e03 commit 6571d95

File tree

9 files changed

+434
-29
lines changed

9 files changed

+434
-29
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,9 @@ build/
1313
*.Module
1414
dependency-reduced-pom.xml
1515
.DS_Store
16-
tests/test-sigma/avaje-processors.txt
16+
*avaje-processors.txt
17+
*controllers.txt
1718
tests/test-sigma/io.avaje.jsonb.spi.JsonbExtension
19+
tests/test-sigma/*.txt
20+
tests/test-javalin-jsonb/*.txt
21+
tests/test-nima-jsonb/*.txt
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package io.avaje.http.generator.core;
2+
3+
import static java.util.stream.Collectors.toList;
4+
5+
import java.util.List;
6+
import java.util.Map.Entry;
7+
import java.util.regex.Pattern;
8+
9+
import javax.lang.model.element.AnnotationMirror;
10+
import javax.lang.model.element.AnnotationValue;
11+
import javax.lang.model.element.Element;
12+
import javax.lang.model.element.ExecutableElement;
13+
import javax.lang.model.element.VariableElement;
14+
15+
final class AnnotationCopier {
16+
private AnnotationCopier() {}
17+
18+
private static final Pattern ANNOTATION_TYPE_PATTERN = Pattern.compile("@([\\w.]+)\\.");
19+
20+
static String trimAnnotationString(String input) {
21+
return ANNOTATION_TYPE_PATTERN.matcher(input).replaceAll("@");
22+
}
23+
24+
static void copyAnnotations(Append writer, Element element, boolean newLines) {
25+
copyAnnotations(writer, element, "", newLines);
26+
}
27+
28+
static void copyAnnotations(Append writer, Element element, String indent, boolean newLines) {
29+
for (final AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
30+
final var type = annotationMirror.getAnnotationType().asElement().asType().toString();
31+
if (!type.contains("io.avaje.http.api.")
32+
|| type.contains("Produces")
33+
|| type.contains("Consumes")
34+
|| type.contains("InstrumentServerContext")
35+
|| type.contains("Default")
36+
|| type.contains("OpenAPI")
37+
|| type.contains("Valid")) {
38+
continue;
39+
}
40+
41+
String annotationString = toAnnotationString(indent, annotationMirror, false);
42+
43+
annotationString =
44+
annotationString
45+
.replace("io.avaje.http.api.", "")
46+
.replace("value=", "")
47+
.replace("(\"\")", "");
48+
49+
writer.append(annotationString);
50+
51+
if (newLines) {
52+
writer.eol();
53+
} else {
54+
writer.append(" ");
55+
}
56+
}
57+
}
58+
59+
static String toSimpleAnnotationString(AnnotationMirror annotationMirror) {
60+
return trimAnnotationString(toAnnotationString("", annotationMirror, true)).substring(1);
61+
}
62+
63+
static String toAnnotationString(
64+
String indent, AnnotationMirror annotationMirror, boolean simpleEnums) {
65+
final String annotationName = annotationMirror.getAnnotationType().toString();
66+
67+
final StringBuilder sb =
68+
new StringBuilder(indent).append("@").append(annotationName).append("(");
69+
boolean first = true;
70+
71+
for (final var entry : sortedValues(annotationMirror)) {
72+
if (!first) {
73+
sb.append(", ");
74+
}
75+
sb.append(entry.getKey().getSimpleName()).append("=");
76+
writeVal(sb, entry.getValue(), simpleEnums);
77+
first = false;
78+
}
79+
80+
return sb.append(")").toString().replace("()", "");
81+
}
82+
83+
private static List<Entry<? extends ExecutableElement, ? extends AnnotationValue>> sortedValues(
84+
AnnotationMirror annotationMirror) {
85+
return APContext.elements().getElementValuesWithDefaults(annotationMirror).entrySet().stream()
86+
.sorted(AnnotationCopier::compareBySimpleName)
87+
.collect(toList());
88+
}
89+
90+
private static int compareBySimpleName(
91+
Entry<? extends ExecutableElement, ? extends AnnotationValue> entry1,
92+
Entry<? extends ExecutableElement, ? extends AnnotationValue> entry2) {
93+
return entry1
94+
.getKey()
95+
.getSimpleName()
96+
.toString()
97+
.compareTo(entry2.getKey().getSimpleName().toString());
98+
}
99+
100+
@SuppressWarnings("unchecked")
101+
private static void writeVal(
102+
final StringBuilder sb, final AnnotationValue annotationValue, boolean simpleEnums) {
103+
final var value = annotationValue.getValue();
104+
if (value instanceof List) {
105+
// handle array values
106+
sb.append("{");
107+
boolean first = true;
108+
for (final AnnotationValue listValue : (List<AnnotationValue>) value) {
109+
if (!first) {
110+
sb.append(", ");
111+
}
112+
writeVal(sb, listValue, simpleEnums);
113+
first = false;
114+
}
115+
sb.append("}");
116+
117+
} else if (value instanceof VariableElement) {
118+
// Handle enum values
119+
final var element = (VariableElement) value;
120+
final var type = element.asType();
121+
final var str = simpleEnums ? element : type.toString() + "." + element;
122+
sb.append(str);
123+
124+
} else if (value instanceof AnnotationMirror) {
125+
// handle annotation values
126+
final var mirror = (AnnotationMirror) value;
127+
final String annotationName = mirror.getAnnotationType().toString();
128+
sb.append("@").append(annotationName).append("(");
129+
boolean first = true;
130+
131+
for (final var entry : sortedValues(mirror)) {
132+
if (!first) {
133+
sb.append(", ");
134+
}
135+
sb.append(entry.getKey().getSimpleName()).append("=");
136+
writeVal(sb, entry.getValue(), simpleEnums);
137+
first = false;
138+
}
139+
sb.append(")");
140+
141+
} else {
142+
sb.append(annotationValue);
143+
}
144+
}
145+
}

http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java

Lines changed: 83 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
import static java.util.stream.Collectors.toMap;
99

1010
import java.io.IOException;
11+
import java.nio.file.Files;
12+
import java.nio.file.StandardOpenOption;
1113
import java.util.HashMap;
14+
import java.util.HashSet;
1215
import java.util.Map;
1316
import java.util.Map.Entry;
1417
import java.util.Set;
@@ -27,28 +30,54 @@
2730

2831
@GenerateAPContext
2932
@GenerateModuleInfoReader
30-
@SupportedOptions({"useJavax", "useSingleton", "instrumentRequests","disableDirectWrites","disableJsonB"})
33+
@SupportedOptions({
34+
"useJavax",
35+
"useSingleton",
36+
"instrumentRequests",
37+
"disableDirectWrites",
38+
"disableJsonB"
39+
})
3140
public abstract class BaseProcessor extends AbstractProcessor {
3241

42+
private static final String HTTP_CONTROLLERS_TXT = "testAPI/controllers.txt";
3343
protected String contextPathString;
3444

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

47+
private final Set<String> clientFQNs = new HashSet<>();
48+
3749
@Override
3850
public SourceVersion getSupportedSourceVersion() {
3951
return SourceVersion.latest();
4052
}
4153

4254
@Override
4355
public Set<String> getSupportedAnnotationTypes() {
44-
return Set.of(PathPrism.PRISM_TYPE, ControllerPrism.PRISM_TYPE, OpenAPIDefinitionPrism.PRISM_TYPE);
56+
return Set.of(
57+
PathPrism.PRISM_TYPE, ControllerPrism.PRISM_TYPE, OpenAPIDefinitionPrism.PRISM_TYPE);
4558
}
4659

4760
@Override
4861
public synchronized void init(ProcessingEnvironment processingEnv) {
4962
super.init(processingEnv);
5063
APContext.init(processingEnv);
5164
ProcessingContext.init(processingEnv, providePlatformAdapter());
65+
66+
try {
67+
var txtFilePath = APContext.getBuildResource(HTTP_CONTROLLERS_TXT);
68+
69+
if (txtFilePath.toFile().exists()) {
70+
Files.lines(txtFilePath).forEach(clientFQNs::add);
71+
}
72+
if (APContext.isTestCompilation()) {
73+
for (var path : clientFQNs) {
74+
TestClientWriter.writeActual(path);
75+
}
76+
}
77+
} catch (IOException e) {
78+
e.printStackTrace();
79+
// not worth failing over
80+
}
5281
}
5382

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

85-
for (final Element controller : round.getElementsAnnotatedWith(typeElement(ControllerPrism.PRISM_TYPE))) {
114+
for (final var controller :
115+
ElementFilter.typesIn(
116+
round.getElementsAnnotatedWith(typeElement(ControllerPrism.PRISM_TYPE)))) {
86117
writeAdapter(controller);
87118
}
88119

89120
if (round.processingOver()) {
90121
writeOpenAPI();
91122
ProcessingContext.validateModule();
123+
124+
if (!APContext.isTestCompilation()) {
125+
try {
126+
Files.write(
127+
APContext.getBuildResource(HTTP_CONTROLLERS_TXT),
128+
clientFQNs,
129+
StandardOpenOption.CREATE,
130+
StandardOpenOption.WRITE);
131+
} catch (IOException e) {
132+
// not worth failing over
133+
}
134+
}
92135
}
93136
return false;
94137
}
95138

96139
private void readOpenApiDefinition(RoundEnvironment round) {
97-
for (final Element element : round.getElementsAnnotatedWith(typeElement(OpenAPIDefinitionPrism.PRISM_TYPE))) {
140+
for (final Element element :
141+
round.getElementsAnnotatedWith(typeElement(OpenAPIDefinitionPrism.PRISM_TYPE))) {
98142
doc().readApiDefinition(element);
99143
}
100144
}
@@ -103,16 +147,19 @@ private void readTagDefinitions(RoundEnvironment round) {
103147
for (final Element element : round.getElementsAnnotatedWith(typeElement(TagPrism.PRISM_TYPE))) {
104148
doc().addTagDefinition(element);
105149
}
106-
for (final Element element : round.getElementsAnnotatedWith(typeElement(TagsPrism.PRISM_TYPE))) {
150+
for (final Element element :
151+
round.getElementsAnnotatedWith(typeElement(TagsPrism.PRISM_TYPE))) {
107152
doc().addTagsDefinition(element);
108153
}
109154
}
110155

111156
private void readSecuritySchemes(RoundEnvironment round) {
112-
for (final Element element : round.getElementsAnnotatedWith(typeElement(SecuritySchemePrism.PRISM_TYPE))) {
157+
for (final Element element :
158+
round.getElementsAnnotatedWith(typeElement(SecuritySchemePrism.PRISM_TYPE))) {
113159
doc().addSecurityScheme(element);
114160
}
115-
for (final Element element : round.getElementsAnnotatedWith(typeElement(SecuritySchemesPrism.PRISM_TYPE))) {
161+
for (final Element element :
162+
round.getElementsAnnotatedWith(typeElement(SecuritySchemesPrism.PRISM_TYPE))) {
116163
doc().addSecuritySchemes(element);
117164
}
118165
}
@@ -121,31 +168,42 @@ private void writeOpenAPI() {
121168
doc().writeApi();
122169
}
123170

124-
private void writeAdapter(Element controller) {
125-
if (controller instanceof TypeElement) {
126-
final var packageFQN = elements().getPackageOf(controller).getQualifiedName().toString();
127-
final var contextPath = Util.combinePath(contextPathString, packagePath(packageFQN));
128-
final var reader = new ControllerReader((TypeElement) controller, contextPath);
129-
reader.read(true);
130-
try {
131-
writeControllerAdapter(reader);
132-
} catch (final Throwable e) {
133-
logError(reader.beanType(), "Failed to write $Route class " + e);
171+
private void writeAdapter(TypeElement controller) {
172+
final var packageFQN = elements().getPackageOf(controller).getQualifiedName().toString();
173+
final var contextPath = Util.combinePath(contextPathString, packagePath(packageFQN));
174+
final var reader = new ControllerReader(controller, contextPath);
175+
reader.read(true);
176+
try {
177+
178+
writeControllerAdapter(reader);
179+
writeClientAdapter(reader);
180+
181+
} catch (final Throwable e) {
182+
logError(reader.beanType(), "Failed to write $Route class " + e);
183+
}
184+
}
185+
186+
private void writeClientAdapter(ControllerReader reader) {
187+
188+
try {
189+
if (reader.beanType().getInterfaces().isEmpty()
190+
&& "java.lang.Object".equals(reader.beanType().getSuperclass().toString())) {
191+
new TestClientWriter(reader).write();
192+
clientFQNs.add(reader.beanType().getQualifiedName().toString() + "TestAPI");
134193
}
194+
} catch (final IOException e) {
195+
logError(reader.beanType(), "Failed to write $Route class " + e);
135196
}
136197
}
137198

138199
private String packagePath(String packageFQN) {
139200
return packagePaths.entrySet().stream()
140-
.filter(k -> packageFQN.startsWith(k.getKey()))
141-
.map(Entry::getValue)
142-
.reduce(Util::combinePath)
143-
.orElse(null);
201+
.filter(k -> packageFQN.startsWith(k.getKey()))
202+
.map(Entry::getValue)
203+
.reduce(Util::combinePath)
204+
.orElse(null);
144205
}
145206

146-
/**
147-
* Write the adapter code for the given controller.
148-
*/
207+
/** Write the adapter code for the given controller. */
149208
public abstract void writeControllerAdapter(ControllerReader reader) throws IOException;
150-
151209
}

http-generator-core/src/main/java/io/avaje/http/generator/core/PrimitiveUtil.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ private PrimitiveUtil() {}
1515
"short", "Short",
1616
"double", "Double",
1717
"float", "Float",
18-
"boolean", "Boolean");
18+
"boolean", "Boolean",
19+
"void", "Void");
1920

2021
public static String wrap(String shortName) {
2122
final var wrapped = wrapperMap.get(shortName);

0 commit comments

Comments
 (0)