Creating a generic reusable function with implicit and types in Scala

368 views Asked by At

I have a block of code that gets replicated literally all over the place. This block of code (about 10 lines) handles inbound actions, validates them, handles the JSON serialization, and calls an internal function, preparing a result.

I know it's possible to reduce this to a one-liner call to a common helper function, but because of the implicits, the types, and my not-yet-great knowledge of Scala syntax I keep running into problems.

The code (two separate examples, to demonstrate how they differ):

val authenticate = GPAuthenticatedAction(parse.json) { implicit request =>
    request.body.validate[AuthenticationRequest] match {
        case JsSuccess(request, _) => {
            val (status, response) = performAuthentication(request)
            status(Json.toJson(response.asInstanceOf[AuthenticationResponse]))
        }

        case e: JsError => NotAcceptable(Json.toJson(GPError.MalformedJSON.value(e.toString + " REQUEST: " + request.body.toString)))
    }
}

val register = GPAuthenticatedAction(parse.json) { implicit request =>
    request.body.validate[RegistrationRequest] match {
        case JsSuccess(request, _) => {
            val (status, response) = performRegistration(request)
            status(Json.toJson(response.asInstanceOf[RegistrationResponse]))
        }

        case e: JsError => NotAcceptable(Json.toJson(GPError.MalformedJSON.value(e.toString + " REQUEST: " + request.body.toString)))
    }
}

As you can see – very, very nearly identical, with the exception of the request type (AuthenticationRequest versus RegistrationRequest), and the response type (AuthenticationResponse versus RegistrationResponse). Otherwise it's boilerplate.

There should be a way to distill this down to something like:

val register = GPAuthenticatedAction(parse.son) from(RegistrationRequest, RegistrationResponse)

I took a crack at defining a from[I,O](request: I, response: O) but this led to a bunch of problems (no information about what JSON was deserializing, etc). So, I tried to abstract this out a bit by creating a couple of traits, GPRequest and GPResult:

trait GPRequest[T] {
    implicit val format = Json.format[T]
}

trait GPResponse[T] {
    implicit val format: Format[T] = Json.format[T]
    def from(error: GPError): GPResponse
}

And then tried to define a function such as:

def from[I: GPRequest, O: GPResponse](request: Request) { implicit request =>
    request.body.validate[I] match {
        case JsSuccess(request, _) => {
            val (status, response) = performAuthentication(request)
            status(Json.toJson(response.asInstanceOf[O]))
        }

        case e: JsError => NotAcceptable(Json.toJson(GPError.MalformedJSON.value(e.toString + " REQUEST: " + request.body.toString)))
    }
}

But this leads to all forms of problems. I've pasted the compiler errors below, but the general gist is:

  1. JSON can't figure out what to do with the implicit serialization (Format/Reads/Writes). Can't find apply and unapply of the actual type.
  2. Type parameter errors on GPRequest, GPResponse.
  3. Can't declare the request as implicit (my Scala syntax is probably screwed up here).

And it gets worse.

The the bottom line question: Does anyone have a clean design pattern that achieves what I'm looking for here? (Or alternatively, can someone that knows Play and Scala give some guidance on where to go from here). This really seems like it should be possible but I'm stuck figuring out the next step.

For the intrepid, here are the errors:

[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:33: trait GPRequest takes type parameters
[error]     object AuthenticationRequest extends GPRequest {
[error]                                          ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/utility/GPResponseMapping.scala:121: trait GPResponse takes type parameters
[error]     def okResponse(response: GPProcedureResult, instance: GPResponse): (Results.Status, GPResponse) = { (resultCodeFor(response.code), instance) }
[error]                                                                                         ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/utility/GPResponseMapping.scala:121: trait GPResponse takes type parameters
[error]     def okResponse(response: GPProcedureResult, instance: GPResponse): (Results.Status, GPResponse) = { (resultCodeFor(response.code), instance) }
[error]                                                           ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/utility/GPResponseMapping.scala:109: trait GPResponse takes type parameters
[error]     def okResponse(response: Results.Status, instance: GPResponse): (Results.Status, GPResponse) = { (response, instance) }
[error]                                                                                      ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/utility/GPResponseMapping.scala:109: trait GPResponse takes type parameters
[error]     def okResponse(response: Results.Status, instance: GPResponse): (Results.Status, GPResponse) = { (response, instance) }
[error]                                                        ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:37: trait GPResponse takes type parameters
[error]     case class AuthenticationResponse(token: Option[GPToken], id: Option[GPID], error: Option[GPError]) extends GPResponse {
[error]                                                                                                                 ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/utility/GPResponseMapping.scala:98: trait GPResponse takes type parameters
[error]     def errorResponse(error: GPProcedureResult, instance: GPResponse): (Results.Status, GPResponse) = { (resultCodeFor(error.code), instance.from(error.failed.getOrElse(GPError.errorForCode(error.code)))) }
[error]                                                                                         ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/utility/GPResponseMapping.scala:98: trait GPResponse takes type parameters
[error]     def errorResponse(error: GPProcedureResult, instance: GPResponse): (Results.Status, GPResponse) = { (resultCodeFor(error.code), instance.from(error.failed.getOrElse(GPError.errorForCode(error.code)))) }
[error]                                                           ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/utility/GPResponseMapping.scala:85: trait GPResponse takes type parameters
[error]     def errorResponse(error: GPError, instance: GPResponse): (Results.Status, GPResponse) = { (resultCodeFor(error), instance.from(error)) }
[error]                                                                               ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/utility/GPResponseMapping.scala:85: trait GPResponse takes type parameters
[error]     def errorResponse(error: GPError, instance: GPResponse): (Results.Status, GPResponse) = { (resultCodeFor(error), instance.from(error)) }
[error]                                                 ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:43: trait GPResponse takes type parameters
[error]     object AuthenticationResponse extends GPResponse {
[error]                                           ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:53: trait GPRequest takes type parameters
[error]     object RegistrationRequest extends GPRequest {
[error]                                        ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:57: trait GPResponse takes type parameters
[error]     case class RegistrationResponse(token: Option[GPToken], id: Option[GPID], error: Option[GPError]) extends GPResponse {
[error]                                                                                                               ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:63: trait GPResponse takes type parameters
[error]     object RegistrationResponse extends GPResponse {
[error]                                         ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:73: trait GPRequest takes type parameters
[error]     object LogoutRequest extends GPRequest {
[error]                                  ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:77: trait GPResponse takes type parameters
[error]     case class LogoutResponse(error: Option[GPError] = None) extends GPResponse {
[error]                                                                      ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:81: trait GPResponse takes type parameters
[error]     object LogoutResponse extends GPResponse {
[error]                                   ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:88: trait Request takes type parameters
[error]     def from[I: GPRequest, O: GPResponse](request: Request) { implicit request =>
[error]                                                    ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:88: missing parameter type
[error]     def from[I: GPRequest, O: GPResponse](request: Request) { implicit request =>
[error]                                                                        ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:91: type mismatch;
[error]  found   : Any
[error]  required: controllers.GPSecurityService.AuthenticationRequest
[error]                 val (status, response) = performAuthentication(request)
[error]                                                                ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/controllers/GPSecurityService.scala:92: Any does not take parameters
[error]                 status(Json.toJson(response.asInstanceOf[O]))
[error]                       ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/utility/GPResponseMapping.scala:15: No unapply or unapplySeq function found
[error]     implicit val format = Json.format[T]
[error]                                      ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/utility/GPResponseMapping.scala:19: No unapply or unapplySeq function found
[error]     implicit val format: Format[T] = Json.format[T]
[error]                                                 ^
[error] /Users/zbeckman/Projects/Glimpulse/Server/project/glimpulse-server/app/utility/GPResponseMapping.scala:20: trait GPResponse takes type parameters
[error]     def from(error: GPError): GPResponse
1

There are 1 answers

0
Thiago Pereira On

You could write parent case classes instead of your traits and extend your existing types:

AuthenticationRequest, RegistrationRequest, AuthenticationResponse, RegistrationResponse

being a subclass of your new parent case classes.

With this, you could change your signature to extend your generic types like this:

def from[I <: GPRequest, O <: GPResponse]