Skip to content

Commit 16ebf29

Browse files
committed
#55 - Support client generation with @client and @Client.Import
Initial support, still need to generate client code for headers, formParams and body
1 parent 862cfde commit 16ebf29

File tree

28 files changed

+782
-29
lines changed

28 files changed

+782
-29
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.avaje.http.api;
2+
3+
import java.lang.annotation.Retention;
4+
import java.lang.annotation.Target;
5+
6+
import static java.lang.annotation.ElementType.PACKAGE;
7+
import static java.lang.annotation.ElementType.TYPE;
8+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
9+
10+
/**
11+
* Marker annotation for client.
12+
*
13+
* <pre>{@code
14+
*
15+
* @Client
16+
* interface CustomerApi {
17+
* ...
18+
* @Get("/{id}")
19+
* Customer getById(long id);
20+
*
21+
* @Post
22+
* long save(Customer customer);
23+
* }
24+
*
25+
* }</pre>
26+
*
27+
* <h3>Client.Import</h3>
28+
* <p>
29+
* When the client interface already exists in another module we
30+
* use <code>Client.Import</code> to generate the client.
31+
* <p>
32+
* Specify the <code>@Client.Import</code> on the package or class
33+
* to refer to the client interface we want to generate.
34+
*
35+
* <pre>{@code
36+
*
37+
* @Client.Import(types = OtherApi.class)
38+
* package org.example;
39+
*
40+
* }</pre>
41+
*/
42+
@Target(value = TYPE)
43+
@Retention(value = RUNTIME)
44+
public @interface Client {
45+
46+
@Target(value = {TYPE, PACKAGE})
47+
@Retention(value = RUNTIME)
48+
@interface Import {
49+
50+
/**
51+
* Client interface types that we want to generate HTTP clients for.
52+
*/
53+
Class<?>[] types();
54+
}
55+
}

http-generator-client/pom.xml

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,45 @@
22
<project xmlns="http://maven.apache.org/POM/4.0.0"
33
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
44
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
57
<parent>
6-
<artifactId>avaje-http-generator-parent</artifactId>
78
<groupId>io.avaje</groupId>
8-
<version>1.22-SNAPSHOT</version>
9+
<artifactId>avaje-http-generator-parent</artifactId>
10+
<version>1.4-SNAPSHOT</version>
11+
<relativePath>..</relativePath>
912
</parent>
10-
<modelVersion>4.0.0</modelVersion>
1113

12-
<artifactId>generator-client</artifactId>
14+
<artifactId>avaje-http-generator-client</artifactId>
15+
16+
<properties>
17+
<java.version>11</java.version>
18+
</properties>
19+
20+
<dependencies>
21+
22+
<dependency>
23+
<groupId>io.avaje</groupId>
24+
<artifactId>avaje-http-generator-core</artifactId>
25+
<version>${project.version}</version>
26+
</dependency>
27+
28+
</dependencies>
1329

30+
<build>
31+
<plugins>
32+
<plugin>
33+
<groupId>org.apache.maven.plugins</groupId>
34+
<artifactId>maven-compiler-plugin</artifactId>
35+
<version>3.2</version>
36+
<configuration>
37+
<source>11</source>
38+
<target>11</target>
39+
<!-- Turn off annotation processing for building -->
40+
<compilerArgument>-proc:none</compilerArgument>
41+
</configuration>
42+
</plugin>
43+
</plugins>
44+
</build>
1445

1546
</project>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package io.avaje.http.generator.client;
2+
3+
import io.avaje.http.generator.core.*;
4+
5+
import java.util.List;
6+
import java.util.Set;
7+
8+
/**
9+
* Write code to register Web route for a given controller method.
10+
*/
11+
class ClientMethodWriter {
12+
13+
private static final KnownResponse KNOWN_RESPONSE = new KnownResponse();
14+
private final MethodReader method;
15+
private final Append writer;
16+
private final WebMethod webMethod;
17+
private final ProcessingContext ctx;
18+
private final UType returnType;
19+
20+
ClientMethodWriter(MethodReader method, Append writer, ProcessingContext ctx) {
21+
this.method = method;
22+
this.writer = writer;
23+
this.webMethod = method.getWebMethod();
24+
this.ctx = ctx;
25+
this.returnType = Util.parseType(method.getReturnType());
26+
}
27+
28+
void addImportTypes(ControllerReader reader) {
29+
reader.addImportTypes(returnType.importTypes());
30+
for (MethodParam param : method.getParams()) {
31+
param.addImports(reader);
32+
}
33+
}
34+
35+
private void methodStart(Append writer) {
36+
writer.append(" // %s %s", method.getWebMethod(), method.getWebMethodPath()).eol();
37+
writer.append(" @Override").eol();
38+
writer.append(" public %s %s(", returnType.shortType(), method.simpleName());
39+
int count = 0;
40+
for (MethodParam param : method.getParams()) {
41+
if (count++ > 0) {
42+
writer.append(", ");
43+
}
44+
writer.append(param.getShortType()).append(" ");
45+
writer.append(param.getName());
46+
}
47+
writer.append(") {").eol();
48+
}
49+
50+
void write() {
51+
methodStart(writer);
52+
writer.append(" ");
53+
if (!method.isVoid()) {
54+
writer.append("return ");
55+
}
56+
writer.append("clientContext.request()").eol();
57+
58+
PathSegments pathSegments = method.getPathSegments();
59+
Set<PathSegments.Segment> segments = pathSegments.getSegments();
60+
if (!segments.isEmpty()) {
61+
writer.append(" ");
62+
}
63+
for (PathSegments.Segment segment : segments) {
64+
if (segment.isLiteral()) {
65+
writer.append(".path(\"").append(segment.literalSection()).append("\")");
66+
} else {
67+
writer.append(".path(").append(segment.name()).append(")");
68+
//TODO: matrix params
69+
}
70+
}
71+
if (!segments.isEmpty()) {
72+
writer.eol();
73+
}
74+
75+
List<MethodParam> params = method.getParams();
76+
for (MethodParam param : params) {
77+
ParamType paramType = param.getParamType();
78+
if (paramType == ParamType.QUERYPARAM) {
79+
PathSegments.Segment segment = pathSegments.segment(param.getParamName());
80+
if (segment == null) {
81+
writer.append(" .queryParam(\"%s\", %s)", param.getParamName(), param.getName()).eol();
82+
}
83+
}
84+
}
85+
86+
// TODO: headers, formParams, body
87+
88+
WebMethod webMethod = method.getWebMethod();
89+
writer.append(" .%s()", webMethod.name().toLowerCase()).eol();
90+
if (returnType == UType.VOID) {
91+
writer.append(" .asDiscarding();").eol();
92+
} else {
93+
String known = KNOWN_RESPONSE.get(returnType.full());
94+
if (known != null) {
95+
writer.append(" %s", known).eol();
96+
} else if (isReturnList()) {
97+
writer.append(" .list(%s.class);", Util.shortName(returnType.param0())).eol();
98+
} else {
99+
writer.append(" .bean(%s.class);", Util.shortName(returnType.full())).eol();
100+
}
101+
}
102+
writer.append(" }").eol().eol();
103+
}
104+
105+
private boolean isReturnList() {
106+
return returnType.shortType().startsWith("List<");
107+
}
108+
109+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.avaje.http.generator.client;
2+
3+
import io.avaje.http.generator.core.Append;
4+
import io.avaje.http.generator.core.ControllerReader;
5+
import io.avaje.http.generator.core.ParamType;
6+
import io.avaje.http.generator.core.PlatformAdapter;
7+
8+
import java.util.List;
9+
10+
class ClientPlatformAdapter implements PlatformAdapter {
11+
12+
@Override
13+
public boolean isContextType(String rawType) {
14+
return false;
15+
}
16+
17+
@Override
18+
public String platformVariable(String rawType) {
19+
return null;
20+
}
21+
22+
@Override
23+
public String bodyAsClass(String shortType) {
24+
return null;
25+
}
26+
27+
@Override
28+
public boolean isBodyMethodParam() {
29+
return false;
30+
}
31+
32+
@Override
33+
public String indent() {
34+
return null;
35+
}
36+
37+
@Override
38+
public void controllerRoles(List<String> roles, ControllerReader controller) {
39+
40+
}
41+
42+
@Override
43+
public void methodRoles(List<String> roles, ControllerReader controller) {
44+
45+
}
46+
47+
@Override
48+
public void writeReadParameter(Append writer, ParamType paramType, String paramName) {
49+
50+
}
51+
52+
@Override
53+
public void writeReadParameter(Append writer, ParamType paramType, String paramName, String paramDefault) {
54+
55+
}
56+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package io.avaje.http.generator.client;
2+
3+
import io.avaje.http.api.Client;
4+
import io.avaje.http.generator.core.ControllerReader;
5+
import io.avaje.http.generator.core.ProcessingContext;
6+
7+
import javax.annotation.processing.AbstractProcessor;
8+
import javax.annotation.processing.ProcessingEnvironment;
9+
import javax.annotation.processing.RoundEnvironment;
10+
import javax.lang.model.SourceVersion;
11+
import javax.lang.model.element.AnnotationMirror;
12+
import javax.lang.model.element.AnnotationValue;
13+
import javax.lang.model.element.Element;
14+
import javax.lang.model.element.TypeElement;
15+
import java.io.IOException;
16+
import java.util.LinkedHashSet;
17+
import java.util.List;
18+
import java.util.Set;
19+
20+
public class ClientProcessor extends AbstractProcessor {
21+
22+
protected ProcessingContext ctx;
23+
24+
@Override
25+
public SourceVersion getSupportedSourceVersion() {
26+
return SourceVersion.latest();
27+
}
28+
29+
@Override
30+
public Set<String> getSupportedAnnotationTypes() {
31+
Set<String> annotations = new LinkedHashSet<>();
32+
annotations.add(Client.class.getCanonicalName());
33+
annotations.add(Client.Import.class.getCanonicalName());
34+
return annotations;
35+
}
36+
37+
@Override
38+
public synchronized void init(ProcessingEnvironment processingEnv) {
39+
super.init(processingEnv);
40+
this.processingEnv = processingEnv;
41+
this.ctx = new ProcessingContext(processingEnv, new ClientPlatformAdapter());
42+
}
43+
44+
@Override
45+
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment round) {
46+
for (Element controller : round.getElementsAnnotatedWith(Client.class)) {
47+
writeClient(controller);
48+
}
49+
for (Element importedElement : round.getElementsAnnotatedWith(Client.Import.class)) {
50+
writeForImported(importedElement);
51+
}
52+
return false;
53+
}
54+
55+
private void writeForImported(Element importedElement) {
56+
for (AnnotationMirror annotationMirror : importedElement.getAnnotationMirrors()) {
57+
for (AnnotationValue value : annotationMirror.getElementValues().values()) {
58+
for (Object apiClassDef : (List<?>) value.getValue()) {
59+
String fullName = apiClassDef.toString();
60+
writeImported(fullName);
61+
}
62+
}
63+
}
64+
}
65+
66+
private void writeImported(String fullName) {
67+
// trim .class suffix
68+
String apiClassName = fullName.substring(0, fullName.length() - 6);
69+
//ctx.logError(null, "build import:" + apiClassName);
70+
TypeElement typeElement = ctx.getTypeElement(apiClassName);
71+
if (typeElement != null) {
72+
writeClient(typeElement);
73+
}
74+
}
75+
76+
private void writeClient(Element controller) {
77+
if (controller instanceof TypeElement) {
78+
ControllerReader reader = new ControllerReader((TypeElement) controller, ctx);
79+
reader.read();
80+
try {
81+
writeClientAdapter(ctx, reader);
82+
} catch (Throwable e) {
83+
e.printStackTrace();
84+
ctx.logError(reader.getBeanType(), "Failed to write client class " + e);
85+
}
86+
}
87+
}
88+
89+
protected void writeClientAdapter(ProcessingContext ctx, ControllerReader reader) throws IOException {
90+
new ClientWriter(reader, ctx).write();
91+
}
92+
93+
}

0 commit comments

Comments
 (0)