我正在尝试使用Shapeless中的自动类型类派生机制来实现各种类型的ReadJsonCodec。
这是我的ReadCodecCompanionObject:
object ReadCodec extends LabelledProductTypeClassCompanion[ReadCodec] {
implicit object StringCodec extends SimpleCodec[String] {
def read(j: Json): String = j.stringOr(throw ...)
}
implicit object IntCodec ...
implicit object BooleanCodec ...
implicit object LongCodec ...
implicit object ShortCodec ...
implicit object DoubleCodec ...
implicit object BigDecimalCodec ...
implicit def readCodecInstance: LabelledProductTypeClass[ReadCodec] = new LabelledProductTypeClass[ReadCodec] {
def emptyProduct = new ReadCodec[HNil] {
// This will silently accept extra fields within a JsonObject
// To change this behavior make sure json is a JsonObject and that it is empty
def read(json: Json) = HNil
}
def product[F, T <: HList](name: String, FHead: ReadCodec[F], FTail: ReadCodec[T]) = new ReadCodec[F :: T] {
def read(json: Json): F :: T = {
val map = castOrThrow(json)
val fieldValue = map.getOrElse(name, throw new MappingException(s"Expected field $name on JsonObject $map"))
// Try reading the value of the field
// If we get a mapping exception, intercept it and add the name of this field to the path
// If we get another exception, don't touch!
// Pitfall: if handle did not accept a PartialFunction, we could transform an unknow exception into a match exception
val head: F = Try(FHead.read(fieldValue)).handle{ case MappingException(msg, path) => throw MappingException(msg, s"$name/$path")}.get
val tail = FTail.read(json)
head :: tail
}
}
def product[A, T <: HList](name: String, FHead: ReadCodec[Option[A]], FTail: ReadCodec[T]) = new ReadCodec[Option[A] :: T] {
def read(json: Json): Option[A] :: T = {
val map = castOrThrow(json)
val head: Option[A] = map.get(name).map { fieldValue =>
Try(FHead.read(fieldValue)).handle{ case MappingException(msg, path) => throw MappingException(msg, s"$name/$path")}.get.get
}
val tail = FTail.read(json)
head :: tail
}
}
def project[F, G](instance: => ReadCodec[G], to : F => G, from : G => F) = new ReadCodec[F] {
def read(json: Json): F = from(instance.read(json))
}
}
}
这是一段很复杂的代码,可以快速掌握,但一旦你理解它就非常简单。重要的部分是两个def product
方法。我遇到的问题是,如果该字段将映射到类型Option[A]
的值,我希望此编解码器接受缺少字段的Json AST。这意味着我需要产品函数来知道HList的头部是否为Option [A]类型。
具体地:
case class Foo(a: String, b: Option[Boolean])
val test = read[Foo](json"""{"a" : "Beaver"}""") // should work
但是目前这会失败,因为它没有区分Option并期望一个字段b
。我尝试了两种解决方法,但两种方法都没有。
第一个和最干净的一个是使用一个版本来重载product方法,其中F类型参数被Option [A]替换。这种方法相当干净,虽然我不确定它是否能与无形推导宏一起运行良好。但是,这是不可能的,因为当擦除后的类型签名相同时,Scala不支持重载函数的能力。这是上面的版本。不幸的是,目前还没有使用Scala编译器进行编译。
第二种方法是使用TypeTag在运行时查找F是否为Option [_]类型且行为正确。这个版本几乎肯定不那么优雅,并且涉及演员,但我可以忍受它。但是,似乎不可能,因为添加TypeTag会更改product方法的签名(添加隐式参数),然后编译器会抱怨我没有定义抽象方法产品。
有没有人对最佳方法有任何建议。
答案 0 :(得分:2)
您需要获取一个类型类,其中包含有关F
传递的信息。但是你已经以ReadCodec
的形式传递了一个类型类。所以解决方案是将其替换为包含所需信息的那个:
trait ReadCodecAndTypeTag[A] {
val rc: ReadCodec[A]
val tt: TypeTag[A]
}
但在这种情况下,你也可以将解码从可选值映射到这个类型类:
trait OReadCodec[A] {
val rc: ReadCodec[A]
def missingField(name: String, map: Any): A =
throw new MappingException(s"Expected field $name on JsonObject $map")
}
implicit object StringCodec extends OReadCodec[String] {
val rc = new ReadCodec[String] {...}
}
implicit object IntCodec ...
...
implicit def OptionCodec[A](implicit orc: OReadCodec[A]) =
new OReadCodec[Option[A]] {
val rc = ...
override def missingField(name: String, map: Any) = None
}
...
def product[F, T <: HList](name: String, FHead: OReadCodec[F], FTail: OReadCodec[T]) =
new OReadCodec[F :: T] {
val rc = new ReadCodec[F :: T] {
def read(json: Json): F :: T = {
val map = castOrThrow(json)
val fieldValue = map.getOrElse(name, FHead.missingField(name, map))
val head: F = ...
...
}
}
}
implicit def ReadCodecFromOReadCodec[A](implicit orc: OReadCodec[A]) = orc.rc