一般将任意大小写 class 编码为 AWS Sdk DynamoDb 项目

Generically encode arbitrary case class into AWS Sdk DynamoDb Item

你觉得我做的有道理吗?有没有更好的方法将案例 class 编码为 Item?例如我不喜欢在某些情况下忽略输入参数!

import shapeless.labelled.FieldType
import shapeless.{::, DepFn2, HList, HNil, LabelledGeneric, Witness}

import scala.collection.mutable

// mock of sdk item
class Item(val map: mutable.Map[String, Any] = mutable.Map[String, Any]()) {
  def getString(attrName: String): String = map.get(attrName).get.asInstanceOf[String]
  def getInt(attrName: String): Int = map.get(attrName).get.asInstanceOf[Int]
  def getBoolean(attrName: String): Boolean = map.get(attrName).get.asInstanceOf[Boolean]
//  def getMap(attrName: String): Map[String, String] = Map("attrName" -> "attrValue")

  def setString(attrName: String, value: String): Unit = map.put(attrName, value)
  def setInt(attrName: String, value: Int): Unit = map.put(attrName, value)
  def setBoolean(attrName: String, value: Boolean): Unit = map.put(attrName, value)

  override def toString() = map.toString()
}

trait ItemEncoder[A] extends DepFn2[String, A] {
  type Out = Item
}

object ItemEncoder {
  def apply[A](implicit encoder: ItemEncoder[A]): ItemEncoder[A] = encoder

  def instance[A](f: (String, A) => Item): ItemEncoder[A] =
    new ItemEncoder[A] {
      override def apply(attrName: String, value: A): Out = f(attrName, value)
    }
}

implicit val stringEncoder: ItemEncoder[String] =
  ItemEncoder.instance { (attrName, value) =>
    val item = new Item()
    item.setString(attrName, value)
    item
  }

implicit val intEncoder: ItemEncoder[Int] =
  ItemEncoder.instance { (attrName, value) =>
    val item = new Item()
    item.setInt(attrName, value)
    item
  }

implicit val booleanEncoder: ItemEncoder[Boolean] =
  ItemEncoder.instance { (attrName, value) =>
    val item = new Item()
    item.setBoolean(attrName, value)
    item
  }

implicit val hnilEncoder: ItemEncoder[HNil] =
  ItemEncoder.instance((attrName, value) => new Item())

def merge(i1: Item, i2: Item): Item = new Item(i1.map ++ i2.map)

implicit def hlistEncoder[K <: Symbol, L, H, T <: HList](
  implicit
  witness: Witness.Aux[K],
  hEncoder: ItemEncoder[H],
  tEncoder: ItemEncoder[T]
): ItemEncoder[FieldType[K, H] :: T] = {
  ItemEncoder.instance { (_, value) =>
    val attrName = witness.value.name
    merge(hEncoder.apply(attrName, value.head), tEncoder.apply(attrName, value.tail))
  }
}

implicit def genericEncoder[A, R](
  implicit
  generic: LabelledGeneric.Aux[A, R],
  itemEncoder: ItemEncoder[R]
): ItemEncoder[A] =
  ItemEncoder.instance { (attrName, value) =>
    itemEncoder.apply(attrName, generic.to(value))
  }

case class Person(name: String, age: Int, married: Boolean, manager: Boolean)
case class IceCream(name: String, subName: String, price: Int)

val genericPerson = LabelledGeneric[Person].to(Person("bob", 37, true, true))

def encode[A](toEncode: A)(implicit itemEncoder: ItemEncoder[A]) =
  itemEncoder("", toEncode)

也许使用 ToMap 或类似的东西会更好,并将其转换为 Item

在深入探讨该主题后,我设法实现了 ItemEncoder,它将具有任意嵌套的案例 class 转换为 Item,如下所示:

import com.amazonaws.services.dynamodbv2.document.Item
import shapeless.labelled.FieldType
import shapeless.{::, HList, HNil, LabelledGeneric, Witness, _}

trait ItemEncoder[A] {
  def encode(value: A): Item
}

object ItemEncoder {
  def apply[A](implicit encoder: ItemEncoder[A]): ItemEncoder[A] = encoder

  def instance[A](f: A => Item): ItemEncoder[A] =
    new ItemEncoder[A] {
      override def encode(value: A): Item = f(value)
    }

  implicit def stringEncoder[K <: Symbol, V <: String](
    implicit witness: Witness.Aux[K]
  ): ItemEncoder[FieldType[K, V]] =
    instance { value =>
      val item = new Item
      item.withString(witness.value.name, value)
      item
    }

  implicit def intEncoder[K <: Symbol, V <: Int](
    implicit witness: Witness.Aux[K]
  ): ItemEncoder[FieldType[K, V]] =
    instance { value =>
      val item = new Item
      item.withInt(witness.value.name, value)
      item
    }

  implicit def booleanEncoder[K <: Symbol, V <: Boolean](
    implicit witness: Witness.Aux[K]
  ): ItemEncoder[FieldType[K, V]] =
    instance { value =>
      val item = new Item
      item.withBoolean(witness.value.name, value)
      item
    }

  // K is key, A is value, R is HList representation of A
  implicit def nestedClassEncoder[K <: Symbol, A, R](
    implicit
    witness: Witness.Aux[K],
    generic: LabelledGeneric.Aux[A, R],
    encoder: ItemEncoder[R]
  ): ItemEncoder[FieldType[K, A]] =
    instance { value =>
      val i = encoder.encode(generic.to(value))
      val item = new Item
      val m = new java.util.HashMap[String, Any]()
      item.withMap(witness.value.name, i.asMap())
      item
    }

  import cats.Monoid

  implicit val itemMonoid: Monoid[Item] = new Monoid[Item] {
    override def empty: Item = new Item()

    override def combine(x: Item, y: Item): Item = {
      val m = x.asMap
      m.putAll(y.asMap())
      Item.fromMap(m)
    }
  }

  implicit val hnilEncoder: ItemEncoder[HNil] =
   instance(_ => new Item())

  implicit def hlistEncoder[H, T <: HList](
    implicit
    hEncoder: Lazy[ItemEncoder[H]],
    tEncoder: ItemEncoder[T],
    monoid: Monoid[Item]
  ): ItemEncoder[H :: T] =
   instance { value =>
      //      println("hlist enc")
      val itemX = hEncoder.value.encode(value.head)
      val itemY = tEncoder.encode(value.tail)
      monoid.combine(itemX, itemY)
    }

  implicit def genericEncoder[A, R](
    implicit
    generic: LabelledGeneric.Aux[A, R],
    itemEncoder: Lazy[ItemEncoder[R]]
  ): ItemEncoder[A] =
    instance { value =>
      //      println("gen enc")
      itemEncoder.value.encode(generic.to(value))
    }

  def encode[A](toEncode: A)(implicit itemEncoder: ItemEncoder[A]) =
    itemEncoder.encode(toEncode)
}

当前的实现有点简化。因此它仅包含 ItemEncoder 实现,仅用于 StringIntBoolean 等基本类型。但其他原始类型可以很容易地添加,以现有的为例。

您可以在 Git

上找到带有 QuickCheck 测试的完整实现