我正在尝试派生用于将案例类序列化为查询字符串的类型类。但是有一个不同之处-列表不是按常规方式编码的(据我所知,“常规”方式是什么),而是像下面这样,并结合了列表的字段名称。
case class Example(attributes: List[String])
val example = Example(List("foo", "bar"))
encode(example) // attributes.1=foo&attributes.2=bar
我有一个非常基本的东西适用于基元,现在我需要一些使列表按预期方式工作的方法的想法。
trait Encoder[T] {
def encode(value: T): String
}
object Encoder {
def apply[T](implicit encoder: Encoder[T]): Encoder[T] = encoder
}
def createEncoder[A](fn: A => String): Encoder[A] =
(value: A) => fn(value)
implicit def hlistEncoder[K <: Symbol, H, T <: HList](
implicit
witness: Witness.Aux[K],
hEncoder: Lazy[Encoder[H]],
tEncoder: Encoder[T]
): Encoder[FieldType[K, H] :: T] = {
val fieldName: String = witness.value.name
createEncoder { hlist =>
val head = hEncoder.value.encode(hlist.head)
hlist.tail match {
case HNil => s"$fieldName=$head"
case _ =>
val tail = tEncoder.encode(hlist.tail)
s"$fieldName=$head&$tail"
}
}
}
implicit def genericEncoder[A, H](
implicit
generic: LabelledGeneric.Aux[A, H],
hEncoder: Lazy[Encoder[H]]
): Encoder[A] =
createEncoder { value =>
hEncoder.value.encode(generic.to(value))
}
implicit val intEncoder: Encoder[Int] = createEncoder(_.toString)
implicit val strEncoder: Encoder[String] = createEncoder(identity)
implicit val boolEncoder: Encoder[Boolean] = createEncoder(_.toString)
implicit val hnilEncoder: Encoder[HNil] = createEncoder(_ => "")
谢谢!
此案例很好地描述了Type Astronaut
书:https://books.underscore.io/shapeless-guide/shapeless-guide.html#records-and-labelledgeneric。
因此,基本上,您需要看一下LabeledGeneric
类型类-它对任何产品类型(例如案例类)派生都有帮助。
在您的情况下,实现可能看起来像:
import shapeless.{::, HList, HNil, LabelledGeneric, Lazy, Witness}
import shapeless.labelled.FieldType
trait Encoder[T] {
def encode(value: T): String
}
object Encoder {
def createEncoder[T](f: T => String): Encoder[T] = f(_)
def apply[T](implicit encoder: Encoder[T]): Encoder[T] = encoder
implicit val stringEncoder: Encoder[String] = createEncoder[String](identity)
implicit val intEncoder: Encoder[Int] = createEncoder[Int](_.toString)
}
// Special encoder, which can encode product or object
trait ObjectEncoder[T] extends Encoder[T] {
final override def encode(value: T): String = {
encodeObject(value).map{
case (key, value) => s"$key=$value"
}.mkString("&")
}
def encodeObject(t: T): Map[String, String]
}
object ObjectEncoder {
def createEncoder[T](f: T => Map[String, String]): ObjectEncoder[T] = f(_)
def apply[T](implicit encoder: ObjectEncoder[T]): ObjectEncoder[T] = encoder
// This need to terminate derivation process
implicit val hNilObjectEncoder: ObjectEncoder[HNil] = {
ObjectEncoder.createEncoder(_ => Map.empty)
}
// Generate object encoder derivation
implicit def hlistObjectEncoder[K <: Symbol, H, T <: HList]
(implicit
fieldWitness: Witness.Aux[K],
headEncoder: Lazy[Encoder[H]],
tailEncoder: ObjectEncoder[T]
): ObjectEncoder[FieldType[K, H] :: T] = {
val fieldName: String = fieldWitness.value.name
ObjectEncoder.createEncoder { hlist =>
val headValue: String = headEncoder.value.encode(hlist.head)
val head: Map[String, String] = List(fieldName -> headValue).toMap
val tail: Map[String, String] = tailEncoder.encodeObject(hlist.tail)
head ++ tail
}
}
implicit def genericObjectEncoder[A, H]
( implicit
generic: LabelledGeneric.Aux[A, H],
hEncoder: Lazy[ObjectEncoder[H]]
): ObjectEncoder[A] = {
ObjectEncoder.createEncoder { value =>
val t = generic.to(value)
hEncoder.value.encodeObject(t)
}
}
}
// Just syntactic sugar helpers
object EncoderSyntax {
implicit class EncodeOps[A](a: A) {
def encode(implicit encoder: Encoder[A]): String = encoder.encode(a)
}
}
def main(args: Array[String]): Unit = {
import Encoder._
import ObjectEncoder._
import EncoderSyntax._
case class Example(foo: String, bar: String)
val example = Example("foo", "bar")
println(example.encode)
}
将产生下一个结果:
foo=foo&bar=bar
希望这会有所帮助!
我最终通过修改typeclass来解决此问题,以返回一个在应用时构造正确字符串的函数。它不是完美的,但我很确定它能解决问题。
trait Encoder[T] {
def encode(value: T): String => List[String]
}
object Encoder {
def createEncoder[T](f: T => String): Encoder[T] = value => name => List(s"$name=${f(value)}")
def apply[T](implicit encoder: Encoder[T]): Encoder[T] = encoder
implicit val stringEncoder: Encoder[String] = createEncoder[String](identity)
implicit val intEncoder: Encoder[Int] = createEncoder[Int](_.toString)
implicit val boolEncoder: Encoder[Boolean] = createEncoder(_.toString)
implicit def listEncoder[T](implicit encoder: Encoder[T]): Encoder[List[T]] =
(list: List[T]) => (name: String) => list.zipWithIndex.map { case (value, index) => s"$name.${index + 1}=$value" }
implicit def mapEncoder[T](implicit encoder: Encoder[T]): Encoder[Map[String, T]] =
(map: Map[String, T]) =>
(name: String) =>
map.zipWithIndex.flatMap {
case ((key, value), index) => List(s"$name.${index + 1}.Name=$key", s"$name.${index + 1}.Value=$value")
}.toList
}
trait ObjectEncoder[T] extends Encoder[T] {
final override def encode(value: T): String => List[String] =
name => encodeObject(value).apply(name)
def encodeObject(t: T): String => List[String]
}
object ObjectEncoder {
def createEncoder[T](f: T => String => List[String]): ObjectEncoder[T] = new ObjectEncoder[T] {
override def encodeObject(t: T): String => List[String] = name => f.apply(t).apply(name)
}
def apply[T](implicit encoder: ObjectEncoder[T]): ObjectEncoder[T] = encoder
implicit val hNilObjectEncoder: ObjectEncoder[HNil] = createEncoder(_ => _ => List.empty)
implicit def hlistObjectEncoder[K <: Symbol, H, T <: HList](
implicit
fieldWitness: Witness.Aux[K],
headEncoder: Lazy[Encoder[H]],
tailEncoder: ObjectEncoder[T]): ObjectEncoder[FieldType[K, H] :: T] = {
val fieldName: String = fieldWitness.value.name
createEncoder { hlist =>
val head: String => List[String] = headEncoder.value.encode(hlist.head)
val tail: String => List[String] = tailEncoder.encodeObject(hlist.tail)
(name: String) =>
head.apply(fieldName) ++ tail.apply(name)
}
}
implicit def genericObjectEncoder[A, H](
implicit
generic: LabelledGeneric.Aux[A, H],
hEncoder: Lazy[ObjectEncoder[H]]): ObjectEncoder[A] =
ObjectEncoder.createEncoder { value =>
val t = generic.to(value)
hEncoder.value.encodeObject(t)
}
// Unfortunately we must apply the returned function with an empty string, the arg is used for formatting
// each piece but is unecessary for the whole thing
def encode[T](input: T)(implicit encoder: Encoder[T]) = encoder.encode(input).apply("").mkString("&")
}