A little bit complex proposition here, though it could simplify life for some people writing services on Javalin. I will name it TypedHandler here.
After a little bit of experimenting with request and response transformers I've noticed that they can be used to allow handlers like (T) -> R (Kotlin lambda syntax) or R method (T param). This will allow almost painless parse of request and response and much cleaner controllers.
Let's imagine we have a controller which takes an JSON body from POST request, does something with it and returns result in JSON. (JSON can be replaced with any kind of serialization)
This controller will be a function from PostBody instance to PostResponse instance. (PostResponse doSomething(PostBody body))
Now to translate Context to PostBody and back to PostResponse we have to define a handler or use custom wrapper. However, if we can define a functional interface like this:
interface TypedHandler<Req, Res> {
Res handle(Req request);
}
and the Javalin instance method like this:
public <Req, Res> Javalin get(String path, Transformer<Req, Res> transformer, TypedHandler<Req, Res> handler) {
get(path, ctx -> transformer.response.tranform(handler.handle(tranformer.request.transform(ctx)), ctx);
}
then the controller method doSomething can be utilized straightaway (if transformer is predefined):
javalin.get("some-path", transformer, controller::doSomething)
The transformer itself has the following interface:
interface RequestTransformer<Request> {
fun transform(ctx: Context): Request
}
interface ResponseTransformer<Response> {
fun transform(value: Response, ctx: Context)
}
interface Transformer<Request, Response> {
val request: RequestTransformer<Request>
val response: ResponseTransformer<Response>
}
class JsonRequestTransformer<T>(private val mapper: ObjectMapper, val cls: Class<T>) : RequestTransformer<T> {
override fun transform(ctx: Context): T = mapper.readValue(ctx.request().inputStream, cls)
}
class JsonResponseTransformer<T>(private val mapper: ObjectMapper): ResponseTransformer<T> {
override fun transform(value: T, ctx: Context) {
mapper.writeValue(ctx.response().outputStream, value)
}
}
class JsonTransformer<In, Out>(mapper: ObjectMapper, cls: Class<In>): Transformer<In, Out> {
companion object {
inline fun <reified T, reified R> get(mapper: ObjectMapper) = JsonTransformer<T, R>(mapper, T::class.java)
}
override val request = JsonRequestTransformer(mapper, cls)
override val response = JsonResponseTransformer<Out>(mapper)
}
Example with custom Jackson features (view class):
data class View<T>(val view: Class<in T>, val value: T)
class JsonViewResponseTransformer<T>(private val mapper: ObjectMapper): ResponseTransformer<View<T>> {
override fun transform(value: View<T>, ctx: Context) {
mapper.writerWithView(value.view)
.writeValue(ctx.response().outputStream, value.value)
}
}
There could be some inaccurate code definitions here, as I was writing that from memory (tested some days ago).
EDIT:
After some thoughts, I noticed that transformers can be used as an entity to produce the Handler:
interface TypedRequestHandler<Req> {
void handle(Req request)
}
interface RequestTransformer<Req> {
Req transform(Context context);
default Handler apply(TypedRequestHandler<Req> handler) {
return ctx -> handler.handle(transform(ctx))
}
}
// The same for response
interface TypedResponseHandler<Res> {
Res handle()
}
interface ResponseTransformer<Res> {
void transform(value: Res, Context context);
default Handler apply(TypedResponseHandler<Res> handler) {
return ctx -> transform(handler.handle(), ctx);
}
}
// For both TypedHandler is used (described above)
interface Transformer<Req, Res> {
RequestTranformer<Req> request();
ResponseTransformer<Res> response();
default Handler apply(TypedHandler<Req, Res> handler) {
return ctx -> response.transform(handler.handle(request.transform(ctx)), ctx);
}
}
Usage:
get("path", transformer.apply(controller::doSomething));
This will allow almost painless parse of request and response and much cleaner controllers.
It would be helpful if you could add a "side-by-side" example of the current approach vs your proposed approach. I'll try to read through it and understand how it works tomorrow.
I've been trying to understand what this is supposed to do for almost an hour, I need you to ELI5 (showing the controller code should be enough).
Yeah, I was too abstracted when I was explaining that, sorry 馃槃
So, imagine you have a controller which accepts PostBody class and answers with PostResponse.
data class PostBody(val id: String, val value: String)
data class PostResponse(val result: String)
class Controller {
fun action(body: PostBody): PostResponse = PostResponse(body.id+body.value)
}
Javalin.start()
.get("controller") { ctx ->
val postBody: PostBody = ctx.bodyAsClass(PostClass::class.java)
val postResponse = controller.action(postBody)
ctx.json(postResponse)
}
Effectively, it is doing nothing, just concatenating strings and wrapping them.
My idea is to introduce transformer (code in OP).
val transformer = JsonTranformer.get<PostBody, PostResponse>()
Javalin.start()
.post("controller", transformer, controller::action)
Here the post method is defined as follows:
public Javalin post<Req, Res>(path: String, transformer: Transformer<Req, Res>, handler: TypedHandler<Req, Res>) {
return post(path, ctx -> transformer.response.transform(
handler.handle(transformer.request.transform(ctx)),
ctx
))
}
So the suggestion is to introduce the Transformer<Req, Res> class (and its RequestTransformer<Req> and ResponseTransformer<Res> parts) and integrate them to the route definition.
So the suggestion is to introduce the Transformer
class (and its RequestTransformer and ResponseTransformer parts) and integrate them to the route definition.
I understand that part, what I'm having trouble with is understanding what problem introducing these concepts will solve for Javalin users. I'm feeling very unimaginative now, but I can't really see a clear use case. In your first post you wrote This will allow almost painless parse of request and response and much cleaner controllers..
Can you show this in action by creating two non-trivial example-controllers, one with a transformer and one without?
Hm, I don't think it illustrates the concept from any perspective. Controller usually contains some business logic, which is not the topic.
However, I could have been a little bit excited about the idea writing almost painless stuff (sorry about that). Let's say that it is standardized, customizable and integrated way of marshalling/unmarshalling which can possibly change the way plugins are defined right now.
For me, shaping some ways for writing the logic without caring much of transformation of entities to bytes is always an advantage for the REST framework. This proposal is strictly about it.
Hm, I don't think it illustrates the concept from any perspective. Controller usually contains some business logic, which is not the topic.
This feature doesn't exist in a vacuum, the premise is that it would allow people to write controllers differently. To me it looks like it would abstract away two lines (serialize/deserialize) from a controller, but at the added cost of creating a transformer and understanding these concepts. You have to argue how this will be beneficial for Javalin users. As you might have noticed, I have tried to put as few abstractions as possible in Javalin (I already think it has too many).
I'm not trying to sound negative (that's just a gift I have), I really want to understand if this feature is worth the added complexity it brings. It looks neat to have one-liner like
fun action(body: PostBody): PostResponse = PostResponse(body.id + body.value)
but in a real-world scenario where most of the complexity comes from business logic, is it necessary?
To me it looks like it would abstract away two lines (serialize/deserialize) from a controller, but at the added cost of creating a transformer and understanding these concepts.
Sometimes it's not two lines :) Though you're right, it is doing only that job. For me it was a much cleaner controller when I didn't care about Context interactions in controller anymore and just was writing the transformation separately.
As you might have noticed, I have tried to put as few abstractions as possible in Javalin (I already think it has too many).
In that case this feature is not needed here, I just wanted to add an instrument to allow entity manipulation straightaway, without any complications with Context interactions.
In that case this feature is not needed here, I just wanted to add an instrument to allow entity manipulation straightaway, without any complications with Context interactions.
Let's not close it, I think the feature sounds interesting. What worries me the most is intimidating and/or confusing users. It would be helpful if you could show how this helps you structure your apps by creating a bigger example, preferably with some fake business logic.
Ok, example of usage from my last quick project.
Assuming all the boilerplate from above and Kotlin. Implementing fairly simple functionality: updating and retrieving settings (id and values) with the database.
Database interface:
interface DB {
fun update(id: String, values: HashMap<String, String>): HashMap<String, String>
fun get(id: String): HashMap<String, String>
}
Models (and transformers):
data class SettingsUpdate(val id: String, val values: HashMap<String, String>) {
companion object {
fun transformer(mapper: ObjectMapper) = object : RequestTransformer<SettingsUpdate> {
override fun transform(ctx: Context): SettingsUpdate {
val values = mapper.readValue<HashMap<String, String>>(ctx.request().inputStream, object : TypeReference<HashMap<String, String>>() {})
val id = ctx.param("id") ?: throw HaltException(404, "Not found")
return SettingsUpdate(id, values)
}
}
}
}
data class SettingsResponse(val id: String, val values: HashMap<String, String>) {
companion object {
fun transformer(mapper: ObjectMapper) = JsonResponseTransformer<SettingsResponse>(mapper)
}
}
class IdRequestTransformer : RequestTransformer<String> {
override fun transform(ctx: Context) = ctx.param("id") ?: throw HaltException(404, "Not found")
}
Utility functions providing proposed syntax and some useful shorthands:
fun<Req, Res> transform(req: RequestTransformer<Req>, res: ResponseTransformer<Res>) = object : Transformer<Req, Res> {
override val requestTransformer = req
override val responseTransformer = res
}
fun <T, R> get(path: String, transform: Transformer<T, R>, handler: (T) -> R) = get(path, {
transform.responseTransformer.transform(handler.invoke(transform.requestTransformer.transform(it)), it)
})
fun <T, R> post(transform: Transformer<T, R>, handler: (T) -> R) = post({
transform.responseTransformer.transform(handler.invoke(transform.requestTransformer.transform(it)), it)
})
The controller itself:
class SettingsController(private val db: DB, private val mapper: ObjectMapper) {
private fun get(id: String) = db.get(id).let { SettingsResponse(id, it) }
private fun update(update: SettingsUpdate) = db.update(update.id, update.values).let { SettingsResponse(update.id, it) }
fun routes() {
val id = IdRequestTransformer()
val update = SettingsUpdate.transformer(mapper)
val response = SettingsResponse.transformer(mapper)
path("settings") {
get(":id", transform(id, response), this::get)
post(transform(update, response), this::update)
}
}
}
Running Javalin:
fun run(controller: SettingsController) {
Javalin.start(8080)
.routes {
controller.routes()
}
}
I hope it is illustrative enough. I could miss some pieces, though this example should work.
@tipsy Created a repo illustrating this approach. What do you think of a separate library for that?
Created a repo illustrating this approach
Thanks @ShikaSD, I've been playing around with your repo for some hours, trying to "get it", but I'm still having trouble seeing the benefit. I have asked people who are smarter than me, and they don't get it either. You have this in SimpleExample:
get("static/", plainText(), () -> "Always the same");
In plain Javalin this would be:
get("static/", ctx -> ctx.result("Always the same").status(200));
But it involves
fun <Req, Res> get(path: String, transformer: Transformer<Req, Res>, handler: TypedHandler<Req, Res>) = get(path, transformer.toHandler(handler))
// and
class PlainText : ResponseTransformer<String> {
companion object {
@JvmStatic
fun plainText() = PlainText()
}
override fun invoke(ctx: Context, value: String) {
ctx.result(value)
ctx.status(200)
}
}
// and
public interface ResponseTransformer<Response> {
void invoke(@NotNull Context context, Response value);
default Handler toHandler(TypedResponseHandler<Response> handler) {
return ctx -> invoke(ctx, handler.handle());
}
}
I realize plainText() is probably the transformer which gives the least benefit, which is why I picked it. The point is that something that used to be very simple is now fairly complicated. Instead of having just Handler, Context, and explicit control-flow, you now have six new concepts (three handlers and three transformers). The transformers still operate on Handler and Context, so you also need to be familiar with these concepts.
Another thought, you could create all of these transformers "outside" of Javalin really, they don't have to be part of the route declaration?
What do you think of a separate library for that?
Understanding the purpose of this concept has been too hard for me and everyone I've asked, so it can't be made part of the core at least. I'm not totally opposed to the idea of having it as a separate library, but you will need to get some support for it first. If it becomes an official module I will have to spend time on maintaining and documenting it, and I can't do that unless there is significant interest in it.
Thank you for spending some time on that :)
I've been playing around with your repo for some hours, trying to "get it", but I'm still having trouble seeing the benefit.
I certainly have the feeling that I am overcomplicating things, so maybe I need to review that once more.
My main concern which led me to creating this concept was lack of standard ways of handling serialization in my handlers. I had like twenty of them all handling some serialization (JSON and Proto mostly) and did like seven different wrappers around that.
class Controller(private val db: DB) {
fun getUser(ctx: Context) {
val id = ctx.param("id")
val user = db.getUser(id)
ctx.result(user.toJson()).status(200)
}
}
After converting this entities back and forth I have realized that I am basically duplicating the serializing/unserializing code everywhere (with occasional differences), so I started changing the handling of parameters.
fun <reified T: Any> handler(f: (String) -> T): Handler = Handler { ctx -> f(ctx.param("id")).toJson() }
handler(db::getUser)
handler(db::getMessage)
Later I had to separate them to request/response, as they started to repeat again. Given that I was writing some endpoints in the service in the same way, it quickly went to something similar to the concept I am trying to introduce. Now I see that this unnecessary complication with classes as an attempt to unify many ways of doing the same thing could be not suitable for everyone.
Another thought, you could create all of these transformers "outside" of Javalin really, they don't have to be part of the route declaration?
Yeah, I can take a look on that.
Current implementation allows to do transformer.toHandler(th: TypedHandler<T, R>). However, these transformers are totally not needed outside the scope, as they are just a mapping of the entities.