Skip to content
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
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,54 @@ try {
} catch (NetworkException e) {
// You may want to find out the root cause
System.out.println(e.getCause().getCause());
}
}
```
### Request Options

Every operation on the client has an overload that accepts a `RequestOptions` object. Use it to set
per-request options such as an idempotency key.

#### Idempotency Keys

[Idempotency keys](https://developers.recurly.com/api/latest/#section/Getting-Started/Idempotent-Requests)
allow you to safely retry mutating requests (POST, PUT, DELETE) without the risk of performing the same
operation twice. Pass a unique value per logical operation — Recurly will deduplicate requests that
share a key.

```java
import com.recurly.v3.RequestOptions;
import com.recurly.v3.requests.AccountCreate;
import com.recurly.v3.resources.Account;

final AccountCreate accountReq = new AccountCreate();
accountReq.setCode("myaccountcode");

final RequestOptions options = RequestOptions.builder()
.idempotencyKey("unique-key-for-this-operation");

final Account account = client.createAccount(accountReq, options);
```

#### Custom Headers

You can also set arbitrary request headers via `RequestOptions`:

```java
final RequestOptions options = RequestOptions.builder()
.header("X-Custom-Header", "value")
.header("X-Another-Header", "other-value");

final Account account = client.createAccount(accountReq, options);
```

Options can be combined — `idempotencyKey` and `header` calls chain together:

```java
final RequestOptions options = RequestOptions.builder()
.idempotencyKey("unique-key")
.header("X-Custom-Header", "value");
```

## Support

Looking for help? Please contact [support@recurly.com](mailto:support@recurly.com) or visit
Expand Down
4 changes: 2 additions & 2 deletions scripts/build
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ if [ ! -d ./bin ]; then
mkdir -p ./bin;
fi

FORMAT=./bin/google-java-format-1.7-all-deps.jar
FORMAT=./bin/google-java-format-1.35.0-all-deps.jar
if test -f $FORMAT; then
echo "Formatter file exists"
else
curl -L https://github.com/google/google-java-format/releases/download/google-java-format-1.7/google-java-format-1.7-all-deps.jar > ./bin/google-java-format-1.7-all-deps.jar
curl -L https://github.com/google/google-java-format/releases/download/v1.35.0/google-java-format-1.35.0-all-deps.jar > $FORMAT
fi
2 changes: 1 addition & 1 deletion scripts/format
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -e

java -jar ./bin/google-java-format-1.7-all-deps.jar -i $(ls src/main/java/com/recurly/v3/**/*.java)
java -jar ./bin/google-java-format-1.35.0-all-deps.jar -i $(ls src/main/java/com/recurly/v3/**/*.java)
56 changes: 49 additions & 7 deletions src/main/java/com/recurly/v3/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ protected static boolean envEnabled(final String envVar) {
}

protected void makeRequest(final String method, final String url) {
final okhttp3.Request request = buildRequest(method, url, null, null);
makeRequest(method, url, (RequestOptions) null);
}

protected void makeRequest(final String method, final String url, final RequestOptions options) {
final okhttp3.Request request = buildRequest(method, url, null, null, options);

try (final Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
Expand All @@ -108,20 +112,38 @@ protected void makeRequest(final String method, final String url) {
}

protected <T> T makeRequest(final String method, final String url, final Type resourceClass) {
return makeRequest(method, url, null, null, resourceClass);
return makeRequest(method, url, null, null, null, resourceClass);
}

protected <T> T makeRequest(final String method, final String url, final RequestOptions options, final Type resourceClass) {
return makeRequest(method, url, null, null, options, resourceClass);
}

protected <T> T makeRequest(
final String method, final String url, final Request body, final Type resourceClass) {
return makeRequest(method, url, body, null, resourceClass);
return makeRequest(method, url, body, null, null, resourceClass);
}

protected <T> T makeRequest(
final String method, final String url, final Request body, final RequestOptions options, final Type resourceClass) {
return makeRequest(method, url, body, null, options, resourceClass);
}

protected <T> T makeRequest(
final String method,
final String url,
final HashMap<String, Object> queryParams,
final Type resourceClass) {
return makeRequest(method, url, null, queryParams, null, resourceClass);
}

protected <T> T makeRequest(
final String method,
final String url,
final HashMap<String, Object> queryParams,
final RequestOptions options,
final Type resourceClass) {
return makeRequest(method, url, null, queryParams, resourceClass);
return makeRequest(method, url, null, queryParams, options, resourceClass);
}

protected <T> T makeRequest(
Expand All @@ -130,7 +152,17 @@ protected <T> T makeRequest(
final Request body,
final HashMap<String, Object> queryParams,
final Type resourceClass) {
final okhttp3.Request request = buildRequest(method, url, body, queryParams);
return makeRequest(method, url, body, queryParams, null, resourceClass);
}

protected <T> T makeRequest(
final String method,
final String url,
final Request body,
final HashMap<String, Object> queryParams,
final RequestOptions options,
final Type resourceClass) {
final okhttp3.Request request = buildRequest(method, url, body, queryParams, options);

try (final Response response = client.newCall(request).execute()) {

Expand Down Expand Up @@ -160,7 +192,7 @@ protected <T> T makeRequest(
}

public int getRecordCount(final String url, final HashMap<String, Object> queryParams) {
final okhttp3.Request request = buildRequest("HEAD", url, null, queryParams);
final okhttp3.Request request = buildRequest("HEAD", url, null, queryParams, null);

try (final Response response = client.newCall(request).execute()) {

Expand Down Expand Up @@ -195,7 +227,8 @@ private okhttp3.Request buildRequest(
final String method,
final String url,
final Request body,
final HashMap<String, Object> queryParams) {
final HashMap<String, Object> queryParams,
final RequestOptions options) {
final HttpUrl.Builder httpBuilder = HttpUrl.parse(this.apiUrl + url).newBuilder();

final RequestBody requestBody =
Expand Down Expand Up @@ -239,6 +272,15 @@ private okhttp3.Request buildRequest(

final Builder requestBuilder = new okhttp3.Request.Builder().url(requestUrl);

if (options != null) {
for (Map.Entry<String, String> entry : options.getHeaders().entrySet()) {
requestBuilder.header(entry.getKey(), entry.getValue());
}
if (options.getIdempotencyKey() != null) {
requestBuilder.header("Idempotency-Key", options.getIdempotencyKey());
}
}

switch (method) {
case "HEAD":
return requestBuilder.head().build();
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/com/recurly/v3/RequestOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.recurly.v3;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class RequestOptions {
private final String idempotencyKey;
private final Map<String, String> headers;

private RequestOptions(final Builder builder) {
this.idempotencyKey = builder.idempotencyKey;
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
}

public static Builder builder() {
return new Builder();
}

public String getIdempotencyKey() {
return idempotencyKey;
}

public Map<String, String> getHeaders() {
return headers;
}

public static class Builder {
private String idempotencyKey;
private Map<String, String> headers = new HashMap<>();

private Builder() {}

public Builder idempotencyKey(final String idempotencyKey) {
this.idempotencyKey = idempotencyKey;
return this;
}

public Builder header(final String name, final String value) {
this.headers.put(name, value);
return this;
}

public RequestOptions build() {
return new RequestOptions(this);
}
}
}
59 changes: 59 additions & 0 deletions src/test/java/com/recurly/v3/BaseClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.recurly.v3.fixtures.MockQueryParams;
import com.recurly.v3.fixtures.MyRequest;
import com.recurly.v3.fixtures.MyResource;
import com.recurly.v3.RequestOptions;
import okhttp3.Call;
import okhttp3.Headers;
import okhttp3.HttpUrl;
Expand Down Expand Up @@ -397,6 +398,64 @@ public void testUsingRegionEUClientOptions() {
assertEquals("https://v3.eu.recurly.com", client.getApiUrl());
}

@Test
public void testIdempotencyKeyHeader() throws IOException {
final Call mCall = mock(Call.class);
final String idempotencyKey = "test-idempotency-key-123";
Answer answer = (i) -> {
Request request = i.getArgument(0);
assertEquals(idempotencyKey, request.header("Idempotency-Key"));
return mCall;
};
when(mCall.execute()).thenReturn(MockClient.buildResponse(200, "OK", getResponseJson()));

OkHttpClient mockOkHttpClient = MockClient.getMockOkHttpClient(answer);

final MockClient client = new MockClient("apiKey", mockOkHttpClient);
final MyRequest body = new MyRequest();
final RequestOptions options = RequestOptions.builder().idempotencyKey(idempotencyKey).build();
client.createResource(body, options);
}

@Test
public void testRawHeaders() throws IOException {
final Call mCall = mock(Call.class);
Answer answer = (i) -> {
Request request = i.getArgument(0);
assertEquals("bar", request.header("X-Custom-Foo"));
assertEquals("baz", request.header("X-Custom-Qux"));
return mCall;
};
when(mCall.execute()).thenReturn(MockClient.buildResponse(200, "OK", getResponseJson()));

OkHttpClient mockOkHttpClient = MockClient.getMockOkHttpClient(answer);

final MockClient client = new MockClient("apiKey", mockOkHttpClient);
final MyRequest body = new MyRequest();
final RequestOptions options = RequestOptions.builder()
.header("X-Custom-Foo", "bar")
.header("X-Custom-Qux", "baz")
.build();
client.createResource(body, options);
}

@Test
public void testNoIdempotencyKeyHeader() throws IOException {
final Call mCall = mock(Call.class);
Answer answer = (i) -> {
Request request = i.getArgument(0);
assertEquals(null, request.header("Idempotency-Key"));
return mCall;
};
when(mCall.execute()).thenReturn(MockClient.buildResponse(200, "OK", getResponseJson()));

OkHttpClient mockOkHttpClient = MockClient.getMockOkHttpClient(answer);

final MockClient client = new MockClient("apiKey", mockOkHttpClient);
final MyRequest body = new MyRequest();
client.createResource(body);
}

@Test
public void testInterpolatePathWithoutParams() {
final MockClient client = new MockClient("apiKey");
Expand Down
10 changes: 9 additions & 1 deletion src/test/java/com/recurly/v3/fixtures/MockClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.recurly.v3.BaseClient;
import com.recurly.v3.Pager;
import com.recurly.v3.ClientOptions;
import com.recurly.v3.RequestOptions;
import com.recurly.v3.fixtures.MockQueryParams;

import org.mockito.stubbing.Answer;
Expand Down Expand Up @@ -64,12 +65,19 @@ public Pager<MyResource> listResources(MockQueryParams queryParams) {
public MyResource createResource(MyRequest body) {
final String url = "/resources";
final HashMap<String, String> urlParams = new HashMap<String, String>();
final HashMap<String, Object> queryParams = new HashMap<String, Object>();
final String path = this.interpolatePath(url, urlParams);
Type returnType = MyResource.class;
return this.makeRequest("POST", path, body, returnType);
}

public MyResource createResource(MyRequest body, RequestOptions options) {
final String url = "/resources";
final HashMap<String, String> urlParams = new HashMap<String, String>();
final String path = this.interpolatePath(url, urlParams);
Type returnType = MyResource.class;
return this.makeRequest("POST", path, body, options, returnType);
}

public MyResource updateResource(String resourceId, MyRequest body) {
final String url = "/resources/{resource_id}";
final HashMap<String, String> urlParams = new HashMap<String, String>();
Expand Down