Skip to content

Commit 0685cac

Browse files
committed
Swagger: POST with Query Parameters fix #611
1 parent ef1239b commit 0685cac

File tree

14 files changed

+301
-21
lines changed

14 files changed

+301
-21
lines changed

jooby-raml/src/test/java/org/jooby/raml/RamlBuilderTest.java

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,23 @@ public Type type() {
6767
return type;
6868
}
6969

70+
@Override
71+
public RouteParam type(final Type type) {
72+
this.type = type;
73+
return this;
74+
}
75+
7076
@Override
7177
public RouteParamType paramType() {
7278
return paramType;
7379
}
7480

81+
@Override
82+
public RouteParam paramType(final RouteParamType type) {
83+
paramType = type;
84+
return this;
85+
}
86+
7587
@Override
7688
public Object value() {
7789
return value;
@@ -242,7 +254,7 @@ public Spec rsp(final Consumer<Response> rsp) {
242254
}
243255

244256
private Config conf = ConfigFactory.empty()
245-
.withValue("mediaType", ConfigValueFactory.fromAnyRef("application/json"));
257+
.withValue("mediaType", ConfigValueFactory.fromAnyRef("application/json"));
246258

247259
@Test
248260
public void routes() {
@@ -395,8 +407,10 @@ public void bodyParam() {
395407
" post:\n" +
396408
" description: |-\n" +
397409
" Enters the file content for an existing song entity. \n\n" +
398-
" * Use the `binary/octet-stream` content type to specify the content from any consumer (excepting web-browsers).\n\n" +
399-
" * Use the `multipart-form/data` content type to upload a file which content will become the file-content\n" +
410+
" * Use the `binary/octet-stream` content type to specify the content from any consumer (excepting web-browsers).\n\n"
411+
+
412+
" * Use the `multipart-form/data` content type to upload a file which content will become the file-content\n"
413+
+
400414
" body:\n" +
401415
" application/json:\n" +
402416
" type: integer", raml);
@@ -420,8 +434,10 @@ public void fileParam() {
420434
" post:\n" +
421435
" description: |-\n" +
422436
" Enters the file content for an existing song entity. \n\n" +
423-
" * Use the `binary/octet-stream` content type to specify the content from any consumer (excepting web-browsers).\n\n" +
424-
" * Use the `multipart-form/data` content type to upload a file which content will become the file-content\n" +
437+
" * Use the `binary/octet-stream` content type to specify the content from any consumer (excepting web-browsers).\n\n"
438+
+
439+
" * Use the `multipart-form/data` content type to upload a file which content will become the file-content\n"
440+
+
425441
" body:\n" +
426442
" multipart/form-data:\n" +
427443
" formParameters:\n" +
@@ -472,7 +488,8 @@ public void rsp2() {
472488
.build(Arrays.asList(
473489
route("GET", "/users/:userId", path("userId", int.class, "The id of the user"))
474490
.rsp(
475-
rsp -> rsp.type(Person.class).status(200, "Success").status(404, "Not found\nNextLine"))))
491+
rsp -> rsp.type(Person.class).status(200, "Success").status(404,
492+
"Not found\nNextLine"))))
476493
.trim();
477494
assertEquals("#%RAML 1.0\n" +
478495
"mediaType: application/json\n" +

jooby-spec/src/main/java/org/jooby/internal/spec/DocCollector.java

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.List;
2525
import java.util.Map;
2626
import java.util.Optional;
27+
import java.util.function.BiConsumer;
2728
import java.util.regex.Matcher;
2829
import java.util.regex.Pattern;
2930
import java.util.stream.Collectors;
@@ -45,15 +46,15 @@ public class DocCollector extends VoidVisitorAdapter<Context> {
4546

4647
private static final Pattern SPLITTER = Pattern.compile("\\s+\\*");
4748

48-
private static final Pattern PARAM = Pattern.compile("(@param)\\s+([^\\s]+)([^@]+)");
49-
5049
private static final String RETURNS = "@return";
5150

51+
private static final String PARAM = "@param";
52+
53+
private static final String THROWS = "@throws";
54+
5255
private static final Pattern CODE = Pattern
5356
.compile("<code>\\s*(\\d+)\\s*(=\\s*([^<]+))?\\s*</code>");
5457

55-
private static final Pattern TYPE = Pattern.compile("\\{@link\\s+([^\\}]+)\\}");
56-
5758
private Map<String, Object> doc = new HashMap<>();
5859

5960
public Map<String, Object> accept(final Node node, final String method, final Context ctx) {
@@ -126,10 +127,8 @@ private Map<String, Object> doc(final Node node, final Context ctx) {
126127
Map<Integer, String> codes = Collections.emptyMap();
127128
String tail = clean.substring(Math.max(0, at));
128129
// params
129-
Matcher pmatcher = PARAM.matcher(tail);
130-
while (pmatcher.find()) {
131-
hash.put(pmatcher.group(2).trim(), pmatcher.group(3).trim());
132-
}
130+
params(tail, hash::put);
131+
133132
// returns
134133
String returnText = returnText(tail);
135134
codes = new LinkedHashMap<>();
@@ -142,17 +141,40 @@ private Map<String, Object> doc(final Node node, final Context ctx) {
142141
codes.put(status.value(), message);
143142
}
144143

145-
Matcher tmatcher = TYPE.matcher(returnText);
146-
if (tmatcher.find()) {
147-
ctx.resolveType(node, tmatcher.group(1).trim()).ifPresent(t -> hash.put("@type", t));
148-
}
144+
TypeFromDoc.parse(node, ctx, returnText).ifPresent(type -> hash.put("@type", type));
149145
}
150146
hash.put("@statusCodes", codes);
151147
hash.put("@text", text);
152148
}
153149
return hash;
154150
}
155151

152+
private void params(final String text, final BiConsumer<String, String> callback) {
153+
int at = text.indexOf(PARAM);
154+
while (at != -1) {
155+
int start = at + PARAM.length();
156+
int end = firstOf(text, start, PARAM, RETURNS, THROWS);
157+
String raw = text.substring(start, end).trim();
158+
int space = raw.indexOf(" ");
159+
if (space != -1) {
160+
String name = raw.substring(0, space).trim();
161+
String desc = raw.substring(space).trim();
162+
callback.accept(name, desc);
163+
}
164+
at = text.indexOf(PARAM, end);
165+
}
166+
}
167+
168+
private int firstOf(final String text, final int start, final String... tokens) {
169+
for (String token : tokens) {
170+
int pos = text.indexOf(token, start);
171+
if (pos != -1) {
172+
return pos;
173+
}
174+
}
175+
return text.length();
176+
}
177+
156178
private String returnText(final String doc) {
157179
int retIdx = doc.indexOf(RETURNS);
158180
if (retIdx >= 0) {

jooby-spec/src/main/java/org/jooby/internal/spec/RouteParamImpl.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ public Type type() {
5858
return get("type");
5959
}
6060

61+
@Override
62+
public RouteParam type(final Type type) {
63+
put("type", type);
64+
return this;
65+
}
66+
6167
@Override
6268
public String name() {
6369
return get("name");
@@ -68,6 +74,12 @@ public RouteParamType paramType() {
6874
return RouteParamType.valueOf(get("paramType"));
6975
}
7076

77+
@Override
78+
public RouteParam paramType(final RouteParamType type) {
79+
put("paramType", type.name());
80+
return this;
81+
}
82+
7183
@Override
7284
public String toString() {
7385
return MoreObjects.toStringHelper("")
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.jooby.internal.spec;
2+
3+
import java.lang.reflect.Type;
4+
import java.util.Optional;
5+
import java.util.regex.Matcher;
6+
import java.util.regex.Pattern;
7+
8+
import com.github.javaparser.ast.Node;
9+
import com.google.inject.util.Types;
10+
11+
public class TypeFromDoc {
12+
13+
private static final Pattern TYPE = Pattern.compile("\\{@link\\s+([^\\}]+)\\}");
14+
15+
16+
public static Optional<Type> parse(final Node node, final Context ctx, final String text) {
17+
Matcher matcher = TYPE.matcher(text);
18+
Type type = null;
19+
while (matcher.find()) {
20+
String link = matcher.group(1).trim();
21+
String stype = link.split("\\s+")[0];
22+
Optional<Type> resolvedType = ctx.resolveType(node, stype);
23+
if (resolvedType.isPresent()) {
24+
Type ittype = resolvedType.get();
25+
if (type != null) {
26+
type = Types.newParameterizedType(type, ittype);
27+
} else {
28+
type = ittype;
29+
}
30+
}
31+
}
32+
return Optional.ofNullable(type);
33+
}
34+
}

jooby-spec/src/main/java/org/jooby/spec/RouteParam.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,27 @@ public interface RouteParam {
3838
*/
3939
Type type();
4040

41+
/**
42+
* Set parameter data type.
43+
*
44+
* @param type Data type.
45+
* @return This param.
46+
*/
47+
RouteParam type(Type type);
48+
4149
/**
4250
* @return Type of HTTP param.
4351
*/
4452
RouteParamType paramType();
4553

54+
/**
55+
* Set parameter type.
56+
*
57+
* @param type Paremater type.
58+
* @return This param.
59+
*/
60+
RouteParam paramType(RouteParamType type);
61+
4662
/**
4763
* @return Default value or <code>null</code>
4864
*/
@@ -61,4 +77,4 @@ default boolean optional() {
6177
return type().toString().startsWith(java.util.Optional.class.getName())
6278
|| value() != null;
6379
}
64-
}
80+
}

jooby-spec/src/main/java/org/jooby/spec/RouteProcessor.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import java.lang.reflect.Type;
3030
import java.nio.file.Path;
3131
import java.util.ArrayList;
32-
import java.util.Collections;
3332
import java.util.HashSet;
3433
import java.util.List;
3534
import java.util.Map;
@@ -57,6 +56,7 @@
5756
import org.jooby.internal.spec.RouteSpecImpl;
5857
import org.jooby.internal.spec.SourceResolver;
5958
import org.jooby.internal.spec.SourceResolverImpl;
59+
import org.jooby.internal.spec.TypeFromDoc;
6060
import org.jooby.internal.spec.TypeResolverImpl;
6161
import org.jooby.mvc.Body;
6262
import org.jooby.mvc.Flash;
@@ -277,7 +277,7 @@ private List<RouteSpec> processInternal(final Class<? extends Jooby> appClass,
277277
Type retType = (Type) doc.remove("@type");
278278

279279
/** params and return type */
280-
List<RouteParam> params = Collections.emptyList();
280+
List<RouteParam> params;
281281
RouteResponse rsp;
282282
if (method == null) {
283283
// script params
@@ -293,6 +293,22 @@ private List<RouteSpec> processInternal(final Class<? extends Jooby> appClass,
293293
retDoc, codes);
294294
}
295295

296+
// test if body param is present, when present all the other params are set to query
297+
params.stream()
298+
.filter(p -> p.paramType() == RouteParamType.BODY)
299+
.findFirst()
300+
.ifPresent(p -> {
301+
params.stream().filter(it -> it != p)
302+
.forEach(it -> it.paramType(RouteParamType.QUERY));
303+
});
304+
// ovewrite param type if need it
305+
params.stream()
306+
.forEach(p -> {
307+
p.doc().ifPresent(pdoc -> {
308+
TypeFromDoc.parse(entry.getValue(), ctx, pdoc).ifPresent(p::type);
309+
});
310+
});
311+
296312
/** Create spec . */
297313
specs.add(new RouteSpecImpl(route, summary, desc, params, rsp));
298314
} else {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package apps;
2+
3+
import org.jooby.Jooby;
4+
5+
public class App611 extends Jooby {
6+
7+
{
8+
use(Controller611.class);
9+
}
10+
11+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package apps;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
6+
import org.jooby.Deferred;
7+
import org.jooby.mvc.Body;
8+
import org.jooby.mvc.POST;
9+
import org.jooby.mvc.Path;
10+
import org.jooby.spec.User611;
11+
12+
import com.fasterxml.jackson.databind.node.ArrayNode;
13+
14+
public class Controller611 {
15+
16+
/**
17+
* Find users by email address.
18+
*
19+
* @param userId User ID.
20+
* @param context Context value.
21+
* @param emails {@link List} of {@link String mails}.
22+
* @return Returns a {@link List} of {@link User611}.
23+
*/
24+
@POST
25+
@Path("/friends/email")
26+
public Deferred findUsersByEmail(final Long userId, final Optional<String> context,
27+
@Body final ArrayNode emails) {
28+
return Deferred.deferred(deferred -> {
29+
return null;
30+
});
31+
}
32+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.jooby.spec;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import java.io.File;
6+
import java.nio.file.Path;
7+
8+
import org.junit.Test;
9+
10+
import apps.App611;
11+
12+
public class Issue611Test extends RouteSpecTest {
13+
14+
private Path basedir = new File(System.getProperty("user.dir")).toPath();
15+
16+
@Test
17+
public void postWithQueryParamereters() throws Exception {
18+
routes(new RouteProcessor().process(new App611(), basedir))
19+
.next(r -> {
20+
assertEquals("POST", r.method());
21+
assertEquals("/friends/email", r.pattern());
22+
23+
params(r.params())
24+
.next(p -> {
25+
assertEquals("userId", p.name());
26+
assertEquals("User ID.", p.doc().get());
27+
assertEquals(RouteParamType.QUERY, p.paramType());
28+
assertEquals(false, p.optional());
29+
assertEquals(Long.class, p.type());
30+
})
31+
.next(p -> {
32+
assertEquals("context", p.name());
33+
assertEquals("Context value.", p.doc().get());
34+
assertEquals(RouteParamType.QUERY, p.paramType());
35+
assertEquals(true, p.optional());
36+
assertEquals("java.util.Optional<java.lang.String>", p.type().getTypeName());
37+
}).next(p -> {
38+
assertEquals("emails", p.name());
39+
assertEquals("{@link List} of {@link String mails}.", p.doc().get());
40+
assertEquals(RouteParamType.BODY, p.paramType());
41+
assertEquals(false, p.optional());
42+
assertEquals("java.util.List<java.lang.String>",
43+
p.type().getTypeName());
44+
});
45+
46+
RouteResponse rsp = r.response();
47+
assertEquals("java.util.List<org.jooby.spec.User611>", rsp.type().getTypeName());
48+
assertEquals("Returns a {@link List} of {@link User611}.", rsp.doc().get());
49+
});
50+
}
51+
}

0 commit comments

Comments
 (0)