Skip to content

Commit cd249b8

Browse files
committed
openapi: regex from path variable is ignored it fix jooby-project#1585
- wildcard patterns now works too
1 parent e28ef89 commit cd249b8

File tree

6 files changed

+202
-2
lines changed

6 files changed

+202
-2
lines changed

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,21 @@ default Router error(@Nonnull Predicate<StatusCode> predicate,
844844
* @return Path keys.
845845
*/
846846
static @Nonnull List<String> pathKeys(@Nonnull String pattern) {
847+
return pathKeys(pattern, (k, v) -> {
848+
});
849+
}
850+
851+
/**
852+
* Extract path keys from given path pattern. A path key (a.k.a path variable) looks like:
853+
*
854+
* <pre>/product/{id}</pre>
855+
*
856+
* @param pattern Path pattern.
857+
* @param consumer Listen for key and regex variables found.
858+
* @return Path keys.
859+
*/
860+
static @Nonnull List<String> pathKeys(@Nonnull String pattern,
861+
BiConsumer<String, String> consumer) {
847862
List<String> result = new ArrayList<>();
848863
int start = -1;
849864
int end = Integer.MAX_VALUE;
@@ -863,16 +878,26 @@ default Router error(@Nonnull Predicate<StatusCode> predicate,
863878
curly -= 1;
864879
if (curly == 0) {
865880
String id = pattern.substring(start, Math.min(i, end));
881+
String value;
882+
if (end == Integer.MAX_VALUE) {
883+
value = null;
884+
} else {
885+
value = pattern.substring(end + 1, i);
886+
}
887+
consumer.accept(id, value);
866888
result.add(id);
867889
start = -1;
868890
end = Integer.MAX_VALUE;
869891
}
870892
} else if (ch == '*') {
893+
String id;
871894
if (i == len - 1) {
872-
result.add("*");
895+
id = "*";
873896
} else {
874-
result.add(pattern.substring(i + 1));
897+
id = pattern.substring(i + 1);
875898
}
899+
result.add(id);
900+
consumer.accept(id, "\\.*");
876901
i = len;
877902
}
878903
}

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import static org.junit.jupiter.api.Assertions.assertEquals;
1313
import static org.junit.jupiter.api.Assertions.assertFalse;
14+
import static org.junit.jupiter.api.Assertions.assertNull;
1415
import static org.junit.jupiter.api.Assertions.assertTrue;
1516

1617
public class RouterTest {
@@ -40,6 +41,39 @@ public void pathKeys() {
4041
pathKeys("/foo/{x}", keys -> assertEquals(1, keys.size()));
4142
}
4243

44+
@Test
45+
public void pathKeyMap() {
46+
pathKeyMap("/{lang:[a-z]{2}}", map -> {
47+
assertEquals("[a-z]{2}", map.get("lang"));
48+
});
49+
50+
pathKeyMap("/{id:[0-9]+}", map -> {
51+
assertEquals("[0-9]+", map.get("id"));
52+
});
53+
54+
pathKeyMap("/edit/{id}?", keys -> {
55+
assertEquals(null, keys.get("id"));
56+
});
57+
58+
pathKeyMap("/path/{id}/{start}?/{end}?", keys -> {
59+
assertEquals(null, keys.get("id"));
60+
assertEquals(null, keys.get("start"));
61+
assertEquals(null, keys.get("end"));
62+
});
63+
64+
pathKeyMap("/*", keys -> {
65+
assertEquals("\\.*", keys.get("*"));
66+
});
67+
68+
pathKeyMap("/foo/?*", keys -> {
69+
assertEquals("\\.*", keys.get("*"));
70+
});
71+
72+
pathKeyMap("/foo/*name", keys -> {
73+
assertEquals("\\.*", keys.get("name"));
74+
});
75+
}
76+
4377
@Test
4478
public void reverseByPosition() {
4579
assertEquals("/foo", Router.reverse("/{k}", "foo"));
@@ -156,6 +190,12 @@ private void pathKeys(String pattern, Consumer<List<String>> consumer) {
156190
consumer.accept(Router.pathKeys(pattern));
157191
}
158192

193+
private void pathKeyMap(String pattern, Consumer<Map<String, String>> consumer) {
194+
Map<String, String> map = new HashMap<>();
195+
Router.pathKeys(pattern, map::put);
196+
consumer.accept(map);
197+
}
198+
159199
private void parse(String pattern, Consumer<List<String>> consumer) {
160200
consumer.accept(Router.expandOptionalVariables(pattern));
161201
}

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.Collections;
1616
import java.util.LinkedList;
1717
import java.util.List;
18+
import java.util.Optional;
1819

1920
import static io.jooby.internal.openapi.StatusCodeParser.isSuccessCode;
2021

@@ -126,6 +127,12 @@ public Parameter getParameter(int i) {
126127
return null;
127128
}
128129

130+
public Optional<Parameter> getParameter(String name) {
131+
return getParameters().stream()
132+
.filter(p -> p.getName().equals(name))
133+
.findFirst();
134+
}
135+
129136
public ResponseExt addResponse(String code) {
130137
responseCodes.add(code);
131138
return (ResponseExt) getResponses().computeIfAbsent(code, statusCode -> {

modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
import java.nio.file.Files;
3131
import java.nio.file.Path;
3232
import java.util.Collections;
33+
import java.util.HashMap;
3334
import java.util.List;
35+
import java.util.Map;
3436
import java.util.Optional;
3537
import java.util.Set;
3638
import java.util.regex.Pattern;
@@ -176,6 +178,27 @@ public enum Format {
176178
log.debug("skipping {}", pattern);
177179
continue;
178180
}
181+
Map<String, String> regexMap = new HashMap<>();
182+
Router.pathKeys(pattern, (key, value) -> Optional.ofNullable(value)
183+
.ifPresent(v -> regexMap.put(key, v)));
184+
if (regexMap.size() > 0) {
185+
for (Map.Entry<String, String> e : regexMap.entrySet()) {
186+
String name = e.getKey();
187+
String regex = e.getValue();
188+
operation.getParameter(name).ifPresent(parameter ->
189+
parameter.getSchema().setPattern(regex)
190+
);
191+
if (regex.equals("\\.*")) {
192+
if (name.equals("*")) {
193+
pattern = pattern.substring(0, pattern.length() - 1) + "{*}";
194+
} else {
195+
pattern = pattern.replace("*" + name, "{" + name + "}");
196+
}
197+
} else {
198+
pattern = pattern.replace(name + ":" + regex, name);
199+
}
200+
}
201+
}
179202
PathItem pathItem = paths.computeIfAbsent(pattern, k -> new PathItem());
180203
pathItem.operation(PathItem.HttpMethod.valueOf(operation.getMethod()), operation);
181204
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package issues;
2+
3+
import io.jooby.Jooby;
4+
5+
public class App1585 extends Jooby {
6+
{
7+
get("/user/{id:[0-9]+}", ctx -> ctx.path("id").intValue());
8+
9+
get("/company/{id}", ctx -> ctx.path("id").intValue());
10+
11+
get("/file/*", ctx -> ctx.path("*").value());
12+
13+
get("/resources/*path", ctx -> ctx.path("path").value());
14+
}
15+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package issues;
2+
3+
import io.jooby.openapi.OpenAPIResult;
4+
import io.jooby.openapi.OpenAPITest;
5+
6+
import static org.junit.jupiter.api.Assertions.assertEquals;
7+
8+
public class Issue1585 {
9+
10+
@OpenAPITest(App1585.class)
11+
public void shouldAddRegex(OpenAPIResult result) {
12+
assertEquals("openapi: 3.0.1\n"
13+
+ "info:\n"
14+
+ " title: 1584 API\n"
15+
+ " description: 1584 API description\n"
16+
+ " version: \"1.0\"\n"
17+
+ "paths:\n"
18+
+ " /user/{id}:\n"
19+
+ " get:\n"
20+
+ " operationId: getUserId09\n"
21+
+ " parameters:\n"
22+
+ " - name: id\n"
23+
+ " in: path\n"
24+
+ " required: true\n"
25+
+ " schema:\n"
26+
+ " pattern: '[0-9]+'\n"
27+
+ " type: integer\n"
28+
+ " format: int32\n"
29+
+ " responses:\n"
30+
+ " \"200\":\n"
31+
+ " description: Success\n"
32+
+ " content:\n"
33+
+ " application/json:\n"
34+
+ " schema:\n"
35+
+ " type: integer\n"
36+
+ " format: int32\n"
37+
+ " /company/{id}:\n"
38+
+ " get:\n"
39+
+ " operationId: getCompanyId\n"
40+
+ " parameters:\n"
41+
+ " - name: id\n"
42+
+ " in: path\n"
43+
+ " required: true\n"
44+
+ " schema:\n"
45+
+ " type: integer\n"
46+
+ " format: int32\n"
47+
+ " responses:\n"
48+
+ " \"200\":\n"
49+
+ " description: Success\n"
50+
+ " content:\n"
51+
+ " application/json:\n"
52+
+ " schema:\n"
53+
+ " type: integer\n"
54+
+ " format: int32\n"
55+
+ " /file/{*}:\n"
56+
+ " get:\n"
57+
+ " operationId: getFile\n"
58+
+ " parameters:\n"
59+
+ " - name: '*'\n"
60+
+ " in: path\n"
61+
+ " required: true\n"
62+
+ " schema:\n"
63+
+ " pattern: \\.*\n"
64+
+ " type: string\n"
65+
+ " responses:\n"
66+
+ " \"200\":\n"
67+
+ " description: Success\n"
68+
+ " content:\n"
69+
+ " application/json:\n"
70+
+ " schema:\n"
71+
+ " type: string\n"
72+
+ " /resources/{path}:\n"
73+
+ " get:\n"
74+
+ " operationId: getResourcesPath\n"
75+
+ " parameters:\n"
76+
+ " - name: path\n"
77+
+ " in: path\n"
78+
+ " required: true\n"
79+
+ " schema:\n"
80+
+ " pattern: \\.*\n"
81+
+ " type: string\n"
82+
+ " responses:\n"
83+
+ " \"200\":\n"
84+
+ " description: Success\n"
85+
+ " content:\n"
86+
+ " application/json:\n"
87+
+ " schema:\n"
88+
+ " type: string\n", result.toYaml());
89+
}
90+
}

0 commit comments

Comments
 (0)