Typed Actions in Play
This is just something I was playing around with when discussing how to build
proper tooling for our teams at work. Play has the idea of an Action
which
is basically a function that takes a Request
object and returns a Result
object. Each of those can be parameterized for the content type being transmitted.
However, when you’re working in an API, you typically work with some exchange format (e.g. JSON) and then serialize and de-serialize to/from your internal data model. If you are using Scala, then possibly some case classes. Rather than having to worry about the exchange format, developers should really just worry about these data-models when writing their code.
So, just the result of me playing with the idea for a few minutes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
object TypedAction {
def TypedAction[A,B](f: A => B)(implicit reader: Reads[A], writer: Writes[B]) : Action[String] =
Action.async(BodyParsers.parse.tolerantText) { request =>
val json = Json.parse(request.body)
Future.successful {
val badRequest: Result = Results.BadRequest("Could not parse input")
json.asOpt[A](reader).map(f).map(Json.toJson(_)(writer)).map(_.toString()).map(Results.Ok(_)).getOrElse(badRequest)
} : Future[Result]
}
def TypedAction[B](f: () => B)(implicit writer: Writes[B]): Action[String] =
Action.async(BodyParsers.parse.tolerantText) { request =>
Future.successful {
Results.Ok(Json.toJson(f())(writer).toString)
}
}
}
And then using the model
1
2
3
4
5
6
case class UserModel(id: Int, name: String)
object UserModel {
implicit val reads = Json.reads[UserModel]
implicit val writes = Json.writes[UserModel]
}
We can create a controller that looks like
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
object UserController extends Controller {
var userStore = Map(
1 -> UserModel(1, "Joe Schmoe"),
2 -> UserModel(2, "Susy Jane"),
3 -> UserModel(3, "Jenny Jackson")
)
def getUser(id: Int) = TypedAction[UserModel] { () =>
userStore(id)
}
def storeUser = TypedAction[UserModel, UserModel] { userIn =>
val nextKey = userStore.keys.max + 1
userStore += nextKey -> userIn
userIn.copy(id = nextKey)
}
}
We could also create different input and output models within the same
controller method. In addition, if you really wanted to do something like
this you would need to handle errors much better, both from the user code
and from the parsing code. I’m also not really doing anything with the
Action.async
portion above, so that could be improved as well.
But yeah, just some (light) food for thought.