scala-playframework-effects
Scala playframework Action builders on batteries.
Versions
Play 2.8.x
Play 2.7.x
Play 2.6.x
Underlying needs
There were three major underlying reasons which led to the creation of this library -
-
Error handling
Often working on projects that use playframework, I observed that teams used failed Future
to model business errors.
The recipe is simple -
- Create an exception case class for a business error
- Wrap the exception in a failed
Future
and return it from the service which reaches the action block and is returned from there. - Handle that exception in the playframework's global error handler to convert it into a
Result
.
That's it. This seems to be such an easy way of error handling that many of us might have done it this way at least once in the past, if not with the playframework but with some other library.
There are few downsides to this approach though
-
The business error encoding is lost in
Future
's syntax. Looking at aFuture
returned from a service, we can't tell which business error it could be carrying. It is also not obvious how to handle a business error in a service thrown by another collaborator service. We will have to peek into the code of collaborator service code to find out which exception to handle. -
The controller action is not complete in its own. The action block does not return a
Result
but instead a failedFuture
in case of business error. The mapping of failedFuture
toResult
is done in a global error handler. This also makes unit testing of the controller action incomplete. We can only test that the action will return a failedFuture
when a business error occurs. We will have to write a separate test for global error handler which can't give any confidence that the business error will be translated into correctResult
as the action method and the global error handler are disconnected. To get full confidence, we will have to write an expensive integration test.
A better approach could be to return Future[Either[E, A]]
from the services which force the action block to handle
the error and convert the Either
into Result
for both success and error.
If we follow this approach of returning Future[Either[E, A]]
from the services, we will realise that our action
methods will become heavy as they will now have to handle the Either
for both success and error.
This seems to be a problem of its own. Either we will handle the Either
in the controller or create some helper method
outside the controller to handle the Either
.
Handling it in some helper method controller is not that bad, but we still need to call that method in the action method.
This library tries to solve the problem of handling the Either
in a neater approach.
-
Sensible defaults
Often while writing action methods for RESTful services, we have to convert a case class into an Ok
result returning a
JSON response or we have to convert a Future.successful(())
into a NoContent
response.
This library provides some sensible defaults to allow easier conversion to a Result
-
Bring your own effect
We can create services where the effect is abstracted, but the controller's action methods are always tied to the only Future
effect. Whatever
the effect we use for our services,
e.g. IO,
Task,
ZIO, we still have to convert it into a Future
.
This library provides a way to create action methods which are abstracted from effect. This allows writing controllers using the tagless-final approach as well.
Usage
Add scala-playframework-effects
as SBT dependency
libraryDependencies += "com.github.anshulbajpai" %% "scala-playframework-effects" % "(version)"
The minimum playframework version we support is 2.6.x. Our versioning strategy is aligned with playframework's versions up to their minor version, e.g. - 2.7.1.0
library version will work with play 2.7.x. The 1.0
sub-version
represents the version of this library for play 2.7.x.
Actions can be created using Action.asyncF
and Action.sync
methods after importing com.github.anshulbajpai.scala_play_effect.ActionBuilderOps._
.
Assuming the following code is present in the scope.
import com.github.anshulbajpai.scala_play_effect.ActionBuilderOps._
import cats.~>
implicit val request = FakeRequest().withJsonBody(Json.obj("message" -> "some message"))
case class ActionMessage(message: String)
implicit val messageWrites: Writes[ActionMessage] = Json.writes[ActionMessage]
case class ActionError(error: String)
implicit val errorWrites: Writes[ActionError] = Json.writes[ActionError]
implicit val actionErrorToResult: ToResult[ActionError] = new ToResult[ActionError] {
override def toResult(error: ActionError): Result = Results.BadRequest(Json.toJson(error))
}
implicit val ioToFuture: IO ~> Future = λ[IO ~> Future](_.unsafeToFuture())
/*
The above `ioToFuture` implicit can also be written as below. We have used kind-projector compiler plugin for brevity above.
implicit val ioToFuture: FunctionK[IO, Future] = new FunctionK[IO, Future] {
override def apply[A](fa: IO[A]): Future[A] = fa.unsafeToFuture()
}
*/
asyncF
The asyncF
method helps create Actions from blocks which can return a value wrapped in an effect F[_]
.
It also comes with some sensible defaults to map the block's return type to a proper Result
. For example an action block
returning a Future[Unit] will be converted into an HTTP NoContent status code.
- Returning
Future[Unit]
val action = Action.asyncF { _ =>
Future.unit
}
// action: play.api.mvc.Action[play.api.mvc.AnyContent] = Action(parser=BodyParser((no name)))
val result = call(action, request) // call is imported from play.api.test.Helpers
// result: Future[Result] = Future(Success(Result(204, TreeMap()))) // call is imported from play.api.test.Helpers
status(result)
// res0: Int = 204
- Returning
Future[A]
val action = Action.asyncF { _ =>
Future.successful(ActionMessage("some message"))
}
// action: play.api.mvc.Action[play.api.mvc.AnyContent] = Action(parser=BodyParser((no name)))
val result = call(action, request)
// result: Future[Result] = Future(Success(Result(200, TreeMap())))
status(result)
// res1: Int = 200
contentAsJson(result)
// res2: play.api.libs.json.JsValue = JsObject(
// Map("message" -> JsString("some message"))
// )
- Returning
Future[Either[A, B]]
val successAction = Action.asyncF { _ =>
Future.successful(ActionMessage("some message").asRight[ActionError])
}
// successAction: play.api.mvc.Action[play.api.mvc.AnyContent] = Action(parser=BodyParser((no name)))
val successActionResult = call(successAction, request)
// successActionResult: Future[Result] = Future(Success(Result(200, TreeMap())))
status(successActionResult)
// res3: Int = 200
contentAsJson(successActionResult)
// res4: play.api.libs.json.JsValue = JsObject(
// Map("message" -> JsString("some message"))
// )
val errorAction = Action.asyncF { _ =>
Future.successful(ActionError("some error").asLeft[ActionMessage])
}
// errorAction: play.api.mvc.Action[play.api.mvc.AnyContent] = Action(parser=BodyParser((no name)))
val errorActionResult = call(errorAction, request)
// errorActionResult: Future[Result] = Future(Success(Result(400, TreeMap())))
status(errorActionResult)
// res5: Int = 400
contentAsJson(errorActionResult)
// res6: play.api.libs.json.JsValue = JsObject(
// Map("error" -> JsString("some error"))
// )
- Using the request body
val successAction = Action(json).asyncF { req =>
Future.successful(ActionMessage((req.body \ "message").as[String]).asRight[ActionError])
}
// successAction: play.api.mvc.Action[play.api.libs.json.JsValue] = Action(parser=BodyParser(conditional, wrapping=BodyParser(json, maxLength=102400)))
val successActionResult = call(successAction, request)
// successActionResult: Future[Result] = Future(Success(Result(200, TreeMap())))
status(successActionResult)
// res7: Int = 200
contentAsJson(successActionResult)
// res8: play.api.libs.json.JsValue = JsObject(
// Map("message" -> JsString("some message"))
// )
- Returning
IO[Unit]
val action = Action.asyncF { _ =>
IO.unit
}
// action: play.api.mvc.Action[play.api.mvc.AnyContent] = Action(parser=BodyParser((no name)))
val result = call(action, request)
// result: Future[Result] = Future(Success(Result(204, TreeMap())))
status(result)
// res9: Int = 204
- Returning
Result
as it is
val action = Action.asyncF { _ =>
IO.pure(Results.NoContent)
}
// action: play.api.mvc.Action[play.api.mvc.AnyContent] = Action(parser=BodyParser((no name)))
val result = call(action, request)
// result: Future[Result] = Future(Success(Result(204, TreeMap())))
status(result)
// res10: Int = 204
sync
The sync
method is similar to asyncF
in all aspects except that it doesn't need an effect to be returned in the return type of action block.
It also comes with the same sensible defaults as asyncF
val action = Action.sync { _ =>
ActionMessage("some message")
}
// action: play.api.mvc.Action[play.api.mvc.AnyContent] = Action(parser=BodyParser((no name)))
val result = call(action, request)
// result: Future[Result] = Future(Success(Result(200, TreeMap())))
status(result)
// res11: Int = 200
contentAsJson(result)
// res12: play.api.libs.json.JsValue = JsObject(
// Map("message" -> JsString("some message"))
// )
These examples and more cases are covered in AsyncActionSpecs.scala and SyncActionSpecs.scala
How does it work
We take help of cats natural transformation data type FunctionK and our own
ToResult
typeclass to do all under-the-hood transformation.
trait ToResult[S] {
def toResult(s: S): Result
}
If your code has a FunctionK[F, Future]
instance available for an effect F[_]
(needs to be a Functor
too) and a ToResult
instance for a type S
,
then you can create an action like this
Action.asyncF { req =>
// return F[S]
}
Using tagless-final
Using this library, it is very easy to create tagless-final components right up to the controllers. There is an example play application included in the repository under the exampleApp directory which shows how to use tagless-final end-to-end using this library and macwire. The HelloController.scala has examples on how to use this library to write actions method effortlessly without thinking of HTTP status code and serialization most of the time.
mdoc
This README is generated via mdoc. This is the source file