带有案例类的 Scala 无形类型 Map[Symbol, String]

标签 scala shapeless

我正在读取查询参数并将它们转换为 Map[Symbol, String] .我想通过一组案例类为这些查询参数添加一些类型安全。

这些案例类会根据传入的 http 请求而有所不同,因此这需要支持不同的案例类。

如果传入的查询参数与定义的 case class 不匹配Parser应该返回 None .

我尝试使用 shapeless 来实现通用解析器。如果所有参数都属于 String 类型,则有效.但我需要支持任何类型的查询参数。

我试图合并在这篇文章中看到的隐式转换逻辑,但无法让它工作。
https://meta.plasm.us/posts/2015/11/08/type-classes-and-generic-derivation/ (新到无形)

现有 Parser (没有字符串到类型转换):

class Parser[A] {
  def from[R <: HList]
  (m: Map[Symbol, String])
  (implicit
   gen: LabelledGeneric.Aux[A, R],
   fromMap: FromMap[R]
  ): Option[A] = fromMap(m).map(gen.from)
}

object Parser {
  def to[A]: Parser[A] = new Parser[A]
}

描述问题的测试:

class ParserSpec extends FlatSpec with Matchers {
  private val sampleName: String = "Bob"
  private val sampleVersion: Int = 1

  //Partial Solution
  case class QueryParams(name: String, version: String)

  //Full Solution (not working)
  case class QueryParams2(name: String, version: Int)

  "A Parser" should "parse query parameters from a map with only string values" in {
    val mapOfQueryParams = Map('name -> sampleName, 'version -> sampleVersion.toString)
    val result = Parser.to[QueryParams].from(mapOfQueryParams)

    result shouldBe 'defined
    result.get.name shouldEqual sampleName
    result.get.version shouldEqual sampleVersion.toString
  }
  it should "parse query parameters from a map with any type of value" in {
    val mapOfQueryParams = Map('name -> sampleName, 'version -> sampleVersion.toString)
    val result = Parser.to[QueryParams2].from(mapOfQueryParams)

    //result is not defined as it's not able to convert a string to integer
    result shouldBe 'defined
    result.get.name shouldEqual sampleName
    result.get.version shouldEqual sampleVersion
  }
}

最佳答案

FromMap用途 shapeless.Typeable将值转换为预期类型。所以让你的代码工作的最简单的方法是定义一个 Typeable 的实例。转换自 StringInt (以及出现在案例类中的任何值类型的其他 Typeable 实例):

implicit val stringToInt: Typeable[Int] = new Typeable[Int] {
  override def cast(t: Any): Option[Int] = t match {
    case t: String => Try(t.toInt).toOption
    case _ => Typeable.intTypeable.cast(t)
  }

  override def describe: String = "Int from String"
}

然而,这不是 Typeable 的预期用途,旨在确认类型为 Any 的变量已经是没有任何转换的预期类型的​​实例。换句话说,它旨在成为 asInstanceOf 的类型安全实现。 ,这也可以解决类型删除问题。

为了正确起见,您可以定义自己的 ReadFromMap类型类,使用您自己的 Read用于从 String 转换的类型类s 到预期的类型。这是 Read 的简单实现类型类(假设 Scala 2.12):
import scala.util.Try

trait Read[T] {
  def apply(string: String): Option[T]
}

object Read {
  implicit val readString: Read[String] = Some(_)
  implicit val readInt: Read[Int] = s => Try(s.toInt).toOption
  // Add more implicits for other types in your case classes
}

您可以复制和改编 FromMap 的实现。使用此 Read类型类:
import shapeless._
import shapeless.labelled._

trait ReadFromMap[R <: HList] extends Serializable {
  def apply(map: Map[Symbol, String]): Option[R]
}

object ReadFromMap {
  implicit def hnil: ReadFromMap[HNil] = _ => Some(HNil)

  implicit def hlist[K <: Symbol, V, T <: HList](implicit
    keyWitness: Witness.Aux[K],
    readValue: Read[V],
    readRest: ReadFromMap[T]
  ): ReadFromMap[FieldType[K, V] :: T] = map => for {
    value <- map.get(keyWitness.value)
    converted <- readValue(value)
    rest <- readRest(map)
  } yield field[K](converted) :: rest
}

然后只需在您的 Parser 中使用这个新类型类:
class Parser[A] {
  def from[R <: HList]
  (m: Map[Symbol, String])
  (implicit
    gen: LabelledGeneric.Aux[A, R],
    fromMap: ReadFromMap[R]
  ): Option[A] = fromMap(m).map(gen.from)
}

关于带有案例类的 Scala 无形类型 Map[Symbol, String],我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56638344/

相关文章:

scala - 在Gradle中设置Scaladoc页眉/页脚

java - 删除两个重复行

scala - Shapeless:无法找到隐含的余积映射

scala - 使用 Generic 从嵌套案例类中收集数据

java - Apache Spark 启动时出错

scala - 返回存在类型有意义吗?

Scala Spark - 将元组的值映射到单个值

scala - 将 HList 转换为列表列表

scala - 拆分使用Prepend [A,B]串联的HList

Scala 类型和编译