我们可以组合一个有效的实例和一组无效的字段(这样every字段,如果复制,将导致验证失败)使用获取无效对象无形的 https://github.com/milessabin/shapeless图书馆。
Shapeless 允许您将类表示为键值对列表,这些键值对仍然是强类型的并支持一些高级操作,并从这种表示形式转换回原始类。
在下面的示例中,我将为提供的每个字段提供一个无效实例
import shapeless._, record._
import shapeless.labelled.FieldType
import shapeless.ops.record.Updater
详细介绍
假设我们有一个数据类和它的一个有效实例(我们只需要一个,因此可以对其进行硬编码)
case class User(id: String, name: String, about: String, age: Int) {
def isValid = id.length == 3 && name.nonEmpty && age >= 0
}
val someValidUser = User("oo7", "Frank", "A good guy", 42)
assert(someValidUser.isValid)
然后我们可以定义一个用于无效值的类:
case class BogusUserFields(name: String, id: String, age: Int)
val bogusData = BogusUserFields("", "1234", -5)
可以使用以下方式提供此类的实例Scala检查。编写一个所有字段都会导致失败的生成器要容易得多。字段的顺序并不重要,重要的是它们的名称和类型。这里我们排除了about
from User
一组字段,以便我们可以执行您所要求的操作(仅提供您想要测试的字段的子集)
然后我们使用LabelledGeneric[T]
转换User
and BogusUserFields
到它们对应的记录值(稍后我们将转换User
back)
val userLG = LabelledGeneric[User]
val bogusLG = LabelledGeneric[BogusUserFields]
val validUserRecord = userLG.to(someValidUser)
val bogusRecord = bogusLG.to(bogusData)
记录是键值对的列表,因此我们可以使用head
获得单个映射,并且+
运算符支持向另一条记录添加/替换字段。让我们一次将每个无效字段挑选到我们的用户中。另外,这里是转换回来的动作:
val invalidUser1 = userLG.from(validUserRecord + bogusRecord.head)// invalid name
val invalidUser2 = userLG.from(validUserRecord + bogusRecord.tail.head)// invalid ID
val invalidUser3 = userLG.from(validUserRecord + bogusRecord.tail.tail.head) // invalid age
assert(List(invalidUser1, invalidUser2, invalidUser3).forall(!_.isValid))
因为我们基本上应用相同的函数(validUserRecord + _
)到我们的每个键值对bogusRecord
,我们还可以使用map
运算符,除非我们将它与不寻常的多态函数一起使用。我们也可以很容易地将其转换为List
,因为现在每个元素都将具有相同的类型。
object polymerge extends Poly1 {
implicit def caseField[K, V](implicit upd: Updater[userLG.Repr, FieldType[K, V]]) =
at[FieldType[K, V]](upd(validUserRecord, _))
}
val allInvalidUsers = bogusRecord.map(polymerge).toList.map(userLG.from)
assert(allInvalidUsers == List(invalidUser1, invalidUser2, invalidUser3))
概括并删除所有样板
现在的重点是我们可以将其推广到适用于任意两个任意类。所有关系和操作的编码有点麻烦,我花了一段时间才正确处理所有关系和操作implicit not found
错误,所以我将跳过细节。
class Picks[A, AR <: HList](defaults: A)(implicit lgA: LabelledGeneric.Aux[A, AR]) {
private val defaultsRec = lgA.to(defaults)
object mergeIntoTemplate extends Poly1 {
implicit def caseField[K, V](implicit upd: Updater[AR, FieldType[K, V]]) =
at[FieldType[K, V]](upd(defaultsRec, _))
}
def from[B, BR <: HList, MR <: HList, F <: Poly](options: B)
(implicit
optionsLG: LabelledGeneric.Aux[B, BR],
mapper: ops.hlist.Mapper.Aux[mergeIntoTemplate.type, BR, MR],
toList: ops.hlist.ToTraversable.Aux[MR, List, AR]
) = {
optionsLG.to(options).map(mergeIntoTemplate).toList.map(lgA.from)
}
}
所以,它正在发挥作用:
val cp = new Picks(someValidUser)
assert(cp.from(bogusData) == allInvalidUsers)
不幸的是,你不能写new Picks(someValidUser).from(bogusData)
因为隐式的mapper
需要一个稳定的标识符。另一方面,cp
实例可以与其他类型重用:
case class BogusName(name: String)
assert(cp.from(BogusName("")).head == someValidUser.copy(name = ""))
现在它适用于所有类型!并且虚假数据必须是类字段的任何子集,因此它甚至适用于类本身
case class Address(country: String, city: String, line_1: String, line_2: String) {
def isValid = Seq(country, city, line_1, line_2).forall(_.nonEmpty)
}
val acp = new Picks(Address("Test country", "Test city", "Test line 1", "Test line 2"))
val invalidAddresses = acp.from(Address("", "", "", ""))
assert(invalidAddresses.forall(!_.isValid))
你可以看到代码运行在斯卡拉小提琴 https://scalafiddle.io/sf/rB64yOO/0