从扁平元组创建嵌套通用 Case Class

Creating a nested generic Case Class from a flattened Tuple

背景

我正在使用 Play Framework 和 Slick 开发 API。为了避免重复样板,我想定义没有 ID 字段的 public JSON 模型,并将它们包装在 WithId 容器中。

import play.api.libs.json._
import play.api.libs.functional.syntax._

case class WithId[T](id: Long, item: T)
case class Wiki(name: String, source: Option[String], text: String)

object WithId {
  implicit def withIdRead[T : Reads] : Reads[WithId[T]] = (
    (JsPath \ "id").read[Long] and
    JsPath.read[T]
  )((id, item) => WithId(id, item))

  implicit def withIdWrite[T : Writes] : Writes[WithId[T]] = (
    (JsPath \ "id").write[Long] and
    JsPath.write[T]
  ).apply(unlift(WithId.unapply[T]))
}

多亏了 ReadsWrites 定义的魔力,无论有没有 id,我都可以轻松处理 JSON。

scala> val rawIdJson = """{"id": 123, "name": "My First Wiki", "text": "This is my first wiki article"}"""
rawIdJson: String = {"id": 123, "name": "My First Wiki", "text": "This is my first wiki article"}

scala> val withId = Json.parse(rawIdJson).validate[WithId[Wiki]].get
withId: model.util.WithId[model.entity.Wiki] = WithId(123,Wiki(My First Wiki,None,This is my first wiki article))

scala> val withIdJson = Json.toJson(withId)
withIdJson: play.api.libs.json.JsValue = {"id":123,"name":"My First Wiki","text":"This is my first wiki article"}



scala> val rawJson = """{"name": "My First Wiki", "text": "This is my first wiki article"}"""
rawJson: String = {"name": "My First Wiki", "text": "This is my first wiki article"}

scala> val withoutId = Json.parse(rawJson).validate[Wiki].get
withoutId: model.entity.Wiki = Wiki(My First Wiki,None,This is my first wiki article)

scala> val withoutIdJson = Json.toJson(withoutId)
withoutIdJson: play.api.libs.json.JsValue = {"name":"My First Wiki","text":"This is my first wiki article"}

一切都很好。

我现在遇到的问题是 Slick 将 return 行以元组或大小写 classes 的形式从数据库中获取,具体取决于我使用的查询。显然,我可以编写很多非常直接的辅助方法来将 tuple/case class 转换为每个 public 模型:

object Wiki {
  implicit val wikiFmt = Json.format[Wiki]
  def fromRow(row: WikiRow) : WithId[Wiki] = WithId(row.id, Wiki(row.name, row.source, row.text))
  def fromRow(tup: (Long, String, Option[String], String)) : WithId[Wiki] = WithId(tup._1, Wiki(tup._2, tup._3, tup._4))
}

...但是随着 public 模型数量的增长,需要维护大量样板文件。

问题

  1. 有没有一种干净的方法可以将 Tuple4[Long, String, Option[String], String]case class WikiRow(id: Long, name: String, source: Option[String], text: String) 转换为 WithId[Wiki](反之亦然)?

  2. 一旦我引入另一个 public 模型,如 case class Template(name: String, description: String),我们能否将解决方案从 #1 推广到现在处理将 Tuple3[Long, String, Strong] 转换为 WithId[Template](反之亦然)?

  3. 如果我们将一个字段放入 public 模型中未使用的私有模型会怎样?例如,case class WikiRow(id: Long, name: String, source: Option[String], text: String, hidden: Boolean)hidden 字段在 WikiRow => WithId[Wiki] 时需要删除,而在 WithId[Wiki] => WikiRow 时需要从另一个来源提供。

至于问题 1 和 2:是的,正如建议的那样,无形是可能的。这是从元组或行到 WithId[T].

的解决方案
scala> :paste
// Entering paste mode (ctrl-D to finish)

case class WikiRow(id: Long, name: String, source: Option[String], text: String) 
case class Wiki(name: String, source: Option[String], text: String)
case class WithId[T](id: Long, item: T)

def createWithId[T] = new WithIdCreator[T]

class WithIdCreator[Out] {
  import shapeless._
  import shapeless.ops.hlist.IsHCons
  def apply[In, InGen <: HList, Tail <: HList](in: In)(
    implicit
    genIn: Generic.Aux[In,InGen],
    hcons: IsHCons.Aux[InGen,Long,Tail],
    genOut: Generic.Aux[Out,Tail]
  ): WithId[Out] = {
    val rep = genIn.to(in)
    val id = hcons.head(rep)
    val tail = hcons.tail(rep)
    WithId(id, genOut.from(tail))
  }
}

// Exiting paste mode, now interpreting.

defined class WikiRow
defined class Wiki
defined class WithId
createWithId: [T]=> WithIdCreator[T]
defined class WithIdCreator

scala> createWithId[Wiki](WikiRow(3L, "foo", None, "barbaz"))
res1: WithId[Wiki] = WithId(3,Wiki(foo,None,barbaz))

scala> createWithId[Wiki]((3L, "foo", None: Option[String], "barbaz"))
res2: WithId[Wiki] = WithId(3,Wiki(foo,None,barbaz))

scala> case class Template(name: String, description: String)
defined class Template

scala> createWithId[Template]((3L, "foo", "barbaz"))
res3: WithId[Template] = WithId(3,Template(foo,barbaz))

另一个方向的转换或多或少是类似的。

我看不出为什么 3 也不可能,但是你将不得不再次重写转换以处理删除或手动提供参数。

您可以在 shapeless guide 中了解更多相关信息。那里解释了所有必要的概念。