Skip to content

Commit 55794da

Browse files
committed
feat(spec): Add PathTemplate value object with regex extraction
1 parent 5ea5a69 commit 55794da

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.retailsvc.http.spec;
2+
3+
import java.util.ArrayList;
4+
import java.util.LinkedHashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Optional;
8+
import java.util.regex.Matcher;
9+
import java.util.regex.Pattern;
10+
11+
public record PathTemplate(String raw, Pattern compiled, List<String> parameterNames) {
12+
13+
private static final Pattern TOKEN = Pattern.compile("\\{([^/}]+)}");
14+
15+
public static PathTemplate compile(String template) {
16+
StringBuilder regex = new StringBuilder("^");
17+
List<String> names = new ArrayList<>();
18+
Matcher m = TOKEN.matcher(template);
19+
int last = 0;
20+
while (m.find()) {
21+
regex.append(Pattern.quote(template.substring(last, m.start())));
22+
regex.append("([^/]+)");
23+
names.add(m.group(1));
24+
last = m.end();
25+
}
26+
regex.append(Pattern.quote(template.substring(last)));
27+
regex.append("$");
28+
return new PathTemplate(template, Pattern.compile(regex.toString()), List.copyOf(names));
29+
}
30+
31+
public Optional<Map<String, String>> match(String path) {
32+
Matcher m = compiled.matcher(path);
33+
if (!m.matches()) return Optional.empty();
34+
Map<String, String> out = new LinkedHashMap<>();
35+
for (int i = 0; i < parameterNames.size(); i++) {
36+
out.put(parameterNames.get(i), m.group(i + 1));
37+
}
38+
return Optional.of(Map.copyOf(out));
39+
}
40+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.retailsvc.http.spec;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
class PathTemplateTest {
8+
@Test
9+
void exactPathMatchesItself() {
10+
PathTemplate t = PathTemplate.compile("/users");
11+
assertThat(t.match("/users")).isPresent();
12+
assertThat(t.match("/users").get()).isEmpty();
13+
}
14+
15+
@Test
16+
void exactPathDoesNotMatchOther() {
17+
assertThat(PathTemplate.compile("/users").match("/orders")).isEmpty();
18+
}
19+
20+
@Test
21+
void singleParamExtracted() {
22+
PathTemplate t = PathTemplate.compile("/users/{id}");
23+
assertThat(t.match("/users/42"))
24+
.hasValueSatisfying(m -> assertThat(m).containsEntry("id", "42"));
25+
assertThat(t.parameterNames()).containsExactly("id");
26+
}
27+
28+
@Test
29+
void twoParamsExtracted() {
30+
PathTemplate t = PathTemplate.compile("/orgs/{org}/repos/{repo}");
31+
var m = t.match("/orgs/acme/repos/widget").orElseThrow();
32+
assertThat(m).containsEntry("org", "acme").containsEntry("repo", "widget");
33+
}
34+
35+
@Test
36+
void doesNotMatchSlashesInsideParam() {
37+
assertThat(PathTemplate.compile("/users/{id}").match("/users/42/foo")).isEmpty();
38+
}
39+
40+
@Test
41+
void rawIsPreserved() {
42+
assertThat(PathTemplate.compile("/users/{id}").raw()).isEqualTo("/users/{id}");
43+
}
44+
}

0 commit comments

Comments
 (0)