Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for handling SSE with multiple events #59

Merged
merged 4 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .sdkmanrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=11.0.15-tem
java=11.0.22-tem
96 changes: 50 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 💎 CleverClient

Java library that makes it easier to use the Java's HttpClient to perform http operations, using interfaces.
Library that makes it easy to use the Java HttpClient to perform http operations through interfaces.

[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=sashirestela_cleverclient&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=sashirestela_cleverclient)
[![codecov](https://codecov.io/gh/sashirestela/cleverclient/graph/badge.svg?token=PEYAFW3EWD)](https://codecov.io/gh/sashirestela/cleverclient)
Expand All @@ -23,7 +23,7 @@ Java library that makes it easier to use the Java's HttpClient to perform http o

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 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 the 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 @@ -115,20 +115,17 @@ We have the following attributes to create a CleverClient object:
| header | Single header as a name and a value | optional |
| httpClient | Java HttpClient object | optional |
| requestInterceptor | Function to modify the request once is built | optional |
| eventsToRead | List of events's name we want to read in streams | optional |
| eventToRead | An event's name we want to read in streams | optional |
| endsOfStream | List of texts used to mark the end of streams | optional |
| endOfStream | Text used to mark the end of streams | optional |

The attributes ```event(s)ToRead``` and ```end(s)OfStream``` are required when you have endpoints sending back streams of data (Server Sent Events - SSE).
The attribute ```end(s)OfStream``` is required when you have endpoints sending back streams of data (Server Sent Events - SSE).

Example:

```java
final var BASE_URL = "https://api.example.com";
final var HEADER_NAME = "Authorization";
final var HEADER_VALUE = "Bearer qwertyasdfghzxcvb";
final var EVENT_TO_READ = "inventory";
final var END_OF_STREAM = "[DONE]";

var httpClient = HttpClient.newBuilder()
Expand All @@ -149,55 +146,62 @@ var cleverClient = CleverClient.builder()
request.setUrl(url);
return request;
})
.eventToRead(EVENT_TO_READ)
.endOfStream(END_OF_STREAM)
.build();
```

### Interface Annotations

| Annotation | Target | Value | Required Value |
|------------|-----------|-----------------------------|----------------|
| Resource | Interface | Resource's url | optional |
| Header | Interface | Header's name and value | mandatory |
| Header | Method | Header's name and value | mandatory |
| GET | Method | GET endpoint's url | optional |
| POST | Method | POST endpoint's url | optional |
| PUT | Method | PUT endpoint's url | optional |
| DELETE | Method | DELETE endpoint's url | optional |
| Multipart | Method | (None) | none |
| Path | Parameter | Path parameter name in url | mandatory |
| Query | Parameter | Query parameter name in url | mandatory |
| Query | Parameter | (None for Pojos) | none |
| Body | Parameter | (None) | none |
| 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 | type class 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 Objects. Commonly you will use more than one to indicate what class (type) is related to what events (array of Strings).
* ```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, Single Class, [Binary](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/InputStream.html) object, and [String](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html) object, 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 Response | Sync/Async | Response Type |
|---------------------------------|------------|----------------------------------------|
| CompletableFuture<Stream\<T>> | Async | Server sent events as Stream of type T |
| Stream\<T> | Sync | Server sent events as Stream of type T |
| CompletableFuture<List\<T>> | Async | List of type T |
| List\<T> | Sync | List of type T |
| CompletableFuture<Generic\<T>> | Async | Generic class of type T |
| Generic\<T> | Sync | Generic class of type T |
| CompletableFuture\<T> | Async | Single class T |
| T | Sync | Single class T |
| CompletableFuture\<InputStream> | Async | Binary Object |
| InputStream | Sync | Binary Object |
| CompletableFuture\<String> | Async | String Object |
| String | Sync | String Object |
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 [Object](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Object.html), 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 |
|------------------------------------|------------|-----------------------------|
| CompletableFuture<Stream\<T>> | Async | SSE (*) as Stream of type T |
| Stream\<T> | Sync | SSE (*) as Stream of type T |
| CompletableFuture<List\<T>> | Async | List of type T |
| List\<T> | Sync | List of type T |
| CompletableFuture<Generic\<T>> | Async | Generic class of type T |
| Generic\<T> | Sync | Generic class of type T |
| CompletableFuture\<T> | Async | Custom class T |
| T | Sync | Custom class T |
| CompletableFuture\<InputStream> | Async | Binary type |
| InputStream | Sync | Binary type |
| CompletableFuture\<String> | Async | String type |
| String | Sync | String type |
| CompletableFuture<Stream\<Object>> | Async | SSE (*) as Stream of Object |
| Stream\<Object> | Sync | SSE (*) as Stream of Object |

(*) SSE: Server Sent Events

```CompletableFuture<Stream<Object>>``` and ```Stream<Object>``` are used for handling SSE with multiple events and classes.

### Interface Default Methods

Expand All @@ -207,26 +211,26 @@ You can create interface default methods to execute special requirements such as
@Resource("/v1/chat/completions")
interface Completions {

@POST
Stream<ChatResponse> __createSyncStream(@Body ChatRequest chatRequest);

default Stream<ChatResponse> createSyncStream(ChatRequest chatRequest) {
var request = chatRequest.withStream(true);
return __createSyncStream(request);
return createSyncStreamBasic(request);
}

@POST
CompletableFuture<Stream<ChatResponse>> __createAsyncStream(@Body ChatRequest chatRequest);

default CompletableFuture<Stream<ChatResponse>> createAsyncStream(ChatRequest chatRequest) {
var request = chatRequest.withStream(true);
return __createAsyncStream(request);
return createAsyncStreamBasic(request);
}

@POST
Stream<ChatResponse> createSyncStreamBasic(@Body ChatRequest chatRequest);

@POST
CompletableFuture<Stream<ChatResponse>> createAsyncStreamBasic(@Body ChatRequest chatRequest);

}
```

Note that we have named the annotated methods with the suffix "__" just to indicate that we should not call them directly but should call the default ones (those without the suffix).
Note that we have named the annotated methods with the suffix "Basic" just to indicate that we should not call them directly but should call the default ones (those without the suffix).

## ✳ Examples

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.2.0</version>
<version>1.3.0</version>
<packaging>jar</packaging>

<name>cleverclient</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
* Before running this example you must have an OpenAI account and keep your Api Key in an
* environment variable called OPENAI_API_KEY.
*
* @see https://platform.openai.com/docs/api-reference/authentication
* @see <a href="https://platform.openai.com/docs/api-reference/authentication">OpenAI
* Authentication</a>
*/
public class StreamExample {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,12 @@ public class CleverClient {
* @param httpClient Custom Java's HttpClient component. One is created by default if none
* is passed. Optional.
* @param requestInterceptor Function to modify the request once it has been built.
* @param eventsToRead List of events's name that we want to read from server sent events
* (SSE). Optional.
* @param endsOfStream Texts used to mark the final of streams when handling server sent
* events (SSE). Optional.
*/
@Builder
public CleverClient(@NonNull String baseUrl, @Singular Map<String, String> headers, HttpClient httpClient,
UnaryOperator<HttpRequestData> requestInterceptor, @Singular("eventToRead") List<String> eventsToRead,
@Singular("endOfStream") List<String> endsOfStream) {
UnaryOperator<HttpRequestData> requestInterceptor, @Singular("endOfStream") List<String> endsOfStream) {
this.baseUrl = baseUrl;
this.headers = Optional.ofNullable(headers).orElse(Map.of());
this.httpClient = Optional.ofNullable(httpClient).orElse(HttpClient.newHttpClient());
Expand All @@ -61,7 +58,6 @@ public CleverClient(@NonNull String baseUrl, @Singular Map<String, String> heade
.requestInterceptor(this.requestInterceptor)
.build();
Configurator.builder()
.eventsToRead(Optional.ofNullable(eventsToRead).orElse(Arrays.asList()))
.endsOfStream(Optional.ofNullable(endsOfStream).orElse(Arrays.asList()))
.build();
logger.debug("CleverClient has been created.");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.sashirestela.cleverclient.annotation;

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

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

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

Class<?> type();

String[] events();

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

StreamType[] value();

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ public Object sendRequest() {
logger.debug("Request Headers : {}", formattedHeaders);

var bodyPublisher = createBodyPublisher(bodyObject, contentType);
var responseClass = returnType.getBaseClass();
var genericClass = returnType.getGenericClassIfExists();
var headersArray = headers.toArray(new String[0]);
HttpRequest httpRequest = null;
if (headersArray.length > 0) {
Expand All @@ -70,7 +68,7 @@ public Object sendRequest() {
.build();
}
var httpSender = HttpSenderFactory.get().createSender(returnType);
return httpSender.sendRequest(httpClient, httpRequest, responseClass, genericClass);
return httpSender.sendRequest(httpClient, httpRequest, returnType);
}

private void interceptRequest() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void save(Class<?> interfaceClass) {
for (var javaMethod : interfaceClass.getMethods()) {
var methodMetadata = MethodMetadata.builder()
.name(javaMethod.getName())
.returnType(new ReturnType(javaMethod.getGenericReturnType().getTypeName()))
.returnType(new ReturnType(javaMethod))
.isDefault(javaMethod.isDefault())
.annotations(getAnnotations(javaMethod.getDeclaredAnnotations()))
.parameters(getParameters(javaMethod.getParameters()))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.github.sashirestela.cleverclient.sender;

import io.github.sashirestela.cleverclient.support.ReturnType;

import java.io.InputStream;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
Expand All @@ -8,9 +10,7 @@
public class HttpAsyncBinarySender extends HttpSender {

@Override
@SuppressWarnings("unchecked")
public <S, T> Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class<T> responseClass,
Class<S> genericClass) {
public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) {

var httpResponseFuture = httpClient.sendAsync(httpRequest, BodyHandlers.ofInputStream());

Expand All @@ -20,7 +20,7 @@ public <S, T> Object sendRequest(HttpClient httpClient, HttpRequest httpRequest,

logger.debug("Response : {}", response.body());

return (T) response.body();
return response.body();
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package io.github.sashirestela.cleverclient.sender;

import io.github.sashirestela.cleverclient.support.ReturnType;
import io.github.sashirestela.cleverclient.util.JsonUtil;

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;

public class HttpAsyncObjectSender extends HttpSender {
public class HttpAsyncCustomSender extends HttpSender {

@Override
public <S, T> Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class<T> responseClass,
Class<S> genericClass) {
public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) {

var httpResponseFuture = httpClient.sendAsync(httpRequest, BodyHandlers.ofString());

Expand All @@ -20,7 +20,7 @@ public <S, T> Object sendRequest(HttpClient httpClient, HttpRequest httpRequest,

logger.debug("Response : {}", response.body());

return JsonUtil.jsonToObject(response.body(), responseClass);
return JsonUtil.jsonToObject(response.body(), returnType.getBaseClass());
});
}

Expand Down
Loading