Skip to content

Commit a1bf098

Browse files
committed
Route reverse path
1 parent e5ca35a commit a1bf098

File tree

3 files changed

+138
-0
lines changed

3 files changed

+138
-0
lines changed

jooby/src/main/java/io/jooby/Route.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,30 @@ public Route(@Nonnull String method, @Nonnull String pattern, @Nonnull Handler h
437437
return pipeline;
438438
}
439439

440+
/**
441+
* Recreate a path pattern using the given variables.
442+
*
443+
* <code>reserve(/{k1}/{k2}, {"k1": ""foo", "k2": "bar"}) => /foo/bar</code>
444+
*
445+
* @param keys Path keys.
446+
* @return Path.
447+
*/
448+
public @Nonnull String reverse(@Nonnull Map<String, Object> keys) {
449+
return Router.reverse(getPattern(), keys);
450+
}
451+
452+
/**
453+
* Recreate a path pattern using the given variables.
454+
*
455+
* <code>reserve(/{k1}/{k2}, "foo", "bar") => /foo/bar</code>
456+
*
457+
* @param values Values.
458+
* @return Path.
459+
*/
460+
public @Nonnull String reverse(Object... values) {
461+
return Router.reverse(getPattern(), values);
462+
}
463+
440464
/**
441465
* Handler instance which might or might not be the same as {@link #getHandler()}.
442466
*

jooby/src/main/java/io/jooby/Router.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@
1717
import java.nio.file.Paths;
1818
import java.util.ArrayList;
1919
import java.util.Collections;
20+
import java.util.HashMap;
2021
import java.util.List;
2122
import java.util.Map;
2223
import java.util.Set;
2324
import java.util.concurrent.Executor;
2425
import java.util.function.Predicate;
26+
import java.util.stream.IntStream;
2527
import java.util.stream.Stream;
2628

2729
import static java.util.Arrays.asList;
2830
import static java.util.Collections.unmodifiableList;
31+
import static java.util.Objects.requireNonNull;
2932

3033
/**
3134
* Routing DSL functions.
@@ -870,4 +873,70 @@ default Router error(@Nonnull Predicate<StatusCode> predicate,
870873
return unmodifiableList(result);
871874
}
872875
}
876+
877+
/**
878+
* Recreate a path pattern using the given variables. Variable replacement is done using the
879+
* current index.
880+
*
881+
* @param pattern Path pattern.
882+
* @param values Path keys.
883+
* @return Path.
884+
*/
885+
static @Nonnull String reverse(@Nonnull String pattern, @Nonnull Object... values) {
886+
Map<String, Object> keys = new HashMap<>();
887+
IntStream.range(0, values.length).forEach(k -> keys.put(Integer.toString(k), values[k]));
888+
return reverse(pattern, keys);
889+
}
890+
891+
/**
892+
* Recreate a path pattern using the given variables.
893+
*
894+
* @param pattern Path pattern.
895+
* @param keys Path keys.
896+
* @return Path.
897+
*/
898+
static @Nonnull String reverse(@Nonnull String pattern, @Nonnull Map<String, Object> keys) {
899+
StringBuilder path = new StringBuilder();
900+
int start = 0;
901+
int end = Integer.MAX_VALUE;
902+
int len = pattern.length();
903+
int keyIdx = 0;
904+
for (int i = 0; i < len; i++) {
905+
char ch = pattern.charAt(i);
906+
if (ch == '{') {
907+
path.append(pattern, start, i);
908+
start = i + 1;
909+
end = Integer.MAX_VALUE;
910+
} else if (ch == ':') {
911+
end = i;
912+
} else if (ch == '}') {
913+
String id = pattern.substring(start, Math.min(i, end));
914+
Object value = keys.getOrDefault(id, keys.get(Integer.toString(keyIdx++)));
915+
requireNonNull(value, "Missing key: '" + id + "'");
916+
path.append(value);
917+
start = i + 1;
918+
end = Integer.MAX_VALUE;
919+
} else if (ch == '*') {
920+
path.append(pattern, start, i);
921+
String id;
922+
if (i == len - 1) {
923+
id = "*";
924+
} else {
925+
id = pattern.substring(i + 1);
926+
}
927+
Object value = keys.getOrDefault(id, keys.get(Integer.toString(keyIdx++)));
928+
requireNonNull(value, "Missing key: '" + id + "'");
929+
path.append(value);
930+
start = len;
931+
i = len;
932+
}
933+
}
934+
if (path.length() == 0) {
935+
return pattern;
936+
}
937+
if (start > 0) {
938+
path.append(pattern, start, len);
939+
}
940+
return path.toString();
941+
}
873942
}

jooby/src/test/java/io/jooby/RouterTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
import org.junit.jupiter.api.Test;
44

5+
import java.util.Collections;
6+
import java.util.HashMap;
57
import java.util.List;
8+
import java.util.Map;
69
import java.util.function.Consumer;
710

811
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -20,10 +23,52 @@ public void pathKeys() {
2023
pathKeys("/", keys -> assertEquals(0, keys.size()));
2124
pathKeys("/foo/bar", keys -> assertEquals(0, keys.size()));
2225
pathKeys("/foo/*", keys -> assertEquals(1, keys.size()));
26+
pathKeys("/foo/*name", keys -> assertEquals(1, keys.size()));
2327
pathKeys("/foo/{x}", keys -> assertEquals(1, keys.size()));
2428
}
2529

30+
@Test
31+
public void reverseByPosition() {
32+
assertEquals("/foo", Router.reverse("/{k}", "foo"));
33+
assertEquals("/foo/bar", Router.reverse("/{k1}/{k2}", "foo", "bar"));
34+
}
35+
36+
@Test
37+
public void reverse() {
38+
assertEquals("foo", Router.reverse("{foo}", map("foo", "foo")));
39+
40+
assertEquals("/tail", Router.reverse("/*", map("*", "tail")));
41+
42+
assertEquals("/file/video.mpg", Router.reverse("/file/*", map("*", "video.mpg")));
43+
assertEquals("/file/video.mpg", Router.reverse("/file/*video", map("video", "video.mpg")));
44+
45+
assertEquals("/123", Router.reverse("/{regex:\\d+}", map("regex", 123)));
46+
47+
assertEquals("/resources/123/edit", Router.reverse("/resources/{num:\\d+}/edit", map("num", 123)));
48+
49+
assertEquals("/prefix/v1/v2", Router.reverse("/prefix/{k1}/{k2}", map("k1", "v1", "k2", "v2")));
50+
51+
assertEquals("/v1/suffix", Router.reverse("/{k1}/suffix", map("k1", "v1", "k2", "v2")));
52+
53+
assertEquals("/foo/v", Router.reverse("/{name}/{k}", map("name", "foo", "k", "v")));
54+
55+
assertEquals("/foo", Router.reverse("/{name}", map("name", "foo")));
56+
57+
assertEquals("/", Router.reverse("/", Collections.emptyMap()));
58+
assertEquals("/path", Router.reverse("/path", Collections.emptyMap()));
59+
assertEquals("/path", Router.reverse("/path", map("k", "v")));
60+
61+
}
62+
2663
private void pathKeys(String pattern, Consumer<List<String>> consumer) {
2764
consumer.accept(Router.pathKeys(pattern));
2865
}
66+
67+
public Map<String, Object> map(Object... values) {
68+
Map<String, Object> map = new HashMap<>();
69+
for (int i = 0; i < values.length; i += 2) {
70+
map.put(values[i].toString(), values[i + 1]);
71+
}
72+
return map;
73+
}
2974
}

0 commit comments

Comments
 (0)