Skip to content

Commit ebf7c9f

Browse files
committed
.map operator for routes fix #349
1 parent 29a46f2 commit ebf7c9f

File tree

7 files changed

+230
-27
lines changed

7 files changed

+230
-27
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.jooby.issues;
2+
3+
import org.jooby.mvc.GET;
4+
import org.jooby.mvc.Path;
5+
import org.jooby.test.ServerFeature;
6+
import org.junit.Test;
7+
8+
public class Issue349 extends ServerFeature {
9+
@Path("/mvc")
10+
public static class Resource {
11+
12+
@GET
13+
@Path("/a")
14+
public Object a() {
15+
return "a";
16+
}
17+
18+
@GET
19+
@Path("/void")
20+
public void ignored() {
21+
}
22+
}
23+
24+
{
25+
get("/a", () -> "a");
26+
27+
with(() -> {
28+
29+
get("/b", () -> "b");
30+
31+
get("/c", req -> "c");
32+
33+
use(Resource.class);
34+
}).map(v -> "//" + v);
35+
36+
get("/d", () -> "d");
37+
38+
use("/g")
39+
.get("/a", () -> "a")
40+
.map(v -> "//" + v);
41+
42+
}
43+
44+
@Test
45+
public void mapper() throws Exception {
46+
request().get("/a")
47+
.expect("a");
48+
49+
request().get("/b")
50+
.expect("//b");
51+
52+
request().get("/c")
53+
.expect("//c");
54+
55+
request().get("/d")
56+
.expect("d");
57+
58+
request().get("/g/a")
59+
.expect("//a");
60+
61+
request().get("/mvc/a")
62+
.expect("//a");
63+
64+
request().get("/mvc/void")
65+
.expect(204);
66+
67+
}
68+
69+
}

jooby/src/main/java/org/jooby/Route.java

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@
214214
*/
215215
public interface Route {
216216

217+
interface Mapper<T> {
218+
Object map(T value) throws Throwable;
219+
}
220+
217221
/**
218222
* Common route properties, like static and global metadata via attributes, path exclusion,
219223
* produces and consumes types.
@@ -341,8 +345,9 @@ default T excludes(final String... excludes) {
341345
* @param excludes A path pattern.
342346
* @return This instance.
343347
*/
344-
public T excludes(final List<String> excludes);
348+
T excludes(final List<String> excludes);
345349

350+
T map(Mapper<?> mapper);
346351
}
347352

348353
class Group implements Props<Group> {
@@ -676,6 +681,14 @@ public Group excludes(final List<String> excludes) {
676681
return this;
677682
}
678683

684+
@Override
685+
public Group map(final Mapper<?> mapper) {
686+
for (Definition definition : routes) {
687+
definition.map(mapper);
688+
}
689+
return this;
690+
}
691+
679692
private void newRoute(final String method, final String pattern,
680693
final Route.Filter filter) {
681694
newRoute(new Route.Definition(method, this.rootPattern + pattern, filter));
@@ -706,65 +719,73 @@ private void newRoute(final Route.Definition route) {
706719
};
707720

708721
/**
709-
* Collection of {@link Route.Definition} useful for registering/setting route options at once.
722+
* Collection of {@link Route.Props} useful for registering/setting route options at once.
710723
*
711724
* @author edgar
712725
* @since 0.5.0
713726
*/
727+
@SuppressWarnings({"unchecked", "rawtypes" })
714728
class Collection implements Props<Collection> {
715729

716730
/** List of definitions. */
717-
private Route.Definition[] routes;
731+
private Route.Props[] routes;
718732

719733
/**
720734
* Creates a new collection of route definitions.
721735
*
722736
* @param definitions Collection of route definitions.
723737
*/
724-
public Collection(final Route.Definition... definitions) {
738+
public Collection(final Route.Props... definitions) {
725739
this.routes = requireNonNull(definitions, "Route definitions are required.");
726740
}
727741

728742
@Override
729743
public Collection name(final String name) {
730-
for (Definition definition : routes) {
744+
for (Props definition : routes) {
731745
definition.name(name);
732746
}
733747
return this;
734748
}
735749

736750
@Override
737751
public Collection consumes(final List<MediaType> types) {
738-
for (Definition definition : routes) {
752+
for (Props definition : routes) {
739753
definition.consumes(types);
740754
}
741755
return this;
742756
}
743757

744758
@Override
745759
public Collection produces(final List<MediaType> types) {
746-
for (Definition definition : routes) {
760+
for (Props definition : routes) {
747761
definition.produces(types);
748762
}
749763
return this;
750764
}
751765

752766
@Override
753767
public Collection attr(final String name, final Object value) {
754-
for (Definition definition : routes) {
768+
for (Props definition : routes) {
755769
definition.attr(name, value);
756770
}
757771
return this;
758772
}
759773

760774
@Override
761775
public Collection excludes(final List<String> excludes) {
762-
for (Definition definition : routes) {
776+
for (Props definition : routes) {
763777
definition.excludes(excludes);
764778
}
765779
return this;
766780
}
767781

782+
@Override
783+
public Collection map(final Mapper<?> mapper) {
784+
for (Props route : routes) {
785+
route.map(mapper);
786+
}
787+
return this;
788+
}
768789
}
769790

770791
/**
@@ -868,6 +889,8 @@ class Definition implements Props<Definition> {
868889

869890
private Map<String, Object> attributes = new HashMap<>();
870891

892+
private Mapper<?> mapper;
893+
871894
/**
872895
* Creates a new route definition.
873896
*
@@ -1206,6 +1229,12 @@ public List<MediaType> produces() {
12061229
return ImmutableList.copyOf(this.produces);
12071230
}
12081231

1232+
@Override
1233+
public Definition map(final Mapper<?> mapper) {
1234+
this.mapper = requireNonNull(mapper, "Mapper is required.");
1235+
return this;
1236+
}
1237+
12091238
@Override
12101239
public String toString() {
12111240
StringBuilder buffer = new StringBuilder();
@@ -1232,7 +1261,7 @@ private Route asRoute(final String method, final RouteMatcher matcher,
12321261
filter = ((AssetProxy) filter).delegate();
12331262
}
12341263
return new RouteImpl(filter, method, matcher.path(), pattern, name, matcher.vars(), consumes,
1235-
produces, attributes);
1264+
produces, attributes, mapper);
12361265
}
12371266

12381267
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.jooby.internal;
2+
3+
import static javaslang.API.Case;
4+
import static javaslang.API.Match;
5+
import static javaslang.Predicates.instanceOf;
6+
7+
import org.jooby.Request;
8+
import org.jooby.Response;
9+
import org.jooby.Route;
10+
import org.jooby.Route.Chain;
11+
import org.jooby.Route.Filter;
12+
import org.jooby.Route.Mapper;
13+
14+
import javaslang.CheckedFunction2;
15+
import javaslang.control.Try;
16+
17+
@SuppressWarnings({"unchecked", "rawtypes" })
18+
public class MappedHandler implements Filter {
19+
20+
private CheckedFunction2<Request, Response, Object> supplier;
21+
private Mapper mapper;
22+
23+
public MappedHandler(final CheckedFunction2<Request, Response, Object> supplier, final Route.Mapper mapper) {
24+
this.supplier = supplier;
25+
this.mapper = mapper;
26+
}
27+
28+
@Override
29+
public void handle(final Request req, final Response rsp, final Chain chain) throws Throwable {
30+
Object input = supplier.apply(req, rsp);
31+
Object output = Try
32+
.of(() -> mapper.map(input))
33+
.recover(x -> Match(x).of(Case(instanceOf(ClassCastException.class), input)))
34+
.get();
35+
rsp.send(output);
36+
chain.next(req, rsp);
37+
}
38+
39+
}

jooby/src/main/java/org/jooby/internal/RouteImpl.java

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
*/
1919
package org.jooby.internal;
2020

21+
import static javaslang.API.$;
22+
import static javaslang.API.Case;
23+
import static javaslang.API.Match;
24+
import static javaslang.Predicates.instanceOf;
25+
2126
import java.util.List;
2227
import java.util.Map;
2328

@@ -27,9 +32,12 @@
2732
import org.jooby.Response;
2833
import org.jooby.Route;
2934
import org.jooby.Status;
35+
import org.jooby.internal.mvc.MvcHandler;
3036

3137
import com.google.common.collect.ImmutableMap;
3238

39+
import javaslang.control.Option;
40+
3341
public class RouteImpl implements Route, Route.Filter {
3442

3543
private static Map<Object, String> NO_VARS = ImmutableMap.of();
@@ -66,7 +74,7 @@ public static RouteImpl notFound(final String method, final String path,
6674
public static RouteImpl fromStatus(final Filter filter, final String method,
6775
final String path, final String name, final List<MediaType> produces) {
6876
return new RouteImpl(filter, method, path, path, name, NO_VARS, MediaType.ALL, produces,
69-
NO_ATTRS) {
77+
NO_ATTRS, null) {
7078
@Override
7179
public boolean apply(final String filter) {
7280
return true;
@@ -77,16 +85,30 @@ public boolean apply(final String filter) {
7785
public RouteImpl(final Filter filter, final String method, final String path,
7886
final String pattern, final String name, final Map<Object, String> vars,
7987
final List<MediaType> consumes, final List<MediaType> produces,
80-
final Map<String, Object> attributes) {
88+
final Map<String, Object> attributes, final Mapper<?> mapper) {
8189
this(filter, method, path, pattern, name, vars, consumes, produces,
82-
ImmutableMap.<String, Object> copyOf(attributes));
90+
ImmutableMap.<String, Object> copyOf(attributes), mapper);
8391
}
8492

8593
public RouteImpl(final Filter filter, final String method, final String path,
8694
final String pattern, final String name, final Map<Object, String> vars,
8795
final List<MediaType> consumes, final List<MediaType> produces,
88-
final ImmutableMap<String, Object> attributes) {
89-
this.filter = filter;
96+
final ImmutableMap<String, Object> attributes, final Mapper<?> mapper) {
97+
this.filter = Option.of(mapper)
98+
.map(m -> Match(filter).of(
99+
Case(instanceOf(Route.OneArgHandler.class),
100+
f -> new MappedHandler((req, rsp) -> f.handle(req), mapper)),
101+
Case(instanceOf(Route.ZeroArgHandler.class),
102+
f -> new MappedHandler((req, rsp) -> f.handle(), mapper)),
103+
Case(instanceOf(MvcHandler.class), f -> {
104+
if (f.method().getReturnType() == void.class) {
105+
// ignore void results
106+
return filter;
107+
}
108+
return new MappedHandler((req, rsp) -> f.invoke(req, rsp), mapper);
109+
}),
110+
Case($(), filter)))
111+
.getOrElse(filter);
90112
this.method = method;
91113
this.path = path;
92114
this.pattern = pattern;

jooby/src/main/java/org/jooby/internal/mvc/MvcHandler.java

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
import com.google.common.base.Throwables;
3333

34-
class MvcHandler implements Route.MethodHandler {
34+
public class MvcHandler implements Route.MethodHandler {
3535

3636
private Method handler;
3737

@@ -50,6 +50,19 @@ public Method method() {
5050
@Override
5151
public void handle(final Request req, final Response rsp) throws Throwable {
5252

53+
Object result = invoke(req, rsp);
54+
55+
Class<?> returnType = handler.getReturnType();
56+
if (returnType == void.class) {
57+
rsp.status(Status.NO_CONTENT);
58+
return;
59+
}
60+
rsp.status(Status.OK);
61+
62+
rsp.send(result);
63+
}
64+
65+
public Object invoke(final Request req, final Response rsp) throws Throwable {
5366
try {
5467
Object target = req.require(handler.getDeclaringClass());
5568

@@ -61,18 +74,11 @@ public void handle(final Request req, final Response rsp) throws Throwable {
6174

6275
final Object result = handler.invoke(target, args);
6376

64-
Class<?> returnType = handler.getReturnType();
65-
if (returnType == void.class) {
66-
rsp.status(Status.NO_CONTENT);
67-
return;
68-
}
69-
rsp.status(Status.OK);
70-
71-
rsp.send(result);
77+
return result;
7278
} catch (InvocationTargetException ex) {
7379
Throwable cause = ex.getCause();
7480
Throwables.propagateIfInstanceOf(cause, Exception.class);
75-
Throwables.propagate(cause);
81+
throw Throwables.propagate(cause);
7682
}
7783
}
7884
}

0 commit comments

Comments
 (0)