Skip to content

Commit 4228bac

Browse files
authored
Merge pull request #62 from sashirestela/61-allow-composite-of-streamtype-annotations
Allow composite of StreamType annotations
2 parents 3337bac + 7b0a775 commit 4228bac

File tree

8 files changed

+96
-28
lines changed

8 files changed

+96
-28
lines changed

README.md

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Library that makes it easy to use the Java HttpClient to perform http operations
2323

2424
CleverClient is a Java 11+ library that makes it easy to use the standard [HttpClient](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html) component to call http services by using annotated interfaces.
2525

26-
For example, if we want to use the public API [JsonPlaceHolder](https://jsonplaceholder.typicode.com/) and call the endpoint ```/posts```, we just have to create an entity ```Post```, an interface ```PostService``` with special annotatons, and call the API through ```CleverClient```:
26+
For example, if we want to use the public API [JsonPlaceHolder](https://jsonplaceholder.typicode.com/) and call its endpoint ```/posts```, we just have to create an entity ```Post```, an interface ```PostService``` with special annotatons, and call the API through ```CleverClient```:
2727

2828
```java
2929
// Entity
@@ -152,37 +152,39 @@ var cleverClient = CleverClient.builder()
152152

153153
### Interface Annotations
154154

155-
| Annotation | Target | Attributes | Required Attrs | Mult |
156-
|------------|-----------|-----------------------------|----------------|------|
157-
| Resource | Interface | Resource's url | optional | One |
158-
| Header | Interface | Header's name and value | mandatory both | Many |
159-
| Header | Method | Header's name and value | mandatory both | Many |
160-
| GET | Method | GET endpoint's url | optional | One |
161-
| POST | Method | POST endpoint's url | optional | One |
162-
| PUT | Method | PUT endpoint's url | optional | One |
163-
| DELETE | Method | DELETE endpoint's url | optional | One |
164-
| Multipart | Method | (None) | none | One |
165-
| StreamType | Method | class type and events array | mandatory both | Many |
166-
| Path | Parameter | Path parameter name in url | mandatory | One |
167-
| Query | Parameter | Query parameter name in url | mandatory | One |
168-
| Query | Parameter | (None for Pojos) | none | One |
169-
| Body | Parameter | (None) | none | One |
155+
| Annotation | Target | Attributes | Required Attrs | Mult |
156+
|------------|------------|-----------------------------|----------------|------|
157+
| Resource | Interface | Resource's url | optional | One |
158+
| Header | Interface | Header's name and value | mandatory both | Many |
159+
| Header | Method | Header's name and value | mandatory both | Many |
160+
| GET | Method | GET endpoint's url | optional | One |
161+
| POST | Method | POST endpoint's url | optional | One |
162+
| PUT | Method | PUT endpoint's url | optional | One |
163+
| DELETE | Method | DELETE endpoint's url | optional | One |
164+
| PATCH | Method | PATCH endpoint's url | optional | One |
165+
| Multipart | Method | (None) | none | One |
166+
| StreamType | Method | Class type and events array | mandatory both | Many |
167+
| StreamType | Annotation | Class type and events array | mandatory both | Many |
168+
| Path | Parameter | Path parameter name in url | mandatory | One |
169+
| Query | Parameter | Query parameter name in url | mandatory | One |
170+
| Query | Parameter | (None for Pojos) | none | One |
171+
| Body | Parameter | (None) | none | One |
170172

171173
* ```Resource``` could be used to separate the repeated part of the endpoints' url in an interface.
172174
* ```Header``` Used to include more headers (pairs of name and value) at interface or method level. It is possible to have multiple Header annotations for the same target.
173175
* ```GET, POST, PUT, DELETE``` are used to mark the typical http methods (endpoints).
174176
* ```Multipart``` is used to mark an endpoint with a multipart/form-data request. This is required when you need to upload files.
175-
* ```StreamType``` is used with methods whose return type is Stream of [Event](./src/main/java/io/github/sashirestela/cleverclient/Event.java). Commonly you will use more than one to indicate what class (type) is related to what events (array of Strings).
177+
* ```StreamType``` is used with methods whose return type is Stream of [Event](./src/main/java/io/github/sashirestela/cleverclient/Event.java). Tipically you will use more than one of this annotation to indicate what classes (types) are related to what events (array of Strings). You can also use them for custom annotations in case you want to reuse them for many methods, so you just apply the custom composite annotation.
176178
* ```Path``` is used to replace the path parameter name in url with the matched method parameter's value.
177179
* ```Query``` is used to add a query parameter to the url in the way: [?]queryValue=parameterValue[&...] for scalar parameters. Also it can be used for POJOs using its properties and values.
178180
* ```Body``` is used to mark a method parameter as the endpoint's payload request, so the request will be application/json at least the endpoint is annotated with Multipart.
179181
* Check the above [Description's example](#-description) or the [Test](https://github.com/sashirestela/cleverclient/tree/main/src/test/java/io/github/sashirestela/cleverclient) folder to see more of these interface annotations in action.
180182

181183
### Supported Response Types
182184

183-
The reponse types are determined from the method responses. We don't need any annotation for that. We have six response types: [Stream](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/Stream.html) of elements, [List](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/List.html) of elements, [Generic](https://docs.oracle.com/javase/tutorial/java/generics/types.html) type, Custom type, [Binary](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/InputStream.html) type, [String](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html) type and Stream of [Event](./src/main/java/io/github/sashirestela/cleverclient/Event.java), and all of them can be asynchronous or synchronous. For async responses you have to use the Java class [CompletableFuture](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/CompletableFuture.html).
185+
The reponse types are determined from the method's return types. We have six response types: [Stream](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/Stream.html) of elements, [List](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/List.html) of elements, [Generic](https://docs.oracle.com/javase/tutorial/java/generics/types.html) type, Custom type, [Binary](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/InputStream.html) type, [String](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html) type and Stream of [Event](./src/main/java/io/github/sashirestela/cleverclient/Event.java), and all of them can be asynchronous or synchronous. For async responses you have to use the Java class [CompletableFuture](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/CompletableFuture.html).
184186

185-
| Method's Response Type | Sync/Async | Description |
187+
| Response Type | Sync/Async | Description |
186188
|------------------------------------|------------|-----------------------------|
187189
| CompletableFuture<Stream\<T>> | Async | SSE (*) as Stream of type T |
188190
| Stream\<T> | Sync | SSE (*) as Stream of type T |
@@ -203,7 +205,7 @@ The reponse types are determined from the method responses. We don't need any an
203205

204206
* ```CompletableFuture<Stream<T>>``` and ```Stream<T>``` are used for handling SSE without events and data of the class ```T``` only.
205207
* ```CompletableFuture<Stream<Event>>``` and ```Stream<Event>``` are used for handling SSE with multiple events and data of different classes.
206-
* The [Event](./src/main/java/io/github/sashirestela/cleverclient/Event.java) class will bring the event name and the data object.
208+
* The [Event](./src/main/java/io/github/sashirestela/cleverclient/Event.java) class will bring for each event: the event name and the data object.
207209

208210
### Interface Default Methods
209211

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>io.github.sashirestela</groupId>
88
<artifactId>cleverclient</artifactId>
9-
<version>1.3.1</version>
9+
<version>1.3.2</version>
1010
<packaging>jar</packaging>
1111

1212
<name>cleverclient</name>

src/main/java/io/github/sashirestela/cleverclient/Event.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import lombok.Builder;
44
import lombok.Value;
55

6+
/**
7+
* Represents every event in a Server Sent Event interaction.
8+
*/
69
@Value
710
@Builder
811
public class Event {

src/main/java/io/github/sashirestela/cleverclient/annotation/StreamType.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
@Documented
1313
@Retention(RetentionPolicy.RUNTIME)
14-
@Target(ElementType.METHOD)
14+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
1515
@Repeatable(List.class)
1616
public @interface StreamType {
1717

@@ -21,7 +21,7 @@
2121

2222
@Documented
2323
@Retention(RetentionPolicy.RUNTIME)
24-
@Target(ElementType.METHOD)
24+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
2525
@interface List {
2626

2727
StreamType[] value();

src/main/java/io/github/sashirestela/cleverclient/support/ReturnType.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.github.sashirestela.cleverclient.annotation.StreamType;
44

5+
import java.lang.annotation.Annotation;
56
import java.lang.reflect.Method;
67
import java.util.Arrays;
78
import java.util.Map;
@@ -44,13 +45,32 @@ public ReturnType(Method method) {
4445
private void setClassByEventIfExists(Method method) {
4546
if (method.isAnnotationPresent(StreamType.List.class)) {
4647
this.classByEvent = calculateClassByEvent(
47-
method.getDeclaredAnnotationsByType(StreamType.List.class)[0].value());
48+
method.getDeclaredAnnotation(StreamType.List.class).value());
4849
} else if (method.isAnnotationPresent(StreamType.class)) {
4950
this.classByEvent = calculateClassByEvent(
5051
new StreamType[] { method.getDeclaredAnnotation(StreamType.class) });
52+
} else {
53+
var annotations = method.getDeclaredAnnotations();
54+
if (isAnnotationPresent(annotations, StreamType.List.class)) {
55+
this.classByEvent = calculateClassByEvent(
56+
Arrays.stream(annotations)
57+
.map(a -> a.annotationType().getDeclaredAnnotation(StreamType.List.class).value())
58+
.findFirst()
59+
.orElse(null));
60+
} else if (isAnnotationPresent(annotations, StreamType.class)) {
61+
this.classByEvent = calculateClassByEvent(
62+
new StreamType[] { Arrays.stream(annotations)
63+
.map(a -> a.annotationType().getDeclaredAnnotation(StreamType.class))
64+
.findFirst()
65+
.orElse(null) });
66+
}
5167
}
5268
}
5369

70+
private boolean isAnnotationPresent(Annotation[] annotations, Class<? extends Annotation> clazz) {
71+
return Arrays.stream(annotations).anyMatch(a -> a.annotationType().isAnnotationPresent(clazz));
72+
}
73+
5474
private Map<String, Class<?>> calculateClassByEvent(StreamType[] streamTypeList) {
5575
Map<String, Class<?>> map = new ConcurrentHashMap<>();
5676
Arrays.stream(streamTypeList).forEach(streamType -> {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.github.sashirestela.cleverclient.support;
2+
3+
import io.github.sashirestela.cleverclient.annotation.StreamType;
4+
import io.github.sashirestela.cleverclient.support.ReturnTypeTest.First;
5+
6+
import java.lang.annotation.ElementType;
7+
import java.lang.annotation.Retention;
8+
import java.lang.annotation.RetentionPolicy;
9+
import java.lang.annotation.Target;
10+
11+
@StreamType(type = First.class, events = { "first.create", "first.complete" })
12+
@Retention(RetentionPolicy.RUNTIME)
13+
@Target(ElementType.METHOD)
14+
public @interface CompositeOne {
15+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.github.sashirestela.cleverclient.support;
2+
3+
import io.github.sashirestela.cleverclient.annotation.StreamType;
4+
import io.github.sashirestela.cleverclient.support.ReturnTypeTest.First;
5+
import io.github.sashirestela.cleverclient.support.ReturnTypeTest.Second;
6+
7+
import java.lang.annotation.ElementType;
8+
import java.lang.annotation.Retention;
9+
import java.lang.annotation.RetentionPolicy;
10+
import java.lang.annotation.Target;
11+
12+
@StreamType(type = First.class, events = { "first.create", "first.complete" })
13+
@StreamType(type = Second.class, events = { "second.create" })
14+
@Retention(RetentionPolicy.RUNTIME)
15+
@Target(ElementType.METHOD)
16+
public @interface CompositeTwo {
17+
}

src/test/java/io/github/sashirestela/cleverclient/support/ReturnTypeTest.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.github.sashirestela.cleverclient.support;
22

33
import io.github.sashirestela.cleverclient.Event;
4-
import io.github.sashirestela.cleverclient.annotation.StreamType;
54
import org.junit.jupiter.api.Test;
65

76
import java.io.InputStream;
@@ -63,7 +62,7 @@ void shouldReturnNullCategoryWhenMethodReturnTypeIsNotExpected() throws NoSuchMe
6362
}
6463

6564
@Test
66-
void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithStreamType()
65+
void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithCompositeMultiStreamType()
6766
throws NoSuchMethodException, SecurityException {
6867
var method = TestInterface.class.getMethod("asyncStreamEventMethod", new Class[] {});
6968
var returnType = new ReturnType(method);
@@ -75,10 +74,21 @@ void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithStreamType()
7574
assertEquals(Boolean.TRUE, expectedMap.equals(actualMap));
7675
}
7776

77+
@Test
78+
void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithCompositeSingleStreamType()
79+
throws NoSuchMethodException, SecurityException {
80+
var method = TestInterface.class.getMethod("syncStreamEventMethod", new Class[] {});
81+
var returnType = new ReturnType(method);
82+
var actualMap = returnType.getClassByEvent();
83+
var expectedMap = new ConcurrentHashMap<>();
84+
expectedMap.put("first.create", First.class);
85+
expectedMap.put("first.complete", First.class);
86+
assertEquals(Boolean.TRUE, expectedMap.equals(actualMap));
87+
}
88+
7889
static interface TestInterface {
7990

80-
@StreamType(type = First.class, events = { "first.create", "first.complete" })
81-
@StreamType(type = Second.class, events = { "second.create" })
91+
@CompositeTwo
8292
CompletableFuture<Stream<Event>> asyncStreamEventMethod();
8393

8494
CompletableFuture<Stream<MyClass>> asyncStreamMethod();
@@ -95,6 +105,7 @@ static interface TestInterface {
95105

96106
CompletableFuture<Set<MyClass>> asyncSetMethod();
97107

108+
@CompositeOne
98109
Stream<Event> syncStreamEventMethod();
99110

100111
Stream<MyClass> syncStreamMethod();

0 commit comments

Comments
 (0)