Skip to content

Commit

Permalink
refactor: (first draft) Introduce error/exception types and abstract …
Browse files Browse the repository at this point in the history
…HttpRequest attempts
  • Loading branch information
booniepepper committed Oct 3, 2023
1 parent 12af785 commit b6853fb
Show file tree
Hide file tree
Showing 10 changed files with 388 additions and 230 deletions.
433 changes: 232 additions & 201 deletions src/main/java/dev/openfga/sdk/api/OpenFgaApi.java

Large diffs are not rendered by default.

31 changes: 5 additions & 26 deletions src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,18 @@

package dev.openfga.sdk.api.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import dev.openfga.sdk.api.client.ApiClient;
import dev.openfga.sdk.api.configuration.*;
import dev.openfga.sdk.errors.ApiException;
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;

public class OAuth2Client {
private final HttpClient httpClient;
private final ApiClient apiClient;
private final Credentials credentials;
private final ObjectMapper mapper;
private final AccessToken token = new AccessToken();
private final CredentialsFlowRequest authRequest;
private final String apiTokenIssuer;
Expand All @@ -37,14 +32,11 @@ public class OAuth2Client {
* Initializes a new instance of the {@link OAuth2Client} class
*
* @param configuration Configuration, including credentials, that can be used to retrieve an access tokens
* @param httpClient Http client
*/
public OAuth2Client(Configuration configuration, HttpClient httpClient, ObjectMapper mapper)
throws FgaInvalidParameterException {
public OAuth2Client(Configuration configuration, ApiClient apiClient) throws FgaInvalidParameterException {
this.credentials = configuration.getCredentials();

this.httpClient = httpClient;
this.mapper = mapper;
this.apiClient = apiClient;
this.apiTokenIssuer = credentials.getClientCredentials().getApiTokenIssuer();
this.authRequest = new CredentialsFlowRequest();
this.authRequest.setClientId(credentials.getClientCredentials().getClientId());
Expand Down Expand Up @@ -78,27 +70,14 @@ public CompletableFuture<String> getAccessToken() throws FgaInvalidParameterExce
private CompletableFuture<CredentialsFlowResponse> exchangeToken()
throws ApiException, FgaInvalidParameterException {
try {
byte[] body = mapper.writeValueAsBytes(authRequest);
byte[] body = apiClient.getObjectMapper().writeValueAsBytes(authRequest);

Configuration config = new Configuration().apiUrl("https://" + apiTokenIssuer);

HttpRequest request = ApiClient.requestBuilder("POST", "/oauth/token", body, config)
.build();

return httpClient
.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenCompose(httpResponse -> {
if (httpResponse.statusCode() != HttpURLConnection.HTTP_OK) {
return CompletableFuture.failedFuture(new ApiException("exchangeToken", httpResponse));
}
try {
CredentialsFlowResponse response =
mapper.readValue(httpResponse.body(), CredentialsFlowResponse.class);
return CompletableFuture.completedFuture(response);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
});
return ApiClient.attemptHttpRequest(apiClient, "exchangeToken", request, CredentialsFlowResponse.class);
} catch (IOException e) {
throw new ApiException(e);
}
Expand Down
73 changes: 72 additions & 1 deletion src/main/java/dev/openfga/sdk/api/client/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import dev.openfga.sdk.api.configuration.Configuration;
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import dev.openfga.sdk.errors.*;
import dev.openfga.sdk.util.Pair;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
Expand All @@ -35,7 +37,9 @@
import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.openapitools.jackson.nullable.JsonNullableModule;

Expand Down Expand Up @@ -239,6 +243,73 @@ protected HttpClient.Builder createDefaultHttpClientBuilder() {
return HttpClient.newBuilder();
}

// Static for testability reasons
public static <T> CompletableFuture<T> attemptHttpRequest(
ApiClient apiClient, String name, HttpRequest request, Class<T> clazz) throws ApiException {
Supplier<CompletableFuture<HttpResponse<String>>> callOnce =
() -> apiClient.getHttpClient().sendAsync(request, HttpResponse.BodyHandlers.ofString());

return callOnce.get().thenCompose(response -> {
int status = response.statusCode();
String responseBody = response.body();

try {
checkStatus(name, response);
} catch (FgaApiRateLimitExceededError | FgaApiInternalError e) {
// TODO handle retry
return CompletableFuture.failedFuture(e);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}

if (status != HttpURLConnection.HTTP_OK && status != HttpURLConnection.HTTP_CREATED) {
return CompletableFuture.failedFuture(new ApiException(name, response));
}

try {
T body = apiClient.getObjectMapper().readValue(responseBody, clazz);
return CompletableFuture.completedFuture(body);
} catch (IOException e) {
return CompletableFuture.failedFuture(new ApiException(e));
}
});
}

private static void checkStatus(String name, HttpResponse<String> response)
throws FgaApiValidationError, FgaApiAuthenticationError, FgaApiNotFoundError, FgaApiRateLimitExceededError,
FgaApiInternalError {

int status = response.statusCode();
String body = response.body();

switch (status) {
case HttpURLConnection.HTTP_BAD_REQUEST:
case 422: // HTTP 422 Unprocessable Entity
throw new FgaApiValidationError(name, status, response.headers(), body);

case HttpURLConnection.HTTP_UNAUTHORIZED:
case HttpURLConnection.HTTP_FORBIDDEN:
throw new FgaApiAuthenticationError(name, status, response.headers(), body);

case HttpURLConnection.HTTP_NOT_FOUND:
throw new FgaApiNotFoundError(name, status, response.headers(), body);

case 429: // HTTP 429 Too Many Requests
throw new FgaApiRateLimitExceededError(name, status, response.headers(), body);

case HttpURLConnection.HTTP_INTERNAL_ERROR:
throw new FgaApiInternalError(name, status, response.headers(), body);
}
}

// public static <T> CompletableFuture<T> retryHttpRequest(
// String name,
// HttpClient httpClient,
// HttpRequest.Builder requestBuilder,
// Class<T> clazz,
// ObjectMapper mapper) {
// }

/**
* Set a custom {@link HttpClient.Builder} object to use when creating the
* {@link HttpClient} that is used by the API client.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiAuthenticationError extends ApiException {
public FgaApiAuthenticationError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiAuthenticationError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
14 changes: 14 additions & 0 deletions src/main/java/dev/openfga/sdk/errors/FgaApiInternalError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiInternalError extends ApiException {
public FgaApiInternalError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiInternalError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
14 changes: 14 additions & 0 deletions src/main/java/dev/openfga/sdk/errors/FgaApiNotFoundError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiNotFoundError extends ApiException {
public FgaApiNotFoundError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiNotFoundError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiRateLimitExceededError extends ApiException {
public FgaApiRateLimitExceededError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiRateLimitExceededError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
14 changes: 14 additions & 0 deletions src/main/java/dev/openfga/sdk/errors/FgaApiValidationError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiValidationError extends ApiException {
public FgaApiValidationError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiValidationError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
2 changes: 1 addition & 1 deletion src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ public void createStore_400() throws Exception {

// Then
mockHttpClient.verify().post("https://localhost/stores").called(1);
ApiException exception = assertInstanceOf(ApiException.class, execException.getCause());
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, execException.getCause());
assertEquals(400, exception.getCode());
assertEquals(
"{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}",
Expand Down
9 changes: 8 additions & 1 deletion src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.pgssoft.httpclient.HttpClientMock;
import dev.openfga.sdk.api.client.ApiClient;
import dev.openfga.sdk.api.configuration.*;
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -36,7 +39,11 @@ public void setup() throws FgaInvalidParameterException {

var configuration = new Configuration().apiUrl("").credentials(credentials);

oAuth2 = new OAuth2Client(configuration, mockHttpClient, mapper);
var apiClient = mock(ApiClient.class);
when(apiClient.getHttpClient()).thenReturn(mockHttpClient);
when(apiClient.getObjectMapper()).thenReturn(mapper);

oAuth2 = new OAuth2Client(configuration, apiClient);
}

@Test
Expand Down

0 comments on commit b6853fb

Please sign in to comment.