Skip to content

Commit c7859dc

Browse files
feat: Batch validation (#57)
1 parent c2773e3 commit c7859dc

File tree

8 files changed

+221
-22
lines changed

8 files changed

+221
-22
lines changed

src/main/java/com/resend/core/net/RequestOptions.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package com.resend.core.net;
22

3+
import java.util.Collections;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
37
/**
48
* Represents a request to create a request options.
59
*/
610
public class RequestOptions {
711
private final String idempotencyKey;
12+
private final Map<String, String> additionalHeaders;
813

914
/**
1015
* Constructs a RequestOptions object using the provided builder.
@@ -13,6 +18,7 @@ public class RequestOptions {
1318
*/
1419
public RequestOptions(Builder builder) {
1520
this.idempotencyKey = builder.idempotencyKey;
21+
this.additionalHeaders = Collections.unmodifiableMap(new HashMap<>(builder.additionalHeaders));
1622
}
1723

1824
/**
@@ -24,6 +30,15 @@ public String getIdempotencyKey() {
2430
return idempotencyKey;
2531
}
2632

33+
/**
34+
* Get the additional headers map.
35+
*
36+
* @return An unmodifiable map of additional headers.
37+
*/
38+
public Map<String, String> getAdditionalHeaders() {
39+
return additionalHeaders;
40+
}
41+
2742
/**
2843
* Create a new builder instance for constructing RequestOptions objects.
2944
*
@@ -38,6 +53,14 @@ public static Builder builder() {
3853
*/
3954
public static class Builder {
4055
private String idempotencyKey;
56+
private final Map<String, String> additionalHeaders;
57+
58+
/**
59+
* Constructs a new Builder with empty additional headers map.
60+
*/
61+
public Builder() {
62+
this.additionalHeaders = new HashMap<>();
63+
}
4164

4265
/**
4366
* Set the idempotencyKey.
@@ -50,6 +73,28 @@ public Builder setIdempotencyKey(String idempotencyKey) {
5073
return this;
5174
}
5275

76+
/**
77+
* Add a custom header to the additional headers map.
78+
*
79+
* @param name The header name.
80+
* @param value The header value.
81+
* @return The builder instance.
82+
*/
83+
public Builder add(String name, String value) {
84+
this.additionalHeaders.put(name, value);
85+
return this;
86+
}
87+
88+
/**
89+
* Add multiple custom headers to the additional headers map.
90+
*
91+
* @param headers A map of headers to add.
92+
* @return The builder instance.
93+
*/
94+
public Builder addAll(Map<String, String> headers) {
95+
this.additionalHeaders.putAll(headers);
96+
return this;
97+
}
5398

5499
/**
55100
* Build a new RequestOptions object.
@@ -60,4 +105,4 @@ public RequestOptions build() {
60105
return new RequestOptions(this);
61106
}
62107
}
63-
}
108+
}

src/main/java/com/resend/core/net/impl/HttpClient.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ public AbstractHttpResponse perform(
154154
requestBuilder.addHeader("Idempotency-Key", requestOptions.getIdempotencyKey());
155155
}
156156

157+
if (requestOptions.getAdditionalHeaders() != null
158+
&& !requestOptions.getAdditionalHeaders().isEmpty()) {
159+
for (Map.Entry<String, String> entry : requestOptions.getAdditionalHeaders().entrySet()) {
160+
requestBuilder.addHeader(entry.getKey(), entry.getValue());
161+
}
162+
}
163+
157164
Request request = requestBuilder.build();
158165

159166
try {

src/main/java/com/resend/services/batch/Batch.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,7 @@ public CreateBatchEmailsResponse send(List<CreateEmailOptions> emails) throws Re
4343

4444
String responseBody = response.getBody();
4545

46-
CreateBatchEmailsResponse createBatchEmailsResponse = resendMapper.readValue(responseBody, CreateBatchEmailsResponse.class);
47-
48-
return createBatchEmailsResponse;
46+
return resendMapper.readValue(responseBody, CreateBatchEmailsResponse.class);
4947
}
5048

5149
/**
@@ -67,9 +65,7 @@ public CreateBatchEmailsResponse send(List<CreateEmailOptions> emails, RequestOp
6765

6866
String responseBody = response.getBody();
6967

70-
CreateBatchEmailsResponse createBatchEmailsResponse = resendMapper.readValue(responseBody, CreateBatchEmailsResponse.class);
71-
72-
return createBatchEmailsResponse;
68+
return resendMapper.readValue(responseBody, CreateBatchEmailsResponse.class);
7369
}
7470

7571
/**
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.resend.services.batch.model;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.annotation.JsonSubTypes;
5+
6+
import java.util.List;
7+
8+
/**
9+
* Base class for batch email responses.
10+
* The actual implementation depends on the validation mode used.
11+
*/
12+
@JsonSubTypes({
13+
@JsonSubTypes.Type(CreateBatchEmailsResponse.class)
14+
})
15+
public abstract class AbstractBatchEmailsResponse {
16+
17+
/**
18+
* The list of e-mail ids created.
19+
*/
20+
@JsonProperty("data")
21+
protected List<BatchEmail> data;
22+
23+
/**
24+
* Default constructor.
25+
*/
26+
public AbstractBatchEmailsResponse() {
27+
}
28+
29+
/**
30+
* Constructor with data.
31+
*
32+
* @param data A list of successfully created batch emails.
33+
*/
34+
public AbstractBatchEmailsResponse(final List<BatchEmail> data) {
35+
this.data = data;
36+
}
37+
38+
/**
39+
* Get the list of successfully created batch emails.
40+
*
41+
* @return A list of batch emails.
42+
*/
43+
public List<BatchEmail> getData() {
44+
return data;
45+
}
46+
47+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.resend.services.batch.model;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
5+
/**
6+
* Represents a validation error for a batch email in permissive mode.
7+
*/
8+
public class BatchError {
9+
10+
@JsonProperty("index")
11+
private Integer index;
12+
13+
@JsonProperty("message")
14+
private String message;
15+
16+
/**
17+
* Default constructor.
18+
*/
19+
public BatchError() {
20+
}
21+
22+
/**
23+
* Constructor with index and message.
24+
*
25+
* @param index The index of the email in the batch request that failed validation.
26+
* @param message The error message identifying the validation error.
27+
*/
28+
public BatchError(final Integer index, final String message) {
29+
this.index = index;
30+
this.message = message;
31+
}
32+
33+
/**
34+
* Get the index of the email in the batch request that failed validation.
35+
*
36+
* @return The index of the failed email.
37+
*/
38+
public Integer getIndex() {
39+
return index;
40+
}
41+
42+
/**
43+
* Get the error message identifying the validation error.
44+
*
45+
* @return The error message.
46+
*/
47+
public String getMessage() {
48+
return message;
49+
}
50+
}

src/main/java/com/resend/services/batch/model/CreateBatchEmailsResponse.java

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,57 @@
55
import java.util.List;
66

77
/**
8-
* Represents the response for creating batch emails.
8+
* Response for batch emails sent in permissive mode.
9+
* In permissive mode, partial success is allowed and validation errors are returned.
910
*/
10-
public class CreateBatchEmailsResponse {
11+
public class CreateBatchEmailsResponse extends AbstractBatchEmailsResponse {
1112

12-
@JsonProperty("data")
13-
private List<BatchEmail> data;
13+
@JsonProperty("errors")
14+
private List<BatchError> errors;
1415

1516
/**
1617
* Default constructor.
1718
*/
1819
public CreateBatchEmailsResponse() {
20+
super();
1921
}
2022

2123
/**
22-
* Constructor with a list of batch emails.
24+
* Constructor with data and errors.
2325
*
24-
* @param data A list of batch emails.
26+
* @param data A list of successfully created batch emails.
27+
* @param errors A list of validation errors.
2528
*/
26-
public CreateBatchEmailsResponse(final List<BatchEmail> data) {
27-
this.data = data;
29+
public CreateBatchEmailsResponse(final List<BatchEmail> data, final List<BatchError> errors) {
30+
super(data);
31+
this.errors = errors;
2832
}
2933

3034
/**
31-
* Get the list of batch emails.
35+
* Get the list of validation errors.
3236
*
33-
* @return A list of batch emails.
37+
* @return A list of batch email errors, never null (but may be empty).
3438
*/
35-
public List<BatchEmail> getData() {
36-
return data;
39+
public List<BatchError> getErrors() {
40+
return errors;
3741
}
38-
}
3942

43+
/**
44+
* Check if the response has any validation errors.
45+
*
46+
* @return true if there are validation errors, false otherwise.
47+
*/
48+
public boolean hasErrors() {
49+
return errors != null && !errors.isEmpty();
50+
}
51+
52+
/**
53+
* Get the total number of failed emails.
54+
*
55+
* @return The count of emails that failed validation.
56+
*/
57+
public int getErrorCount() {
58+
return errors != null ? errors.size() : 0;
59+
}
60+
61+
}

src/test/java/com/resend/services/emails/EmailsTest.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.resend.core.net.ListParams;
44
import com.resend.core.net.RequestOptions;
55
import com.resend.services.batch.Batch;
6+
import com.resend.services.batch.model.AbstractBatchEmailsResponse;
67
import com.resend.services.batch.model.CreateBatchEmailsResponse;
78
import com.resend.services.emails.model.*;
89
import com.resend.services.util.EmailsUtil;
@@ -86,6 +87,22 @@ public void testCreateBatchEmails_Success() throws ResendException {
8687
assertEquals(expectedRes.getData().size(), sendBatchEmailsResponse.getData().size());
8788
}
8889

90+
@Test
91+
public void testCreatePermissiveBatchEmails_Success() throws ResendException {
92+
RequestOptions options = RequestOptions.builder()
93+
.add("x-batch-validation", "permissive").build();
94+
95+
List<CreateEmailOptions> batchEmailsRequest = EmailsUtil.createBatchEmailOptions();
96+
CreateBatchEmailsResponse expectedRes = EmailsUtil.createPermissiveBatchEmailsResponse();
97+
98+
when(batch.send(batchEmailsRequest, options)).thenReturn(expectedRes);
99+
100+
CreateBatchEmailsResponse sendBatchEmailsResponse = batch.send(batchEmailsRequest, options);
101+
102+
assertNotNull(sendBatchEmailsResponse);
103+
assertEquals(expectedRes.getData().size(), sendBatchEmailsResponse.getData().size());
104+
}
105+
89106
@Test
90107
public void testCreateBatchEmailsWithIdempotencyKey_Success() throws ResendException {
91108
List<CreateEmailOptions> batchEmailsRequest = EmailsUtil.createBatchEmailOptions();
@@ -94,7 +111,7 @@ public void testCreateBatchEmailsWithIdempotencyKey_Success() throws ResendExcep
94111

95112
when(batch.send(batchEmailsRequest, requestOptions)).thenReturn(expectedRes);
96113

97-
CreateBatchEmailsResponse sendBatchEmailsResponse = batch.send(batchEmailsRequest, requestOptions);
114+
AbstractBatchEmailsResponse sendBatchEmailsResponse = batch.send(batchEmailsRequest, requestOptions);
98115

99116
assertNotNull(sendBatchEmailsResponse);
100117
assertEquals(expectedRes.getData().size(), sendBatchEmailsResponse.getData().size());

src/test/java/com/resend/services/util/EmailsUtil.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.resend.core.net.RequestOptions;
44
import com.resend.services.batch.model.BatchEmail;
5+
import com.resend.services.batch.model.BatchError;
56
import com.resend.services.batch.model.CreateBatchEmailsResponse;
67
import com.resend.services.emails.model.*;
78
import com.resend.core.net.AbstractHttpResponse;
@@ -65,7 +66,21 @@ public static List<CreateEmailOptions> createBatchEmailOptions() {
6566
}
6667

6768
public static CreateBatchEmailsResponse createBatchEmailsResponse() {
68-
return new CreateBatchEmailsResponse(Arrays.asList(new BatchEmail("123"), new BatchEmail("321")));
69+
return new CreateBatchEmailsResponse(Arrays.asList(new BatchEmail("123"), new BatchEmail("321")), new ArrayList<>());
70+
}
71+
72+
public static CreateBatchEmailsResponse createPermissiveBatchEmailsResponse() {
73+
List<BatchEmail> successes = Arrays.asList(
74+
new BatchEmail("123"),
75+
new BatchEmail("321")
76+
);
77+
78+
List<BatchError> errors = Arrays.asList(
79+
new BatchError(456, "Invalid recipient address"),
80+
new BatchError(789, "Domain not reachable")
81+
);
82+
83+
return new CreateBatchEmailsResponse(successes, errors);
6984
}
7085

7186
public static Email createTestEmail() {

0 commit comments

Comments
 (0)