Skip to content

Commit

Permalink
param falls back to exact
Browse files Browse the repository at this point in the history
  • Loading branch information
jrhee17 committed May 16, 2024
1 parent b2aa86b commit e2d93c2
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,14 @@
*/
final class ParameterizedPathMapping extends AbstractPathMapping {

private static final Pattern VALID_PATTERN = Pattern.compile("(/[^/{}:]+|/:[^/{}]+|/\\{[^/{}]+})+/?");
private static final Pattern VALID_PATTERN = Pattern.compile(
'('
+ "/[^:{][^/]*|" // If the segment doesn't start with ':' or '{', the behavior should be the
// same as ExactPathMapping
+ "/:[^/{}]+|"
+ "/\\{[^/{}]+}"
+ ")+/?"
);

private static final Pattern CAPTURE_REST_PATTERN = Pattern.compile("/\\{\\*([^/{}]*)}|/:\\*([^/{}]*)");

Expand Down Expand Up @@ -136,21 +143,23 @@ private ParameterizedPathMapping(String prefix, String pathPattern) {
for (String token : PATH_SPLITTER.split(pathPattern)) {
final String paramName = paramName(token);
if (paramName == null) {
// If the given token is a constant, do not manipulate it.
// If the token escapes the first colon, then clean it. We don't need to handle '{'
// since it's not an allowed path character per rfc3986.
if (token.startsWith("\\:")) {
token = token.substring(1);
}

patternJoiner.add(token);
normalizedPatternJoiner.add(token);
skeletonJoiner.add(token);
continue;
}
// The paramName should not include colons.
// TODO: Implement param options expressed like `{bar:type=int,range=[0,10]}`.
final String colonRemovedParamName = removeColonAndFollowing(paramName);
final boolean captureRestPathMatching = isCaptureRestPathMatching(token);
final int paramNameIdx = paramNames.indexOf(colonRemovedParamName);
final int paramNameIdx = paramNames.indexOf(paramName);
if (paramNameIdx < 0) {
// If the given token appeared first time, add it to the set and
// replace it with a capturing group expression in regex.
paramNames.add(colonRemovedParamName);
paramNames.add(paramName);
if (captureRestPathMatching) {
patternJoiner.add("(.*)");
} else {
Expand All @@ -162,7 +171,7 @@ private ParameterizedPathMapping(String prefix, String pathPattern) {
patternJoiner.add("\\" + (paramNameIdx + 1));
}

normalizedPatternJoiner.add((captureRestPathMatching ? ":*" : ':') + colonRemovedParamName);
normalizedPatternJoiner.add((captureRestPathMatching ? ":*" : ':') + paramName);
skeletonJoiner.add(captureRestPathMatching ? "*" : "\0");
}

Expand Down Expand Up @@ -201,17 +210,6 @@ private static String paramName(String token) {
return null;
}

/**
* Removes the portion of the specified string following a colon (:).
*/
private static String removeColonAndFollowing(String paramName) {
final int index = paramName.indexOf(':');
if (index == -1) {
return paramName;
}
return paramName.substring(0, index);
}

/**
* Return true if path parameter contains capture the rest path pattern
* ({@code "{*foo}"}" or {@code ":*foo"}).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ private static PathMapping getPathMapping(String pathPattern) {
"pathPattern: " + pathPattern +
" (not an absolute path starting with '/' or a unknown pattern type)");
}
if (!pathPattern.contains("{") && !pathPattern.contains("/:")) {
if (!pathPattern.contains("/{") && !pathPattern.contains("/:")) {
return new ExactPathMapping(pathPattern);
}
return new ParameterizedPathMapping(pathPattern);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.Map;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class ParameterizedPathMappingTest {
@Test
Expand Down Expand Up @@ -239,10 +243,22 @@ void captureRestPattern_invalidPattern() {
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new ParameterizedPathMapping("/service/:* "))
.isInstanceOf(IllegalArgumentException.class);
// "{*...}" or ":*..." can only be preceded by a path separator.
assertThatThrownBy(() -> new ParameterizedPathMapping("/service/foo{*value}"))
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new ParameterizedPathMapping("/service/foo:*value"))
.isInstanceOf(IllegalArgumentException.class);
}

private static Stream<Arguments> colonAllowedArgs() {
return Stream.of(
Arguments.of("/foo:bar", "/foo:bar"),
Arguments.of("/a:/b:asdf", "/a:/b:asdf"),
Arguments.of("/\\:/\\:asdf:", "/:/:asdf:")
);
}

@ParameterizedTest
@MethodSource("colonAllowedArgs")
void colonAllowed(String pathPattern, String reqPath) {
final ParameterizedPathMapping m = new ParameterizedPathMapping(pathPattern);
final RoutingResult res = m.apply(create(reqPath)).build();
assertThat(res.path()).isEqualTo(reqPath);
assertThat(res.pathParams()).isEmpty();
}
}
6 changes: 3 additions & 3 deletions core/src/test/java/com/linecorp/armeria/server/RouteTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ void route() {
assertThat(route.pathType()).isSameAs(RoutePathType.PARAMETERIZED);
assertThat(route.paths()).containsExactly("/foo/\0", "/foo/\0");
assertThat(route.paramNames()).hasSize(1);
assertThat(route.paramNames()).containsExactly("bar");
assertThat(route.paramNames()).containsExactly("bar:biz");

route = Route.builder().path("/bar/:baz").build();
assertThat(route.pathType()).isSameAs(RoutePathType.PARAMETERIZED);
Expand All @@ -87,7 +87,7 @@ void route() {
assertThat(route.pathType()).isSameAs(RoutePathType.PARAMETERIZED);
assertThat(route.paths()).containsExactly("/bar/\0", "/bar/\0");
assertThat(route.paramNames()).hasSize(1);
assertThat(route.paramNames()).containsExactly("baz");
assertThat(route.paramNames()).containsExactly("baz:");

route = Route.builder().path("/bar/:baz:verb").build();
assertThat(route.pathType()).isSameAs(RoutePathType.PARAMETERIZED);
Expand All @@ -100,7 +100,7 @@ void route() {
assertThat(route.pathType()).isSameAs(RoutePathType.PARAMETERIZED);
assertThat(route.paths()).containsExactly("/bar/\0", "/bar/\0");
assertThat(route.paramNames()).hasSize(1);
assertThat(route.paramNames()).containsExactly("baz");
assertThat(route.paramNames()).containsExactly("baz:foo:verb");

route = Route.builder().path("exact:/:foo/bar").build();
assertThat(route.pathType()).isSameAs(RoutePathType.EXACT);
Expand Down
10 changes: 6 additions & 4 deletions site/src/pages/docs/server-basics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,13 @@ You can use the following path patterns to map an HTTP request path to a service
+------------------------------------------+---------------------------------------+

<Tip>
You can append a verb suffix to the path as shown below:
By default, colon style path variables assume that a path segment starting with a ':' is a parameter name.
If the colon starting a path segment should signify a literal, you can prefix the ':' with '\\'.

- `/foo/bar:verb`
- `/users/{userId}:update`
- `/users/:userId:update`
e.g.
- `/foo/\\:colon:` will match a request with path `/foo/:colon:`.
- Note that only the first colon starting the segment should be escaped.
- `/foo/:colon` will match a request with path `/foo/bar`. The path parameter 'colon' will be bound to 'bar'.

</Tip>

Expand Down

0 comments on commit e2d93c2

Please sign in to comment.