Используйте бесформенный для получения параметров строки запроса из класса case - PullRequest
2 голосов
/ 28 февраля 2020

Я пытаюсь получить класс типа для сериализации класса дела в строку запроса. Хотя есть и поворот - списки кодируются не обычным способом (насколько я могу судить, что такое «нормальный» способ), а, как показано ниже, с включенным именем поля списка.

case class Example(attributes: List[String])
val example = Example(List("foo", "bar"))

encode(example) // attributes.1=foo&attributes.2=bar

У меня есть что-то очень базовое c, которое работает для примитивов, но теперь мне нужны идеи, как получить списки, работающие, как ожидалось.

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(_ => "")

Спасибо!

Ответы [ 2 ]

1 голос
/ 29 февраля 2020

Этот случай хорошо описан Type Astronaut книга: https://books.underscore.io/shapeless-guide/shapeless-guide.html#records -and-labelledgeneri c.

Итак, в общем, вам нужно взглянуть на 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

Надеюсь, это поможет!

0 голосов
/ 01 марта 2020

Я решил эту проблему, изменив класс типов так, чтобы он возвращал функцию, которая создает правильную строку при применении. Он не идеален, но я уверен, что сработает.

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