When you are building router in Spring you need to declare routes to which an incoming request will be passed. In simple example it looks like following code :
return router {
//for explanation how GET is build take a look at IntroSpring2
GET("/hello") { _ ->
//and below is HandlerFunction
// Handler function is explained in IntroSpring2
//and Reactor api in IntroSpring2 and MonoDemo
ok().body(just("Hello World!"), String::class.java)
}
}
Of course if you would declare everything in one place you would be forced to create very deeply nested code. So to not fall into this trap you have to decouple your components to dedicated classes and fields.
In this part we will see how to compose routing definition.
Route as a Function
FILE : IntroSpring2.kt
Server in a great simplification can be treated like a function (Request) -> Response . And this is technically what a Handler _in Spring is but handler is not a _route! The role of route is to actually match request to Handler so it is a function (Request) -> Handler and because we've already defined a handler then route is
class Request(...)
class Response(...)
typealias Route = (Request) -> (Request) -> Response
Predicates
When a request is sent to a server router has to check if given Handler can actually handle mentioned request which brings another function (Request) -> Boolean. Such function is very often known as a Predicate.
interface RoutingPredicate {
fun test(candidate: String): Boolean
}
Which looks enough for simple conditions but what if we want to check path and headers?
Then we may want to be able to somehow compose those predicates like in the example below :
buildRoute2(GET("/admin") or GET("/hello"))
This part is more about OOP than FP because we just need to create dedicated implementations of RoutePredicates
As a starter point we can return Single Predicate
fun GET(pattern: String) = SinglePredicate(pattern)
And then in single predicate we can have methods like or, and etc. And you can always move those methods to Predicate Interface.
Routes DSL
After Part 1 of this introduction you should know basics of DSLs in kotlin and how implicit receiver works. Now let's move one step forward and mix 3rd party types to our DSL.
router2 {
//Extract "/index".or("/") outside routes function and see what happens
get("/index".or("/")){
Response("response for $it")
}
}
Have you spotted already that there is actually method called or which actually return our Predicate - how it can be!!!
This is special Kotlin mechanism called extension method
fun String.or(other: String): RoutingPredicate = ...
But something more is happening here - this extension function is actually a method declared inside dsl so it will only work in context of our instance so only in our DSL.
Modularisation
- You can declare and compose Predicate in one place
- You can declare Handler independently
- And then connect in
fun buildRoute2(p: IntroSpring2.RoutingPredicate, handler: ServerFunction2): (IntroSpring2.Request) -> ServerFunction2 = {
if (p.test(it.path)) handler else { r -> IntroSpring2.Response("WRONG REQUEST : $r") }
}
And everyone should be happy.
Exercises
FILE: IntroSpring2Exercises
Here you need to build DSL where articles will be indexed by tags
val result=simpleNews{
article("world".tag, "world news",String::capitalize)
...
}
Notice that there will be an extension method to be implemented. method tag will use also specific Kotlin syntax where you can declare property and attach getter to it :
val String.tag:Tag
get() = TODO("property getter for a tag")
In the second part you will have to implement "tags type family" where multiple tags could be combined
sealed class Tag2{
abstract fun matches(other : String) : Boolean
companion object {
operator fun invoke(tag:String) = SingleTag(tag)
}
}
class SingleTag(private val tag:String) : Tag2() {
override fun matches(other: String): Boolean = TODO("check if tag matches")
}
class MultiTag(private val tags:Set<String>) : Tag2(){
fun append(other:String) : MultiTag = TODO("append")
override fun matches(other: String): Boolean = TODO("check if tag matches any")
}
Which gives :
multiNews {
...
article("world" or "fun" or "sport","funny world cup")
...
}