nested case classes capability #64

Closed
simbo1905 opened this Issue Sep 4, 2013 · 5 comments

Comments

Projects
None yet
2 participants

I am new to scala and first look at anything in scalaz so my apologies as this is a newbie question rather than an actual issue. I have the v6.0 code checked out and have cloned the json example and am trying to use my own nested case classes:

package argonaut.example

import argonaut._, Argonaut._
import org.specs2._

abstract class Message
case class CardDrawn(player: Long, card: Int, mType: String = "CardDrawn") extends Message
case class CardSet(cards: List[CardDrawn], mType: String = "CardSet") extends Message

object CardSetExample extends Specification {
  val json =
    Json(
      "mType" := "CardSet",
      "cards" := List(
        Json ( "mType" := "CardDrawn", "player" := 1L, "card" :=2 ),
        Json ( "mType" := "CardDrawn", "player" := 1L, "card" :=2 ),
        Json ( "mType" := "CardDrawn", "player" := 1L, "card" :=2 )
      )
    )

  val value = CardSet(List(CardDrawn(1L,2),CardDrawn(3L,4)))

  implicit val CodecCardSet = casecodec2(CardSet.apply, CardSet.unapply)("mType","cards")
  implicit val CodecCardDrawn = casecodec3(CardDrawn.apply, CardDrawn.unapply)("mType", "player", "card")

  def is = "JsonExample" ^
    "Can decode hand crafted object" ! {
      json.as[CardSet].toOption must beSome(value)
    } ^
    "Can encode to match hand crafted object" ! {
      value.asJson must_== json
    }
}

This does not work however running it on the sbt console it barfs out

> test-only argonaut.example.CardSetExample
[info] Compiling 1 Scala source to /Users/simbo/git/argonaut-60/target/scala-2.10/test-classes...
[error] /Users/simbo/git/argonaut-60/src/test/scala/argonaut/example/CardSetExample.scala:25: could not find implicit value for evidence parameter of type argonaut.EncodeJson[List[argonaut.example.CardDrawn]]
[error]   implicit val CodecCardSet = casecodec2(CardSet.apply, CardSet.unapply)("mType","cards")
[error]                                                                         ^
[error] /Users/simbo/git/argonaut-60/src/test/scala/argonaut/example/CardSetExample.scala:30: could not find implicit value for parameter e: argonaut.DecodeJson[argonaut.example.CardSet]
[error]       json.as[CardSet].toOption must beSome(value)
[error]              ^
[error] two errors found
[error] (argonaut/test:compile) Compilation failed
[error] Total time: 5 s, completed 04-Sep-2013 22:05:49
> 

Which clearly states that I don't know what I need to do to make a codec which handles the round trip. Is this supported?

Thanks for your consideration in this matter.

Contributor

markhibberd commented Sep 4, 2013

The test doesn't pass (not sure if it should or now, I haven't dived into the detail yet), but this compiles, there were two issues in that you need the implicits to be lazy val's or def's as they are mutually recursive, and also need to annotate the types of the codecs because type inference was failing at some point - it is not entirely clear as to what is causing the inference problem but will take a look later today.

package argonaut.example

import argonaut._, Argonaut._
import org.specs2._

abstract class Message
case class CardDrawn(player: Long, card: Int, mType: String = "CardDrawn") extends Message
case class CardSet(cards: List[CardDrawn], mType: String = "CardSet") extends Message

object CardSetExample extends Specification {
  val json =
    Json(
      "mType" := "CardSet",
      "cards" := List(
        Json ( "mType" := "CardDrawn", "player" := 1L, "card" :=2 ),
        Json ( "mType" := "CardDrawn", "player" := 1L, "card" :=2 ),
        Json ( "mType" := "CardDrawn", "player" := 1L, "card" :=2 )
      )
    )

  val value = CardSet(List(CardDrawn(1L,2),CardDrawn(3L,4)))

  implicit lazy val CodecCardSet: CodecJson[CardSet] = casecodec2(CardSet.apply, CardSet.unapply)("mType","cards")
  implicit lazy val CodecCardDrawn: CodecJson[CardDrawn] = casecodec3(CardDrawn.apply, CardDrawn.unapply)("mType", "player", "card")

  def is = "JsonExample" ^
    "Can decode hand crafted object" ! {
      json.as[CardSet].toOption must beSome(value)
    } ^
    "Can encode to match hand crafted object" ! {
      value.asJson must_== json
    }
}

Having said all this, I am not exactly sure what you are trying to do, it is a bit unusual to parse to a specific Message, i.e. CardSet in your example, I will do up an example for you that demonstrates how to handle sum types in a cleaner way.

Thanks for that. I have figured out that that in the casecode2/3 is defining the json attribute names and I had things in the wrong order. This is what I was wanting to do (the obvious conversions to and from string):

package argonaut.example

import argonaut._, Argonaut._

abstract class Message
case class CardDrawn(player: Long, card: Int, mType: String = "CardDrawn") extends Message
case class CardSet(cards: List[CardDrawn], mType: String = "CardSet") extends Message

object CardSetExample  {

  implicit lazy val CodecCardSet: CodecJson[CardSet] = casecodec2(CardSet.apply, CardSet.unapply)("cards","mType")
  implicit lazy val CodecCardDrawn: CodecJson[CardDrawn] = casecodec3(CardDrawn.apply, CardDrawn.unapply)("player", "card", "mType")

  def main(args: Array[String]): Unit = {
    val value = CardSet(List(CardDrawn(1L,2),CardDrawn(3L,4)))
    println(s"Got some good json ${value.asJson}")

    val jstring =
      """{
        | "cards":[
        |   {"player":"1","card":2,"mType":"CardDrawn"},
        |   {"player":"3","card":4,"mType":"CardDrawn"}
        | ],
        | "mType":"CardSet"
        | }""".stripMargin

    val parsed: Option[CardSet] =
      jstring.decodeOption[CardSet]

    println(s"Got a good object ${parsed.get}")
  }
}

Gives what I was hoping for:

Got some good json {"cards":[{"player":"1","card":2,"mType":"CardDrawn"},{"player":"3","card":4,"mType":"CardDrawn"}],"mType":"CardSet"}
Got a good object CardSet(List(CardDrawn(1,2,CardDrawn), CardDrawn(3,4,CardDrawn)),CardSet)

I was asking on stackoverflow what was a good framework for this if you care to post this as the answer I would be happy to give you the kudos as the answer.

http://stackoverflow.com/questions/18561689/custom-json-serialization-of-structured-scala-case-classes

@simbo1905 simbo1905 closed this Sep 4, 2013

Minor follow up question. If I have a websocket and the browser is sending down json command they may be any one of a large set of types of message depending on the command being sent to the server. At the moment I have a regex statement to do the parse:

  val CardDrawnPattern = ".*CardDrawn.*".r
  val CardUndrawnPattern = ".*CardUndrawn.*".r
  val RevealPattern = ".*Reveal.*".r
  val CardSetPattern = ".*CardSet.*".r

  def objectFrom(json: String): Option[Message] = {
    json match {
      case CardDrawnPattern() => {
        Some(objectFrom[CardDrawn](json))
      }
      case CardUndrawnPattern() => {
        Some(objectFrom[CardUndrawn](json))
      }
      case RevealPattern() => {
        Some(objectFrom[Reveal](json))
      }
      case CardSetPattern() => {
        Some(objectFrom[CardSet](json))
      }
      case _ => None
    }
  }

where my objectFrom[X](json) can now become json.decodeOption[X]. That looks a bit clumsy doing something as crude as a regex to decide what type to decode the json string as. Do you have a good pattern for such a common operation as mapping a json string to any one of a number of types?
Many thanks again for your consideration.

Contributor

markhibberd commented Sep 5, 2013

@simbo1905 Below is an example of how you might handle that using the codecs. We could probably do better with sum types in general so I have raised #65, but how you handle this sort of thing is often very specialized so I am not really sure what will help.

Also, if you are just after general help like this, you are probably better off hitting the mailing list (https://groups.google.com/forum/?fromgroups#!forum/argonaut-json) or irc.

package argonaut.example

import argonaut._, Argonaut._
import org.specs2._

abstract class Message
case class CardDrawn(player: Long, card: Int) extends Message
case class CardSet(cards: List[CardDrawn]) extends Message

object CardSetExample extends Specification {

  val setMessage: Message = CardSet(List(CardDrawn(1L,2),CardDrawn(3L,4)))

  val drawnMessage: Message = CardDrawn(1L,2)

  implicit lazy val CodecCardSet: CodecJson[CardSet] =
    casecodec1(CardSet.apply, CardSet.unapply)("cards")

  implicit lazy val CodecCardDrawn: CodecJson[CardDrawn] =
    casecodec2(CardDrawn.apply, CardDrawn.unapply)("player", "card")

  implicit lazy val CodecMessage: CodecJson[Message] = CodecJson(m => m match {
    case c @ CardSet(_) => Json { "CardSet" -> c.asJson }
    case c @ CardDrawn(_, _) => Json { "CardDrawn" -> c.asJson}
  } , c =>
    (c --\ "CardSet").as[CardSet].map(identity) |||
    (c --\ "CardDrawn").as[CardDrawn].map(identity)
  )

  def is = "JsonExample" ^
    "setMessage " ! {
      setMessage.asJson.as[Message].toOption must beSome(setMessage)
    } ^
    "drawnMessage " ! {
      drawnMessage.asJson.as[Message].toOption must beSome(drawnMessage)
    }

}

Mark,

Thanks for the help. Agreed that the groups would have been the correct place to go. I credited you with the answer to my question over on stackoverflow. I greatly look forward to the release of argonaut 6.1.

Thanks again,

Simon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment