Skip to content

Commit

Permalink
Merge pull request #62 from sashirestela/61-allow-composite-of-stream…
Browse files Browse the repository at this point in the history
…type-annotations

Allow composite of StreamType annotations
  • Loading branch information
sashirestela authored Mar 23, 2024
2 parents 3337bac + 7b0a775 commit 4228bac
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 28 deletions.
42 changes: 22 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Library that makes it easy to use the Java HttpClient to perform http operations

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.

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```:
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```:

```java
// Entity
Expand Down Expand Up @@ -152,37 +152,39 @@ var cleverClient = CleverClient.builder()

### Interface Annotations

| Annotation | Target | Attributes | Required Attrs | Mult |
|------------|-----------|-----------------------------|----------------|------|
| Resource | Interface | Resource's url | optional | One |
| Header | Interface | Header's name and value | mandatory both | Many |
| Header | Method | Header's name and value | mandatory both | Many |
| GET | Method | GET endpoint's url | optional | One |
| POST | Method | POST endpoint's url | optional | One |
| PUT | Method | PUT endpoint's url | optional | One |
| DELETE | Method | DELETE endpoint's url | optional | One |
| Multipart | Method | (None) | none | One |
| StreamType | Method | class type and events array | mandatory both | Many |
| Path | Parameter | Path parameter name in url | mandatory | One |
| Query | Parameter | Query parameter name in url | mandatory | One |
| Query | Parameter | (None for Pojos) | none | One |
| Body | Parameter | (None) | none | One |
| Annotation | Target | Attributes | Required Attrs | Mult |
|------------|------------|-----------------------------|----------------|------|
| Resource | Interface | Resource's url | optional | One |
| Header | Interface | Header's name and value | mandatory both | Many |
| Header | Method | Header's name and value | mandatory both | Many |
| GET | Method | GET endpoint's url | optional | One |
| POST | Method | POST endpoint's url | optional | One |
| PUT | Method | PUT endpoint's url | optional | One |
| DELETE | Method | DELETE endpoint's url | optional | One |
| PATCH | Method | PATCH endpoint's url | optional | One |
| Multipart | Method | (None) | none | One |
| StreamType | Method | Class type and events array | mandatory both | Many |
| StreamType | Annotation | Class type and events array | mandatory both | Many |
| Path | Parameter | Path parameter name in url | mandatory | One |
| Query | Parameter | Query parameter name in url | mandatory | One |
| Query | Parameter | (None for Pojos) | none | One |
| Body | Parameter | (None) | none | One |

* ```Resource``` could be used to separate the repeated part of the endpoints' url in an interface.
* ```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.
* ```GET, POST, PUT, DELETE``` are used to mark the typical http methods (endpoints).
* ```Multipart``` is used to mark an endpoint with a multipart/form-data request. This is required when you need to upload files.
* ```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).
* ```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.
* ```Path``` is used to replace the path parameter name in url with the matched method parameter's value.
* ```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.
* ```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.
* 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.

### Supported Response Types

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).
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).

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

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

### Interface Default Methods

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.sashirestela</groupId>
<artifactId>cleverclient</artifactId>
<version>1.3.1</version>
<version>1.3.2</version>
<packaging>jar</packaging>

<name>cleverclient</name>
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/io/github/sashirestela/cleverclient/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import lombok.Builder;
import lombok.Value;

/**
* Represents every event in a Server Sent Event interaction.
*/
@Value
@Builder
public class Event {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Repeatable(List.class)
public @interface StreamType {

Expand All @@ -21,7 +21,7 @@

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@interface List {

StreamType[] value();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
Expand Down Expand Up @@ -44,13 +45,32 @@ public ReturnType(Method method) {
private void setClassByEventIfExists(Method method) {
if (method.isAnnotationPresent(StreamType.List.class)) {
this.classByEvent = calculateClassByEvent(
method.getDeclaredAnnotationsByType(StreamType.List.class)[0].value());
method.getDeclaredAnnotation(StreamType.List.class).value());
} else if (method.isAnnotationPresent(StreamType.class)) {
this.classByEvent = calculateClassByEvent(
new StreamType[] { method.getDeclaredAnnotation(StreamType.class) });
} else {
var annotations = method.getDeclaredAnnotations();
if (isAnnotationPresent(annotations, StreamType.List.class)) {
this.classByEvent = calculateClassByEvent(
Arrays.stream(annotations)
.map(a -> a.annotationType().getDeclaredAnnotation(StreamType.List.class).value())
.findFirst()
.orElse(null));
} else if (isAnnotationPresent(annotations, StreamType.class)) {
this.classByEvent = calculateClassByEvent(
new StreamType[] { Arrays.stream(annotations)
.map(a -> a.annotationType().getDeclaredAnnotation(StreamType.class))
.findFirst()
.orElse(null) });
}
}
}

private boolean isAnnotationPresent(Annotation[] annotations, Class<? extends Annotation> clazz) {
return Arrays.stream(annotations).anyMatch(a -> a.annotationType().isAnnotationPresent(clazz));
}

private Map<String, Class<?>> calculateClassByEvent(StreamType[] streamTypeList) {
Map<String, Class<?>> map = new ConcurrentHashMap<>();
Arrays.stream(streamTypeList).forEach(streamType -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.sashirestela.cleverclient.support;

import io.github.sashirestela.cleverclient.annotation.StreamType;
import io.github.sashirestela.cleverclient.support.ReturnTypeTest.First;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@StreamType(type = First.class, events = { "first.create", "first.complete" })
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CompositeOne {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.github.sashirestela.cleverclient.support;

import io.github.sashirestela.cleverclient.annotation.StreamType;
import io.github.sashirestela.cleverclient.support.ReturnTypeTest.First;
import io.github.sashirestela.cleverclient.support.ReturnTypeTest.Second;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@StreamType(type = First.class, events = { "first.create", "first.complete" })
@StreamType(type = Second.class, events = { "second.create" })
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CompositeTwo {
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.github.sashirestela.cleverclient.support;

import io.github.sashirestela.cleverclient.Event;
import io.github.sashirestela.cleverclient.annotation.StreamType;
import org.junit.jupiter.api.Test;

import java.io.InputStream;
Expand Down Expand Up @@ -63,7 +62,7 @@ void shouldReturnNullCategoryWhenMethodReturnTypeIsNotExpected() throws NoSuchMe
}

@Test
void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithStreamType()
void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithCompositeMultiStreamType()
throws NoSuchMethodException, SecurityException {
var method = TestInterface.class.getMethod("asyncStreamEventMethod", new Class[] {});
var returnType = new ReturnType(method);
Expand All @@ -75,10 +74,21 @@ void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithStreamType()
assertEquals(Boolean.TRUE, expectedMap.equals(actualMap));
}

@Test
void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithCompositeSingleStreamType()
throws NoSuchMethodException, SecurityException {
var method = TestInterface.class.getMethod("syncStreamEventMethod", new Class[] {});
var returnType = new ReturnType(method);
var actualMap = returnType.getClassByEvent();
var expectedMap = new ConcurrentHashMap<>();
expectedMap.put("first.create", First.class);
expectedMap.put("first.complete", First.class);
assertEquals(Boolean.TRUE, expectedMap.equals(actualMap));
}

static interface TestInterface {

@StreamType(type = First.class, events = { "first.create", "first.complete" })
@StreamType(type = Second.class, events = { "second.create" })
@CompositeTwo
CompletableFuture<Stream<Event>> asyncStreamEventMethod();

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

CompletableFuture<Set<MyClass>> asyncSetMethod();

@CompositeOne
Stream<Event> syncStreamEventMethod();

Stream<MyClass> syncStreamMethod();
Expand Down

0 comments on commit 4228bac

Please sign in to comment.