spring-fu
spring-fu copied to clipboard
Router DSL with matchers
Hello and thanks for bringing such awesome new support to Spring.
The routing DSL as it is now is really cool, but comes with a tradeoff against traditional Spring MVC / JAX-RS / ... : the handler is a pure Request -> Response function (which is what HTTP is all about, true) but we lose the automatic request param / path variable / body checking.
One solution to that, would be to enhance the actual router DSL with a Route type, having many different methods. As an example:
GET("/api/todos/{id}").pathVariable<Int>("id").contentType("text/plain") {
ok("you asked for todo id: $it")
}
it is obviously of type Int, that's the whole point.
We can imagine a lot of stuff like:
POST("/api/todos")
.withValidBody<Todo>()
.orElseSend {
badRequest("todo doesn't seem valid ${it.validationErrors().join(",")}"
}
.handler {
repository.save(it)
noContent()
}
(not sure about the bean validation JSR-303 exact method names, but you see the point)
Or even integrate OpenAPI stuff like documentation for query parameters, models / examples for request/response body, etc.
post("/todos")
.body<Todo>(todoModelDocWithExample)
// ...
What does it bring ?
- A bit more type safety: if you declared a route that needs an integer pathVariable, just declare it at the route level, and work with an int afterwards
- A bit more "DRY": just declare the way you need to handle errors (HTTP 400: the bean you sent is invalid because..." instead of calling
checkBodyIsAValidTodowithin every handler - Declarative / Centralization: imho, the router is the first place I'll look for HTTP endpoints definitions, routing files like in RoR, Grails, ... are nice as "The HTTP entrypoint". I like the idea of having a router defining every endpoint instead of opening every controller in my IDE: spring-fu does that very well, and it'd be even nicer to have a bit more route definitions here.
Some (maybe) interesting pointers:
- Akka's magnet pattern. Akka's Scala DSL is a real gem (imo). It does this declarative + type-safety approach very very well. The only thing I find kinda meh, is the nesting approach
get(...) { withBody { withParam { } } }where a fluent approach looks better to my eyes. Their idea of having the "main handler" body accepting aTupleof every matched param (request, path, body, headers, etc.) is really, really cool. - A very rough prototype I did a while ago using Vert.x and Scala. Mostly abandoned because the implementation is really not satisfying, and would be hard to maintain in the long run, even though the ideas expressed in this issue are basically implemented. Hoping for the huge improvements of Scala 3 (type classes, extension methods, abstracting over tuples/functions arity) to rely less on implicits, and have a really nice implem instead. At least the example in README maybe will give you ideas, hints, or whatever, just feel free.
On the second pointer, here's what I think could be prototyped in a Kotlin Spring-Fu implementation. The key is dealing with tuples.
get("/todos/{id}").pathVariable<Int>("id").queryParam<LocalDate>("from")
would accept a handler of type: (Pair<Int, LocalDate>) -> ServerResponse
The bad thing about my implementation is that you can't (atm) abstract over Tuple arity, so you have (in Scala) to use Tuple1, Tuple2, Tuple3, ... and define extensions (implicits) to trivially define "a Tuple1[Int] joined to a String produces a Tuple2[Int, String]. Not sure how this would be solved in Kotlin. I know there are pairs but... Not much more.
There is also more advanced stuff I don't really know if possible in Kotlin.
For instance, checks are made at compile-time (through implicits) in order to check if returning a POJO from a route actually makes sense. Let me detail:
Say you define a route, not returning ServerResponse but List[Todo].
You've defined it this way:
get("/todos/").produces(`application/json`) {
List(Todo("to do"))
}
It's going to use Scala's implicit feature to check, at compile time, if there's a Marshaller[Json, Todo] in scope. You could define your own marshaller implicitly, or use a generic one, saying implicit val jacksonMarshaller: Marshaller[Json, Any] = ???.
Not sure it'd be needed in Spring-Fu, or even possible with Kotlin.
Quite hard to cover the whole topic at once, feel free to ask any question, I hope this can inspire you folks to build such a (I think) useful routing DSL, so that people can, in a functional way, benefit from the good old @PathVariable and all, and maybe build useful stuff like spring-fox for OpenAPI doc generation, at compile-time.
Thank you :)
@aesteve Thanks a lot for this very interesting and detailed feature request.
@poutsma I would be interested by your thoughts about that since your are both the functional routing creator and a Scala expert.
Something like the mentioned snippet could also improve authorization with Spring security e.g. like this:
POST("/api/todos")
.withValidBody<Todo>()
.withRole("ADMIN") // could internally adjust ServerHttpSecurity rules
...
One of my favorite feature in the RouterDSL is the nest function to factorize api routing.
I think the best (not possible) syntax would be something like that :
router {
nestPath<Int>("/{id}") { id ->
GET("/user") {
// Here id is accessible
}
}
}
but a simplest alternative could be :
router {
nestPath<String>("/{id}") { nestPath ->
GET("/user") {
val id = it.path(nestPath)
ok().syncBody(id)
}
}
}
//
class NestPath<T>(val path: String)
fun <T> RouterFunctionDsl.nestPath(path: String, f: RouterFunctionDsl.(NestPath<T>) -> Unit) =
this.path(path).nest {
f(NestPath("id")) // TODO replace /{..} path param
}
fun <T> ServerRequest.path(np: NestPath<T>): T = this.pathVariable(np.path) as T // TODO betting cast