Search code examples
scalatypeclassshapeless

Use shapeless to derive query string params from case class


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!


Solution

  • 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("&")
    }