Skip to content

Commit f05ea90

Browse files
authored
Merge pull request #229 from SentryMan/client-extends
Extendable Clients
2 parents 8dcb19f + 9b16729 commit f05ea90

File tree

7 files changed

+109
-30
lines changed

7 files changed

+109
-30
lines changed

.lift.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ignoreFiles="""
2+
tests/**
3+
"""

http-api/src/main/java/io/avaje/http/api/Client.java

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,54 @@
1111
* Marker annotation for client.
1212
*
1313
* <pre>{@code
14+
* @Client
15+
* interface CustomerApi {
16+
* ...
17+
* @Get("/{id}")
18+
* Customer getById(long id);
1419
*
15-
* @Client
16-
* interface CustomerApi {
17-
* ...
18-
* @Get("/{id}")
19-
* Customer getById(long id);
20-
*
21-
* @Post
22-
* long save(Customer customer);
23-
* }
20+
* @Post
21+
* long save(Customer customer);
22+
* }
2423
*
2524
* }</pre>
2625
*
2726
* <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.
3427
*
35-
* <pre>{@code
28+
* <p>When the client interface already exists in another module we use <code>Client.Import</code>
29+
* to generate the client.
3630
*
37-
* @Client.Import(types = OtherApi.class)
38-
* package org.example;
31+
* <p>Specify the <code>@Client.Import</code> on the package or class to refer to the client
32+
* interface we want to generate.
33+
*
34+
* <pre>{@code
35+
* @Client.Import(types = OtherApi.class)
36+
* package org.example;
3937
*
4038
* }</pre>
4139
*/
4240
@Target(value = TYPE)
4341
@Retention(value = RUNTIME)
4442
public @interface Client {
4543

44+
/**
45+
* Flag to set whether to generate a Client Implementation. Set false if the interface exists merely to be extended by
46+
* other client interfaces
47+
*/
48+
boolean generate() default true;
49+
50+
/**
51+
* Specify <code>@Client.Import</code> on a package or class to refer to the client interface we
52+
* want to generate.
53+
*
54+
* <pre>{@code
55+
* @Client.Import(types = OtherApi.class)
56+
* package org.example;
57+
*
58+
* }</pre>
59+
*/
4660
@Target(value = {TYPE, PACKAGE})
47-
@Retention(value = RUNTIME)
61+
@Retention(RUNTIME)
4862
@interface Import {
4963

5064
/**

http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
6565
readModule();
6666
for (final Element controller :
6767
round.getElementsAnnotatedWith(typeElement(ClientPrism.PRISM_TYPE))) {
68-
writeClient(controller);
68+
if (ClientPrism.getInstanceOn(controller).generate()) {
69+
writeClient(controller);
70+
}
6971
}
7072
for (final var importedElement : round.getElementsAnnotatedWith(typeElement(ImportPrism.PRISM_TYPE))) {
7173
writeForImported(importedElement);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.avaje.http.generator.client.clients;
2+
3+
import io.avaje.http.api.Client;
4+
import io.avaje.http.api.Patch;
5+
6+
@Client
7+
public interface ExampleClient extends UserClient {
8+
@Patch
9+
void patchy();
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.avaje.http.generator.client.clients;
2+
3+
import io.avaje.http.api.Client;
4+
import io.avaje.http.api.Patch;
5+
6+
@Client
7+
public interface ExampleClient2 extends ExampleClient {
8+
@Patch
9+
void patchy2();
10+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.avaje.http.generator.client.clients;
2+
3+
import io.avaje.http.api.BodyString;
4+
import io.avaje.http.api.Client;
5+
import io.avaje.http.api.Get;
6+
import io.avaje.http.api.Post;
7+
8+
@Client(generate = false)
9+
public interface UserClient {
10+
11+
@Post("/users")
12+
String createUser(@BodyString String body);
13+
14+
@Get("/users/{userId}")
15+
String getUserById(String userId);
16+
}

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

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import static java.util.function.Predicate.not;
1111

1212
import java.util.ArrayList;
13+
import java.util.HashSet;
14+
import java.util.Iterator;
1315
import java.util.List;
1416
import java.util.Optional;
1517
import java.util.Set;
@@ -31,10 +33,11 @@
3133
public final class ControllerReader {
3234

3335
private final TypeElement beanType;
34-
private final List<Element> interfaces;
36+
private final List<TypeElement> interfaces;
3537
private final List<ExecutableElement> interfaceMethods;
3638
private final List<String> roles;
3739
private final List<MethodReader> methods = new ArrayList<>();
40+
private final Set<String> seenMethods = new HashSet<>();
3841
private final Set<String> staticImportTypes = new TreeSet<>();
3942
private final Set<String> importTypes = new TreeSet<>();
4043
private final List<OpenAPIResponsePrism> apiResponses;
@@ -54,7 +57,7 @@ public final class ControllerReader {
5457

5558
public ControllerReader(TypeElement beanType) {
5659
this.beanType = beanType;
57-
this.interfaces = initInterfaces();
60+
this.interfaces = initInterfaces(beanType);
5861
this.interfaceMethods = initInterfaceMethods();
5962
this.roles = buildRoles();
6063
if (isOpenApiAvailable()) {
@@ -115,17 +118,18 @@ void addImports(boolean withSingleton) {
115118
}
116119
}
117120

118-
private List<Element> initInterfaces() {
119-
final List<Element> interfaces = new ArrayList<>();
120-
for (final TypeMirror anInterface : beanType.getInterfaces()) {
121-
final Element ifaceElement = asElement(anInterface);
121+
private List<TypeElement> initInterfaces(TypeElement element) {
122+
final List<TypeElement> superInterfaces = new ArrayList<>();
123+
for (final TypeMirror anInterface : element.getInterfaces()) {
124+
final var ifaceElement = asElement(anInterface);
122125
final var controller = ControllerPrism.getInstanceOn(ifaceElement);
123126
if (controller != null && !controller.value().isBlank()
124-
|| PathPrism.isPresent(ifaceElement)) {
125-
interfaces.add(ifaceElement);
127+
|| PathPrism.isPresent(ifaceElement)
128+
|| ClientPrism.isPresent(ifaceElement)) {
129+
superInterfaces.add(ifaceElement);
126130
}
127131
}
128-
return interfaces;
132+
return superInterfaces;
129133
}
130134

131135
private List<ExecutableElement> initInterfaceMethods() {
@@ -221,6 +225,12 @@ public void read(boolean withSingleton) {
221225
}
222226
}
223227
readSuper(beanType);
228+
229+
if (platform().getClass().getSimpleName().contains("Client")) {
230+
for (final var superInterface : interfaces) {
231+
readInterfaces(superInterface);
232+
}
233+
}
224234
deriveIncludeValidation();
225235
addImports(withSingleton);
226236
}
@@ -268,6 +278,20 @@ private void readSuper(TypeElement beanType) {
268278
}
269279
}
270280

281+
/**
282+
* Read methods from interfaces taking into account generics.
283+
*
284+
* @param interfaceElement
285+
*/
286+
private void readInterfaces(TypeElement interfaceElement) {
287+
for (final var element : ElementFilter.methodsIn(interfaceElement.getEnclosedElements())) {
288+
readMethod(element, (DeclaredType) interfaceElement.asType());
289+
}
290+
for (final var element : initInterfaces(interfaceElement)) {
291+
readInterfaces(element);
292+
}
293+
}
294+
271295
private void readMethod(ExecutableElement element) {
272296
readMethod(element, null);
273297
}
@@ -279,7 +303,7 @@ private void readMethod(ExecutableElement method, DeclaredType declaredType) {
279303
actualExecutable = (ExecutableType) asMemberOf(declaredType, method);
280304
}
281305
final MethodReader methodReader = new MethodReader(this, method, actualExecutable);
282-
if (methodReader.isWebMethod()) {
306+
if (methodReader.isWebMethod() && seenMethods.add(method.toString())) {
283307
methodReader.read();
284308
methods.add(methodReader);
285309
}

0 commit comments

Comments
 (0)