4
votes

I'm new to Scala and exploring possibilities of implicit conversions and famous cake pattern. I tried to create the model class which has the id value listed as abstract type to avoid leaking implementation details. I also mixed it into cake pattern trait wrapper. Everything works fine except for implicit conversion from id to JSON (within Play framework). Scala compiler just can't find implicit conversion no matter what I do.

Here's the code that reproduces the the problem:

import anorm._
import play.api.libs.json._

trait Card {
  type Key
  val NoId: Key
  val id: Key
  val name: String
}

trait CardModelComponent {
  val cardModel: CardModel
  trait CardModel {
    def findById(id: Long): Option[Card]
    def findAll: Seq[Card]
    def delete(id: Long)
    def create(name: String): Option[Card]
  }
}

trait CardModelComponentImpl extends CardModelComponent {
  case class CardImpl(id: Pk[Long], name: String) extends Card {
    type Key = Pk[Long]

    object Key extends Writes[Key] {
      implicit def writes(key: Key): JsValue = {
        key match {
          case Id(idVal: Long) => JsNumber(idVal)
          case _ => JsNull
        }
      }
    }
    val NoId = NotAssigned
  }

  class CardModelImpl extends CardModel {
    def findById(id: Long): Option[Card] = { None }
    def findAll: Seq[Card] = { Seq(CardImpl(Id(1), "Some card"))}
    def delete(id: Long) {}
    def create(name: String): Option[Card] = { Some(CardImpl(Id(1), name)) }
  }
}

object ComponentsRegistry extends
CardModelComponentImpl {

  val cardModel = new CardModelImpl
}

val card = ComponentsRegistry.cardModel.create("Test card").get
Json.toJson(card.id)

Error output I'm getting is like follows:

> card: Card = CardImpl(1,Test card)
> <console>:19: error: No Json deserializer found for type card.Key. Try to implem
  ent an implicit Writes or Format for this type.
                Json.toJson(card.id)
                           ^

Is there any way to make it work? Looks like cake pattern wrapping hides too much type information from compiler, as I guess from card.Key type name.

I also tried to create Writer implementation for Pk directly with same error as result.

3

3 Answers

1
votes

Like Jesper Nordenberg wrote, you have to bring the instance of Writes[Key] into scope. One way of doing that would be to demand that the implementation of Card publishes the Writer instance and then import that whereever you call toJson, like this:

...

trait Card {
  type Key

  val NoId: Key
  val id: Key
  val name: String
  implicit val Key: Writes[Key]
}

...

trait CardModelComponentImpl extends CardModelComponent {
  case class CardImpl(id: Pk[Long], name: String) extends Card {
    type Key = Pk[Long]

    implicit object Key extends Writes[Key] {
      def writes(key: Key): JsValue = {
        key match {
          case Id(idVal: Long) => JsNumber(idVal)
          case _ => JsNull
        }
      }
    }

    val NoId = NotAssigned
  }

  class CardModelImpl extends CardModel {
    def findById(id: Long): Option[Card] = { None }
    def findAll: Seq[Card] = { Seq(CardImpl(Id(1), "Some card"))}
    def delete(id: Long) {}
    def create(name: String): Option[Card] = { Some(CardImpl(Id(1), name)) }
  }
}

...

val card = ComponentsRegistry.cardModel.create("Test card").get
import card.Key
Json.toJson(card.id)

or pass it explicitly:

val card = ComponentsRegistry.cardModel.create("Test card").get
Json.toJson(card.id)(card.Key)
2
votes

It's the Writes[Key] instance that should be implicit, not the writes() member method. The implicit instance also has to be in scope where the toJson() method is called.

1
votes

I've modified your code little bit. I hope this is helpful for you.

package models
import anorm._
import play.api.libs.json._

trait Card[T] {
  val NoId: T
  val id: T
  val name: String
}

trait CardModelComponent[T] {
  val cardModel: CardModel
  trait CardModel {
    def findById(id: Long): Option[Card[T]]
    def findAll: Seq[Card[T]]
    def delete(id: Long)
    def create(name: String): Option[Card[T]]
  }
}

trait CardModelComponentImpl extends CardModelComponent[Pk[Long]] {
  type Key = Pk[Long]
  case class CardImpl(id: Key, name: String) extends Card[Key] {
    val NoId = NotAssigned
  }


  class CardModelImpl extends CardModel {
    def findById(id: Long): Option[Card[Key]] = { None }
    def findAll: Seq[Card[Key]] = { Seq(CardImpl(Id(1), "Some card"))}
    def delete(id: Long) {}
    def create(name: String): Option[Card[Key]] = { Some(CardImpl(Id(1), name)) }
  }
}

object ComponentsRegistry extends CardModelComponentImpl {
  val cardModel = new CardModelImpl
  implicit object KeyWrites extends Writes[Key] {
    def writes(key: Key): JsValue = {
      key match {
        case Id(idVal: Long) => JsNumber(idVal)
        case _ => JsNull
      }
    }
  }
}

And then you can use like this:

import models.ComponentsRegistry
import models.ComponentsRegistry.KeyWrites
val card = ComponentsRegistry.cardModel.create("Test card").get
Json.toJson(card.id)