Skip to content

Commit ba23d4e

Browse files
Merge pull request #335 from recurly/request-options
feat: add support for customer headers / idemptotency key
2 parents f25b732 + 44b6552 commit ba23d4e

7 files changed

Lines changed: 215 additions & 12 deletions

File tree

README.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,54 @@ try {
232232
} catch (NetworkException e) {
233233
// You may want to find out the root cause
234234
System.out.println(e.getCause().getCause());
235-
}
235+
}
236236
```
237+
### Request Options
238+
239+
Every operation on the client has an overload that accepts a `RequestOptions` object. Use it to set
240+
per-request options such as an idempotency key.
241+
242+
#### Idempotency Keys
243+
244+
[Idempotency keys](https://developers.recurly.com/api/latest/#section/Getting-Started/Idempotent-Requests)
245+
allow you to safely retry mutating requests (POST, PUT, DELETE) without the risk of performing the same
246+
operation twice. Pass a unique value per logical operation — Recurly will deduplicate requests that
247+
share a key.
248+
249+
```java
250+
import com.recurly.v3.RequestOptions;
251+
import com.recurly.v3.requests.AccountCreate;
252+
import com.recurly.v3.resources.Account;
253+
254+
final AccountCreate accountReq = new AccountCreate();
255+
accountReq.setCode("myaccountcode");
256+
257+
final RequestOptions options = RequestOptions.builder()
258+
.idempotencyKey("unique-key-for-this-operation");
259+
260+
final Account account = client.createAccount(accountReq, options);
261+
```
262+
263+
#### Custom Headers
264+
265+
You can also set arbitrary request headers via `RequestOptions`:
266+
267+
```java
268+
final RequestOptions options = RequestOptions.builder()
269+
.header("X-Custom-Header", "value")
270+
.header("X-Another-Header", "other-value");
271+
272+
final Account account = client.createAccount(accountReq, options);
273+
```
274+
275+
Options can be combined — `idempotencyKey` and `header` calls chain together:
276+
277+
```java
278+
final RequestOptions options = RequestOptions.builder()
279+
.idempotencyKey("unique-key")
280+
.header("X-Custom-Header", "value");
281+
```
282+
237283
## Support
238284

239285
Looking for help? Please contact [support@recurly.com](mailto:support@recurly.com) or visit

scripts/build

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ if [ ! -d ./bin ]; then
77
mkdir -p ./bin;
88
fi
99

10-
FORMAT=./bin/google-java-format-1.7-all-deps.jar
10+
FORMAT=./bin/google-java-format-1.35.0-all-deps.jar
1111
if test -f $FORMAT; then
1212
echo "Formatter file exists"
1313
else
14-
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
14+
curl -L https://github.com/google/google-java-format/releases/download/v1.35.0/google-java-format-1.35.0-all-deps.jar > $FORMAT
1515
fi

scripts/format

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#!/usr/bin/env bash
22
set -e
33

4-
java -jar ./bin/google-java-format-1.7-all-deps.jar -i $(ls src/main/java/com/recurly/v3/**/*.java)
4+
java -jar ./bin/google-java-format-1.35.0-all-deps.jar -i $(ls src/main/java/com/recurly/v3/**/*.java)

src/main/java/com/recurly/v3/BaseClient.java

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ protected static boolean envEnabled(final String envVar) {
8181
}
8282

8383
protected void makeRequest(final String method, final String url) {
84-
final okhttp3.Request request = buildRequest(method, url, null, null);
84+
makeRequest(method, url, (RequestOptions) null);
85+
}
86+
87+
protected void makeRequest(final String method, final String url, final RequestOptions options) {
88+
final okhttp3.Request request = buildRequest(method, url, null, null, options);
8589

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

110114
protected <T> T makeRequest(final String method, final String url, final Type resourceClass) {
111-
return makeRequest(method, url, null, null, resourceClass);
115+
return makeRequest(method, url, null, null, null, resourceClass);
116+
}
117+
118+
protected <T> T makeRequest(final String method, final String url, final RequestOptions options, final Type resourceClass) {
119+
return makeRequest(method, url, null, null, options, resourceClass);
112120
}
113121

114122
protected <T> T makeRequest(
115123
final String method, final String url, final Request body, final Type resourceClass) {
116-
return makeRequest(method, url, body, null, resourceClass);
124+
return makeRequest(method, url, body, null, null, resourceClass);
125+
}
126+
127+
protected <T> T makeRequest(
128+
final String method, final String url, final Request body, final RequestOptions options, final Type resourceClass) {
129+
return makeRequest(method, url, body, null, options, resourceClass);
130+
}
131+
132+
protected <T> T makeRequest(
133+
final String method,
134+
final String url,
135+
final HashMap<String, Object> queryParams,
136+
final Type resourceClass) {
137+
return makeRequest(method, url, null, queryParams, null, resourceClass);
117138
}
118139

119140
protected <T> T makeRequest(
120141
final String method,
121142
final String url,
122143
final HashMap<String, Object> queryParams,
144+
final RequestOptions options,
123145
final Type resourceClass) {
124-
return makeRequest(method, url, null, queryParams, resourceClass);
146+
return makeRequest(method, url, null, queryParams, options, resourceClass);
125147
}
126148

127149
protected <T> T makeRequest(
@@ -130,7 +152,17 @@ protected <T> T makeRequest(
130152
final Request body,
131153
final HashMap<String, Object> queryParams,
132154
final Type resourceClass) {
133-
final okhttp3.Request request = buildRequest(method, url, body, queryParams);
155+
return makeRequest(method, url, body, queryParams, null, resourceClass);
156+
}
157+
158+
protected <T> T makeRequest(
159+
final String method,
160+
final String url,
161+
final Request body,
162+
final HashMap<String, Object> queryParams,
163+
final RequestOptions options,
164+
final Type resourceClass) {
165+
final okhttp3.Request request = buildRequest(method, url, body, queryParams, options);
134166

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

@@ -160,7 +192,7 @@ protected <T> T makeRequest(
160192
}
161193

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

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

@@ -195,7 +227,8 @@ private okhttp3.Request buildRequest(
195227
final String method,
196228
final String url,
197229
final Request body,
198-
final HashMap<String, Object> queryParams) {
230+
final HashMap<String, Object> queryParams,
231+
final RequestOptions options) {
199232
final HttpUrl.Builder httpBuilder = HttpUrl.parse(this.apiUrl + url).newBuilder();
200233

201234
final RequestBody requestBody =
@@ -239,6 +272,15 @@ private okhttp3.Request buildRequest(
239272

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

275+
if (options != null) {
276+
for (Map.Entry<String, String> entry : options.getHeaders().entrySet()) {
277+
requestBuilder.header(entry.getKey(), entry.getValue());
278+
}
279+
if (options.getIdempotencyKey() != null) {
280+
requestBuilder.header("Idempotency-Key", options.getIdempotencyKey());
281+
}
282+
}
283+
242284
switch (method) {
243285
case "HEAD":
244286
return requestBuilder.head().build();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.recurly.v3;
2+
3+
import java.util.Collections;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
7+
public class RequestOptions {
8+
private final String idempotencyKey;
9+
private final Map<String, String> headers;
10+
11+
private RequestOptions(final Builder builder) {
12+
this.idempotencyKey = builder.idempotencyKey;
13+
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
14+
}
15+
16+
public static Builder builder() {
17+
return new Builder();
18+
}
19+
20+
public String getIdempotencyKey() {
21+
return idempotencyKey;
22+
}
23+
24+
public Map<String, String> getHeaders() {
25+
return headers;
26+
}
27+
28+
public static class Builder {
29+
private String idempotencyKey;
30+
private Map<String, String> headers = new HashMap<>();
31+
32+
private Builder() {}
33+
34+
public Builder idempotencyKey(final String idempotencyKey) {
35+
this.idempotencyKey = idempotencyKey;
36+
return this;
37+
}
38+
39+
public Builder header(final String name, final String value) {
40+
this.headers.put(name, value);
41+
return this;
42+
}
43+
44+
public RequestOptions build() {
45+
return new RequestOptions(this);
46+
}
47+
}
48+
}

src/test/java/com/recurly/v3/BaseClientTest.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.recurly.v3.fixtures.MockQueryParams;
1313
import com.recurly.v3.fixtures.MyRequest;
1414
import com.recurly.v3.fixtures.MyResource;
15+
import com.recurly.v3.RequestOptions;
1516
import okhttp3.Call;
1617
import okhttp3.Headers;
1718
import okhttp3.HttpUrl;
@@ -397,6 +398,64 @@ public void testUsingRegionEUClientOptions() {
397398
assertEquals("https://v3.eu.recurly.com", client.getApiUrl());
398399
}
399400

401+
@Test
402+
public void testIdempotencyKeyHeader() throws IOException {
403+
final Call mCall = mock(Call.class);
404+
final String idempotencyKey = "test-idempotency-key-123";
405+
Answer answer = (i) -> {
406+
Request request = i.getArgument(0);
407+
assertEquals(idempotencyKey, request.header("Idempotency-Key"));
408+
return mCall;
409+
};
410+
when(mCall.execute()).thenReturn(MockClient.buildResponse(200, "OK", getResponseJson()));
411+
412+
OkHttpClient mockOkHttpClient = MockClient.getMockOkHttpClient(answer);
413+
414+
final MockClient client = new MockClient("apiKey", mockOkHttpClient);
415+
final MyRequest body = new MyRequest();
416+
final RequestOptions options = RequestOptions.builder().idempotencyKey(idempotencyKey).build();
417+
client.createResource(body, options);
418+
}
419+
420+
@Test
421+
public void testRawHeaders() throws IOException {
422+
final Call mCall = mock(Call.class);
423+
Answer answer = (i) -> {
424+
Request request = i.getArgument(0);
425+
assertEquals("bar", request.header("X-Custom-Foo"));
426+
assertEquals("baz", request.header("X-Custom-Qux"));
427+
return mCall;
428+
};
429+
when(mCall.execute()).thenReturn(MockClient.buildResponse(200, "OK", getResponseJson()));
430+
431+
OkHttpClient mockOkHttpClient = MockClient.getMockOkHttpClient(answer);
432+
433+
final MockClient client = new MockClient("apiKey", mockOkHttpClient);
434+
final MyRequest body = new MyRequest();
435+
final RequestOptions options = RequestOptions.builder()
436+
.header("X-Custom-Foo", "bar")
437+
.header("X-Custom-Qux", "baz")
438+
.build();
439+
client.createResource(body, options);
440+
}
441+
442+
@Test
443+
public void testNoIdempotencyKeyHeader() throws IOException {
444+
final Call mCall = mock(Call.class);
445+
Answer answer = (i) -> {
446+
Request request = i.getArgument(0);
447+
assertEquals(null, request.header("Idempotency-Key"));
448+
return mCall;
449+
};
450+
when(mCall.execute()).thenReturn(MockClient.buildResponse(200, "OK", getResponseJson()));
451+
452+
OkHttpClient mockOkHttpClient = MockClient.getMockOkHttpClient(answer);
453+
454+
final MockClient client = new MockClient("apiKey", mockOkHttpClient);
455+
final MyRequest body = new MyRequest();
456+
client.createResource(body);
457+
}
458+
400459
@Test
401460
public void testInterpolatePathWithoutParams() {
402461
final MockClient client = new MockClient("apiKey");

src/test/java/com/recurly/v3/fixtures/MockClient.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.recurly.v3.BaseClient;
55
import com.recurly.v3.Pager;
66
import com.recurly.v3.ClientOptions;
7+
import com.recurly.v3.RequestOptions;
78
import com.recurly.v3.fixtures.MockQueryParams;
89

910
import org.mockito.stubbing.Answer;
@@ -64,12 +65,19 @@ public Pager<MyResource> listResources(MockQueryParams queryParams) {
6465
public MyResource createResource(MyRequest body) {
6566
final String url = "/resources";
6667
final HashMap<String, String> urlParams = new HashMap<String, String>();
67-
final HashMap<String, Object> queryParams = new HashMap<String, Object>();
6868
final String path = this.interpolatePath(url, urlParams);
6969
Type returnType = MyResource.class;
7070
return this.makeRequest("POST", path, body, returnType);
7171
}
7272

73+
public MyResource createResource(MyRequest body, RequestOptions options) {
74+
final String url = "/resources";
75+
final HashMap<String, String> urlParams = new HashMap<String, String>();
76+
final String path = this.interpolatePath(url, urlParams);
77+
Type returnType = MyResource.class;
78+
return this.makeRequest("POST", path, body, options, returnType);
79+
}
80+
7381
public MyResource updateResource(String resourceId, MyRequest body) {
7482
final String url = "/resources/{resource_id}";
7583
final HashMap<String, String> urlParams = new HashMap<String, String>();

0 commit comments

Comments
 (0)