Reputation: 10145
I'm attempting to model responses from REST APIs as case classes which I can use pattern matching on.
I thought it would be a good fit assuming inheritance, but I see that this is deprecated. I know there are already questions related to case classes and inheritance, but my question is more about how you would model the following the "right way" here without inheritance.
I started with the following two case classes, which work fine:
case class Body(contentType: String, content: String)
case class Response(statusCode: Int, body: Body)
i.e. a REST call would return with something like:
Response(200, Body("application/json", """{ "foo": "bar" }"""))
which I could pattern match like:
response match {
case Response(200, Body("application/json", json)) => println(json)
case Response(200, Body("text/xml", xml)) => println(xml)
case Response(_,_) => println("Something unexpected")
}
etc. which works fine.
Where I ran into trouble is: I'd like helper extensions for these case classes, such as:
case class OK(body: Body) extends Response(200, body)
case class NotFound() extends Response(404, Body("text/plain", "Not Found"))
case class JSON(json: String) extends Body("application/json", json)
case class XML(xml: String) extends Body("text/xml", xml)
so that I can do simplified pattern matches like this:
response match {
case OK(JSON(json)) => println(json)
case OK(XML(xml)) => println(xml)
case NotFound() => println("Something is not there")
// And still drop down to this if necessary:
case Response(302, _) => println("It moved")
}
and also which would also allow my REST code to directly use and return:
Response(code, Body(contentType, content))
which is easier to build a response dynamically.
so...
I can get it to compile (with deprecation warnings) via:
case class OK(override val body: Body) extends Response(200, body)
However, this doesn't seem to work with pattern matching.
Response(200, Body("application/json", "")) match {
case OK(_) => ":-)"
case _ => ":-("
}
res0: java.lang.String = :-(
Any ideas on how this could work? I'm open to different approaches, but this was my attempt to find a practical use for case classes
Upvotes: 6
Views: 1973
Reputation: 67300
There are several reasons why case classes shouldn't be subclassed. In your case, the problem becomes that OK
is another type than (a subtype of) Response
, therefore the match fails (even if the arguments match, the type doesn't match).
You will want custom extractors, instead. For example:
case class Response(code: Int, body: String)
object OK {
def apply(body: String) = Response(200, body)
def unapply(m: Response): Option[String] = m match {
case Response(200, body) => Some(body)
case _ => None
}
}
def test(m: Response): String = m match {
case OK(_) => ":-)"
case _ => ":-("
}
test(Response(300, "Hallo")) // :-(
test(Response(200, "Welt")) // :-)
test(OK("Welt")) // :-)
There are few more examples of custom extractors in this thread.
Upvotes: 10
Reputation: 297265
While custom extractors mentioned by 0__ can certainly be used, you'll lose exhaustiveness guarantees of sealed type hierarchies. While in the example you gave in the question there's nothing sealed
, the problem is well suited to them.
In that case, my suggestion is to simply make sure the case class
is always at the bottom of the type hierarchy, and make the upper classes normal. For example:
sealed class Response(val statusCode: Int, val body: Body) sealed
case class Ok(override val body: Body) extends Response(200, body)
sealed class NotOk(statusCode: Int, body: Body) extends Response(statusCode, body)
case object NotFound extends NotOk(404, "Not found")
// and so on...
Upvotes: 1
Reputation: 1918
Have you looked at the scala library unfiltered? http://unfiltered.lessis.me/ It may help you with approaching you problem . HTH
Upvotes: 1