I have a Play application with several modules, each of which has its own exception set. Here are three examples:
Module common
:
package services.common
trait CommonErrors {
final case class NotFound(id: String) extends Exception(s"object $id not found")
final case class InvalidId(id: String) extends Exception(s"$id is an invalid id")
...
// `toJson` is just an extension method that converts an exception to JSON
def toResult(e: Exception): Result = e match {
case NotFound => Results.NotFound(e.toJson)
case InvalidId => Results.BadRequest(e.toJson)
case _ => Results.InternalError(e.toJson)
}
}
Module auth
:
package services.auth
trait AuthErrors {
final case class UserNotFound(e: NotFound) extends Exception(s"user ${e.id} not found")
final case class UserAlreadyExists(email: String) extends Exception(s"user identified by $email already exists")
...
// `toJson` is just an extension method that converts an exception to JSON
def toResult(e: Exception): Result = e match {
case UserNotFound => Results.NotFound(e.toJson)
case UserAlreadyExists => Results.BadRequest(e.toJson)
case _ => Results.InternalError(e.toJson)
}
}
Module other
:
trait OtherErrors {
final case class AnotherError(s: String) extends Exception(s"another error: $s")
...
// `toJson` is just an extension method that converts an exception to JSON
def toResult(e: Exception): Result = e match {
case AnotherError => Results.BadRequest(e.toJson)
...
case _ => Results.InternalError(e.toJson)
}
}
As you can see, each trait defines a set of exceptions and provides a method to convert that exception to a JSON response like this:
{
"status": 404,
"code": "not_found",
"message": "user 123456789123456789123456 not found",
"request": "https://myhost.com/users/123456789123456789123456"
}
What I'm trying to achieve is to have each module defining its exceptions, reusing the ones defined in the common
module, and mixin the exception traits as needed:
object Users extends Controller {
val errors = new CommonErrors with AuthErrors with OtherErrors {
// here I have to override `toResult` to make the compiler happy
override def toResult(e: Exception) = super.toResult
}
def find(id: String) = Action { request =>
userService.find(id).map { user =>
Ok(success(user.toJson))
}.recover { case e =>
errors.toResult(e) // this returns the appropriate result
}
}
}
If you look at how I've overridden toResult
, I always return super.toResult
, which corresponds to the implementation contained in trait OtherErrors
... and this implementation might miss some patterns that are expected to be found in CommonErrors.toResult
.
For sure I'm missing something... so the question is: what's the design pattern to fix the issue with multiple implementations of toResult
?
You could use Stackable Trait pattern. I'll replaced your
.toJson
with.getMessage
for simplification reason:define base trait:
and stackable traits:
so if we have some stack
and defined some helper
following code
will produce reasonable output
also we can refactor this code, pulling case classed from traits, and make function's more composable: