Scala 无形输入 Map[Symbol, String] with case 类
Scala shapeless typing Map[Symbol, String] with case classes
我正在读取查询参数并将它们转换为 Map[Symbol, String]
。我想通过一组 case 类 为这些查询参数添加一些类型安全。
这些大小写类会根据传入的http请求而有所不同,所以这需要支持不同的大小写类。
如果传入的查询参数与定义的 case class
不匹配,Parser
应该 return None
.
我曾尝试使用 shapeless 来实现通用解析器。如果所有参数都是 String
类型,它就可以工作。但是我需要支持任何类型的查询参数。
我尝试合并此 post 中看到的隐式转换逻辑,但无法使其正常工作。
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
的实例以从 String
转换为 Int
(以及任何值类型的附加 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
转换为预期类型。这是 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)
}
我正在读取查询参数并将它们转换为 Map[Symbol, String]
。我想通过一组 case 类 为这些查询参数添加一些类型安全。
这些大小写类会根据传入的http请求而有所不同,所以这需要支持不同的大小写类。
如果传入的查询参数与定义的 case class
不匹配,Parser
应该 return None
.
我曾尝试使用 shapeless 来实现通用解析器。如果所有参数都是 String
类型,它就可以工作。但是我需要支持任何类型的查询参数。
我尝试合并此 post 中看到的隐式转换逻辑,但无法使其正常工作。 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
的实例以从 String
转换为 Int
(以及任何值类型的附加 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
转换为预期类型。这是 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)
}