Skip to content

Commit 2ffdda8

Browse files
authored
feat: add typed message tags (#48)
1 parent bccc77b commit 2ffdda8

4 files changed

Lines changed: 102 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,21 @@ SendEmailResponse response = email
101101

102102
// Metadata and tags
103103
.metadata(metadata)
104-
.tag("welcome", "onboarding")
104+
.tag("legacy-tag")
105+
.tags(
106+
new MessageTag("campaign", "welcome"),
107+
new MessageTag("customer", "new")
108+
)
105109

106110
// Idempotency
107111
.idempotencyKey("unique-request-key")
108112

109113
.send();
110114
```
111115

116+
`tag()` remains available for the legacy single tag. The previous map form of
117+
`tags()` also remains available.
118+
112119
Existing constructor-based sending usage still works:
113120

114121
```java

‎src/main/java/co/lettermint/endpoints/EmailEndpoint.java‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import co.lettermint.client.LettermintClient;
44
import co.lettermint.models.Attachment;
55
import co.lettermint.models.SendEmailResponse;
6+
import co.lettermint.models.MessageTag;
67
import co.lettermint.models.api.SendMailRequest;
78
import co.lettermint.models.api.SendMailResponse;
89
import com.fasterxml.jackson.core.type.TypeReference;
@@ -28,7 +29,7 @@ public class EmailEndpoint extends Endpoint {
2829
private String route;
2930
private Map<String, Object> metadata;
3031
private String tag;
31-
private List<Map<String, String>> tags;
32+
private List<MessageTag> tags;
3233
private Map<String, Object> settings;
3334
private String idempotencyKey;
3435

@@ -211,6 +212,9 @@ public EmailEndpoint metadata(String key, Object value) {
211212
* Set the email tag.
212213
*/
213214
public EmailEndpoint tag(String tag) {
215+
if (tag != null && tags.size() >= 20) {
216+
throw new IllegalArgumentException("A legacy tag and no more than 19 message tags are permitted");
217+
}
214218
this.tag = tag;
215219
return this;
216220
}
@@ -220,6 +224,27 @@ public EmailEndpoint tag(String tag) {
220224
*/
221225
@SafeVarargs
222226
public final EmailEndpoint tags(Map<String, String>... tags) {
227+
MessageTag[] normalized = new MessageTag[tags.length];
228+
for (int index = 0; index < tags.length; index++) {
229+
normalized[index] = new MessageTag(tags[index].get("name"), tags[index].get("value"));
230+
}
231+
return tags(normalized);
232+
}
233+
234+
/**
235+
* Set typed reusable name-value tags for the email.
236+
*/
237+
public final EmailEndpoint tags(MessageTag... tags) {
238+
int maximum = tag == null ? 20 : 19;
239+
if (tags.length > maximum) {
240+
throw new IllegalArgumentException("No more than " + maximum + " message tags are permitted");
241+
}
242+
Set<String> names = new HashSet<>();
243+
for (MessageTag messageTag : tags) {
244+
if (!names.add(messageTag.getName())) {
245+
throw new IllegalArgumentException("Message tag names must be unique and case-sensitive");
246+
}
247+
}
223248
this.tags = new ArrayList<>(Arrays.asList(tags));
224249
return this;
225250
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package co.lettermint.models;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
5+
import java.util.Locale;
6+
import java.util.regex.Pattern;
7+
8+
/** A reusable exact-match message tag. */
9+
public final class MessageTag {
10+
private static final Pattern NAME = Pattern.compile("^[A-Za-z0-9_-]{1,32}$");
11+
private static final Pattern VALUE = Pattern.compile("^[A-Za-z0-9_-]{1,64}$");
12+
13+
@JsonProperty("name")
14+
private final String name;
15+
16+
@JsonProperty("value")
17+
private final String value;
18+
19+
public MessageTag(String name, String value) {
20+
if (name == null || !NAME.matcher(name).matches()) {
21+
throw new IllegalArgumentException("Message tag names must match ^[A-Za-z0-9_-]{1,32}$");
22+
}
23+
if (name.toLowerCase(Locale.ROOT).startsWith("__lettermint")) {
24+
throw new IllegalArgumentException("Message tag names must not start with __lettermint");
25+
}
26+
if (value == null || !VALUE.matcher(value).matches()) {
27+
throw new IllegalArgumentException("Message tag values must match ^[A-Za-z0-9_-]{1,64}$");
28+
}
29+
this.name = name;
30+
this.value = value;
31+
}
32+
33+
public String getName() { return name; }
34+
public String getValue() { return value; }
35+
}

‎src/test/java/co/lettermint/EmailEndpointTest.java‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package co.lettermint;
22

33
import co.lettermint.models.SendEmailResponse;
4+
import co.lettermint.models.MessageTag;
45
import okhttp3.mockwebserver.MockResponse;
56
import okhttp3.mockwebserver.MockWebServer;
67
import okhttp3.mockwebserver.RecordedRequest;
@@ -263,4 +264,36 @@ void testSingleMetadataMethod() throws Exception {
263264
assertTrue(body.contains("\"key1\":\"value1\""));
264265
assertTrue(body.contains("\"key2\":123"));
265266
}
267+
268+
@Test
269+
void testTypedMessageTags() throws Exception {
270+
mockWebServer.enqueue(new MockResponse()
271+
.setBody("{\"message_id\": \"msg_123\", \"status\": \"queued\"}")
272+
.setHeader("Content-Type", "application/json"));
273+
274+
lettermint.email()
275+
.from("sender@example.com")
276+
.to("recipient@example.com")
277+
.subject("Test")
278+
.text("Test")
279+
.tags(new MessageTag("campaign", "welcome"), new MessageTag("customer", "new"))
280+
.send();
281+
282+
String body = mockWebServer.takeRequest().getBody().readUtf8();
283+
assertTrue(body.contains("\"tags\":[{\"name\":\"campaign\",\"value\":\"welcome\"}"));
284+
}
285+
286+
@Test
287+
void testMessageTagValidation() {
288+
assertThrows(IllegalArgumentException.class, () -> new MessageTag("invalid name", "value"));
289+
assertThrows(IllegalArgumentException.class, () -> new MessageTag("__LETTERMINT_internal", "value"));
290+
assertThrows(IllegalArgumentException.class, () -> lettermint.email().tags(
291+
new MessageTag("same", "one"), new MessageTag("same", "two")));
292+
293+
MessageTag[] tags = new MessageTag[20];
294+
for (int index = 0; index < tags.length; index++) {
295+
tags[index] = new MessageTag("tag_" + index, "value");
296+
}
297+
assertThrows(IllegalArgumentException.class, () -> lettermint.email().tag("legacy").tags(tags));
298+
}
266299
}

0 commit comments

Comments
 (0)