GSoC 2017 : Vert.x-Web purely functional handlers (with some magic involved)

646 views
Skip to first unread message

Arnaud Estève

unread,
Feb 1, 2017, 8:01:01 AM2/1/17
to vert.x
As Paulo proposed, I'm submitting my proposal of improvements to vertx-web as a GSoC idea.

The idea :

Use any function as a vertx-web handler, then provide an API to compose these functions in a "RouterBuilder API" way.
This should be a totally separated project, keeping vertx-web as is for now and targeting "pure JVM languages" (Java, Kotlin, Groovy, Scala), especially those who have "first-level" functions (and maybe annotations)

What should be achieved :
  1. Any lambda can now be used as a vert.x web handler, automatically resolving lambda parameters
  2. Provide the ability to deal with futures in vert.x-web, if a lambda is returning an AsyncResult, then 
    1. the next handler should only be called once the future is completed
    2. the context should automatically been failed if the asyncresult is failed
  3. Allow users to define custom parameters resolvers by implementing an interface such as Resolvable<RoutingContext, T> or AsyncResolvable<RoutingContext, T>
  4. Create a "builder-like" API on top of vertx-web's Router to deal with such lambdas 

Step 1 : Dealing with lambdas

Inspiration : Rapidoid 

Goal : Any functional interface, with any number of parameters should be an Handler<RoutingContext>

Example of implementation (please understand the goal, more than the implementation that could be totally different)  :

@WebHandler
interface SecurityFilter {

void checkToken(HttpServerRequest req);

}

could be automatically replaced by :
@FunctionalInterface
interface SecurityFilter extends Handler<RoutingContext> {

@Override
default void handle(RoutingContext context) {
checkToken(context.request());
context.next();
}

void checkToken(HttpServerRequest req);

}

As a first (very simple) implementation, we could simply create the following interfaces : 
@FunctionalInterface
interface RequestHandler extends Handler<RoutingContext> {

@Override
default void handle(RoutingContext context) {
handle(context.request());
context.next();
}

void handle(HttpServerRequest req);

}

@FunctionalInterface
interface ResponseFinalizer extends Handler<RoutingContext> {

@Override
default void handle(RoutingContext context) {
finalize(context.response());
}

void finalize(HttpServerResponse resp);

}

@FunctionalInterface
public interface RequestReader<T> extends Handler<RoutingContext> {

@Override
default void handle(RoutingContext context) {
final Map.Entry<String, T> extracted = extract(context.request());
context.put(extracted.getKey(), extracted.getValue());
context.next();
}

Map.Entry<String, T> extract(HttpServerRequest req);

}

@FunctionalInterface
public interface RequestCheck extends Handler<RoutingContext> {

boolean isRequestValid(HttpServerRequest req);

@Override
default void handle(RoutingContext context) {
if (isRequestValid(context.request())) {
context.next();
} else {
context.fail(400);
}
}

}

You get the idea... 

Then add async stuff into the equation, like so :

@FunctionalInterface
public interface AsyncRequestReader<T> extends Handler<RoutingContext> {

@Override
default void handle(RoutingContext context) {
final Future<Map.Entry<String, T>> fut = extract(context.request());
fut.setHandler(res -> {
if (res.failed()) {
context.fail(res.cause());
} else {
final Map.Entry<String, T> extracted = res.result();
context.put(extracted.getKey(), extracted.getValue());
context.next();
}
});
}

Future<Map.Entry<String, T>> extract(HttpServerRequest req);

}

In this step, we should define every kind of handler which is needed in a vertx-web context. (request, response, session, user, async processing, response marshalling, body unmarshalling, ...)

Step 2 : You're a wizard, Harry

Ok, now we should be able to deal with simple lambdas, Request, Response, etc. Even in an async manner.
Now, we'd need some magic, to be able to generate that kind of stuff :
@FunctionalInterface
public interface AutomaticallyGenerated extends Handler<RoutingContext> {

@Override
default void handle(RoutingContext context) {
handle(context.request().getParam("id"), context.session());
context.next();
}

void handle(@Param String id, Session session);

}

I must confess I have absolutely no idea how to achieve this, I think code generation could be involved at some point. But how ? I'm sure rapidoid's implementation is worth the read, the guys did a fantastic job on that matter, great congrats to them.


Step 3 : Custom User-Defined resolution

By some wizard magic (again) let the user implement a very simple interface : 
@FunctionalInterface
public interface RoutingContextBased<T> {

T resolve(RoutingContext ctx, String lambdaParamName, Class<T> lambdaParamType, Annotation[] lambdaParamAnnotations);

}
Unfortunately, I'm not sure this is possible, since lambda parameters are "erased" at runtime.
So maybe a very different approach should be used there, like letting the user define the parameter resolution through a "Processing" API invoked at compile time. This would probably be safer, too.
Feel free to make proposals, please !

I think it's very important to rely on compile-time checks to provide any runtime exception.
For instance, let's imagine the user has not provided any resolver for a specific parameter, he then shouldn't be allowed to use the method as a Handler<RoutingContext> 

Step 4 : Router DSL

Now that we have a set of lambdas that can be mapped onto Handler<RoutingContext>, we are able to use them in the good old vertx-web way : 

router.route(...).handler(...);
router.route(...).handler(...);
...

What could be great, is to add some DSL-sugar to the actual underlying Router and Routes, to be used with any Handler<RoutingContext>, first, then with the specific Handlers we've just created.

Like :

get("/api/v1", checkUserToken.then(validateRequestBody).andThen(saveItToMongo).finishBy(HttpServerResponse::write)));

Let me explain :

  • then : just means we'll be calling the Handlers one after another, they're responsible for calling next() (as we generated before)
  • andThen : means "takes the return of the previous function and pass it to the next function" in a standard functional way
  • finishBy : means take the  return of the preceeding function (maybe async) and the HttpServerReponse and do something with it
And we could add even more examples.

The main idea behind that is to let the users define their handlers as pure functions (or method references) and then provide an API to map that onto a chain of handlers in a "legacy vert.x-web way".

The API has to be properly defined, but I'm sure alot of people will have some nice ideas.


Hope some people will be interested in the idea and have even more fun implementation ideas !
 


Arnaud Estève

unread,
Feb 14, 2017, 5:04:27 AM2/14/17
to vert.x
Some interesting thoughts, related in some way : https://www.playframework.com/documentation/2.5.x/ScalaActionsComposition

Thilina Manamgoda

unread,
Mar 9, 2017, 1:57:24 AM3/9/17
to vert.x
In this step we should implement these handler interfaces(https://github.com/vert-x3/vertx-web/tree/master/vertx-web/src/main/java/io/vertx/ext/web/handler) such that we can add these to the Router object in a Chain kind of a Builder API so the user doesn't have to worry about calling context.next() (simple example) kind of logic. Am I correct ?
Message has been deleted

melnyk....@gmail.com

unread,
Mar 18, 2017, 7:02:56 PM3/18/17
to vert.x
Arnaud, have you considered implementing a conjunction with Spring framework?
The upcoming Spring WebFlux will have functional API as well as well-known annotation mappings.
I created Jira issue some time ago but I was not experienced enough with vertx to implement it.

Anyway I really like the proposal. Hope to work it out!

Arnaud Estève

unread,
May 3, 2017, 4:18:55 AM5/3/17
to vert.x
Hello,

Thanks for the pointer, that's actually a good idea to have some dsl actions in common with web flux or at least some common concepts.

Paulo Lopes

unread,
May 3, 2017, 3:21:22 PM5/3/17
to vert.x
The GSoC student selection will be announced by Google on May 4th 16:00CET, it would be fine to wait a bit more and then we can continue with this discussion :)

Debashis Deb

unread,
Jun 27, 2017, 2:00:18 AM6/27/17
to vert.x
Any Updates on this ? Has the work started ?



On Wednesday, February 1, 2017 at 6:31:01 PM UTC+5:30, Arnaud Estève wrote:

Paulo Lopes

unread,
Jun 27, 2017, 5:44:45 AM6/27/17
to vert.x
Hi,

Arnaud has already prototyped something quite nice: https://github.com/aesteve/vertx-calliope

Feel free to help making it even better if you're interested!
Reply all
Reply to author
Forward
0 new messages