Refining the play scala actions
Play scala is a web architecture that lets a scala developer to write REST APIs quite easily. One sees a lot of tutorials and samples that implement the actions in somewhat this form
1
2
3
4
5
6
7
8
9
10
11
12
13
def normal: Action[AnyContent] = Action.async { implicit request: Request[AnyContent] =>
Location.locationForm.bindFromRequest().fold(
errors => Future.successful(BadRequest(errors.errorsAsJson)),
success => for {
either <- hogwartsService.fetchHorcrux(success)
} yield {
either match {
case Left(e: OpsFailed) => InternalServerError(Json.toJson(e))
case Right(horcrux) => Ok(Json.toJson(horcrux))
}
}
)
}
I feel that this sample contains a lot of noise. If we look closely, the crux of the work being done here is revolving aroud the following lines
1
2
hogwartsService.fetchHorcrux(success)
Ok(Json.toJson(horcrux))
where we interacting with the domain service layer and then sending back the result from there. The rest of the code is mostly about a common pattern which binds the json body with the model object and proceeds further or fails depending upon whether request is valid or not.
Why not take away all this common work and refine the action definition to its bare necessities. For this, of course, we put this boilerplate code in a common function. One approach we could follow is to create a generic action that can work with different forms
1
2
3
4
5
6
7
8
9
def asyncFormAction[T](form: Form[T])(api: (T, CommonHeaders.type) => Future[Result]): Action[AnyContent] = async { implicit request: Request[AnyContent] =>
form.bindFromRequest().fold({
(errors: Form[T]) => Future.successful(handleErrors(errors))
}, {
(requestObj: T) => api(requestObj, CommonHeaders)
})
}
private def handleErrors[T](errors: Form[T]): Result = BadRequest(errors.errorsAsJson)
and a generic result generator that can take scala objects which are json writable
1
2
3
4
5
6
def toResult[T](either: Either[OpsFailed, T])(onSuccess: T => Result) = either match {
case Left(opsFailed) => InternalServerError(toJson(opsFailed))
case Right(t) => onSuccess(t)
}
def toJsonResult[T](either: Either[OpsFailed, T])(implicit writes: Writes[T]) = toResult(either)(x => Ok(toJson(x)))
With the above pieces in place, our action defintion in the controller is shortened to this
1
2
3
4
def refined: Action[AnyContent] = asyncFormAction(Location.locationForm)((obj: Location, ch: CommonHeaders.type) => for {
either <- hogwartsService.fetchHorcrux(obj)
} yield toJsonResult(either)
)
which only is about the crux of the work we are doing in the controller api. Another advantage of extracting out the form validation and initial request handling to a common function is that one can always also add the logic to extract some common headers related to domain. This is especially useful when one is using JWT or similiar technolgies for validations of the requests.
It is also possible to compose the actions in case we need to do a specific request handling like extracting a special header for one-off api.
Feel free to explore the full code in the github repository and the controller HarryPotter.scala