I am attempting to derive a type class for serializing a case class to a query string. There is a twist though - lists are not encoded the normal way (as far as I can tell what the "normal" way is) but like below, with the field name of the list incorporated.
case class Example(attributes: List[String])
val example = Example(List("foo", "bar"))
encode(example) // attributes.1=foo&attributes.2=bar
I have something very basic which works for primitives, now I need some ideas of a way to get lists working as expected though.
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(_ => "")
Thanks!
I ended up solving this by modifying the typeclass to return a function which constructs the right string when applied. It's not perfect but I'm pretty sure it does the trick.
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("&")
}