我正在为Packt写一本新书,该书展示了如何使用各种Scala框架创建REST服务。 对于这本书,我正在研究以下框架列表:Finch,未过滤,Scalatra,Spray和Akka-HTTP(反应流)。
在撰写有关Finch和Unfiltered的章节时,我真的很喜欢这两个框架处理验证的方式。 它们都使用可组合的验证器,这些验证器使您可以创建易于使用的验证链,从而收集所有验证错误。
在本文中,我将向您展示如何使用Scalaz库自己使用这种方法,更具体地讲,如何将Reader monad和Validation应用程序组合在一起以创建灵活的,可指南性的验证器,从而累积任何验证错误。 作为示例,我们将创建许多Reader,可用于处理传入的HTTPRequest,获取一些参数并将其转换为case类。
入门
完整的示例可以在以下要点中找到。 本文的所有代码片段均直接从该Gist中显示。 但是,首先,对于这个项目,我们当然使用SBT。 由于我们将仅使用Scalaz,因此我们有一个非常简单的SBT构建文件:
name := "scalaz-readers"
version := "1.0"
scalaVersion := "2.11.7"
libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.1.3"
要与Scalaz一起使用,您既可以单独导入所需的软件包和功能,也可以像我们在这种情况下那样懒惰,仅导入所有内容。 因此,在示例的顶部,我们将添加以下内容:
import scalaz._
import Scalaz._
object ValidationSample extends App {
}
在看一下读者自己之前,让我们首先定义我们的案例类和一些类型别名,以使事情更容易理解:
// lets pretend this is the http request (or any other type of request)
// that can be mapped to a map of values.
type Request = Map[String, String]
val Request = Map[String, String] _
// we also define what our reader looks like. In this case
// we define that we want the result to be of type T, and our
// reader expects a Request to process. The result of this reader
// won't be a T, but will be a ValidationNel, which contains
// a list of error messages (as String) or the actual T.
type RequestReader[T] = Reader[Request, ValidationNel[String, T]]
// To experiment, lets say we want to parse the incoming request into a
// Person.
case class Person(firstName: String, lastName: String, age: Int)
读者单子
在开始创建自己的阅读器之前,让我们仔细看一下阅读器monad允许您执行的操作。 我们将在这里查看示例(对Scalaz的详尽介绍): http ://eed3si9n.com/learning-scalaz-day10
scala> def myName(step: String): Reader[String, String] = Reader {step + ", I am " + _}
myName: (step: String)scalaz.Reader[String,String]
scala> def localExample: Reader[String, (String, String, String)] = for {
a <- myName("First")
b <- myName("Second") >=> Reader { _ + "dy"}
c <- myName("Third")
} yield (a, b, c)
localExample: scalaz.Reader[String,(String, String, String)]
scala> localExample("Fred")
res0: (String, String, String) = (First, I am Fred,Second, I am Freddy,Third, I am Fred)
阅读器monad的目的是提供一些配置对象(例如HTTPRequest,DB或您可以注入的任何其他对象),而无需手动(或隐式)将其传递给所有功能。 从该示例中可以看到,我们创建了三个Reader(通过调用myName),并将请求仅传递给组合结果一次。 请注意,仅当所有阅读器均为同一类型时,才能理解。 在我们的例子中,我们有字符串和整数,因此我们使用稍有不同的语法来构成读者,我们将在后面看到。 但是,基本思想是相同的。 我们定义了一些阅读器,并传递了要处理的请求。
我们的读者
我们在帮助器对象中定义了读者,以使其使用起来更加容易。 首先让我们看完整的代码:
/**
* Object which contains our readers (or functions that create readers), just simple readers
* that check based on type.
*/
object Readers {
def keyFromMap(request: Request, key: String) = request.get(key).map(Success(_)).getOrElse(Failure(s"Key: $key Not found"))
// convert the provided validation containing a throwable, to a validation
// containing a string.
def toMessage[S](v: Validation[Throwable, S]): Validation[String, S] = {
// Returns new Functor of type self, and set value to result of provided function
((f: Throwable) => s"Exception: ${f.getClass} msg: ${f.getMessage}") <-: v
}
def as[T](key: String)(implicit to: (String) => RequestReader[T]): RequestReader[T] = {
to(key)
}
// Create a requestreader for booleanValues.
implicit def asBool(key: String): RequestReader[Boolean] = {
Reader((request: Request) => keyFromMap(request, key).flatMap({_.parseBoolean |> toMessage[Boolean] }).toValidationNel)
}
// Create a requestreader for intvalues.
implicit def asInt(key: String): RequestReader[Int] = {
Reader((request: Request) => keyFromMap(request, key).flatMap({_.parseInt |> toMessage[Int] }).toValidationNel)
}
// Create a requestreader for string values.
implicit def asString(key: String): RequestReader[String] = {
Reader((request: Request) => keyFromMap(request, key).toValidationNel)
}
}
我们在这里所做的是定义一个带有隐式to方法的as [T]方法,该方法返回指定类型的RequestReader。 通过使用隐式,scala将使用asString,AsInt等方法之一来确定如何将传入的键转换为正确的读取器。 让我们再仔细看看asInt和keyFromMap函数。
def keyFromMap(request: Request, key: String) = request.get(key).map(Success(_)).getOrElse(Failure(s"Key: $key Not found"))
// convert the provided validation containing a throwable, to a validation
// containing a string.
def toMessage[S](v: Validation[Throwable, S]): Validation[String, S] = {
// Returns new Functor of type self, and set value to result of provided function
((f: Throwable) => s"Exception: ${f.getClass} msg: ${f.getMessage}") <-: v
}
implicit def asInt(key: String): RequestReader[Int] = {
Reader((request: Request) => keyFromMap(request, key).flatMap({_.parseInt |> toMessage[Int] }).toValidationNel)
}
asInt函数创建一个新的Reader,并使用传入的请求来调用keyFromMap函数。 此函数尝试从Map中获取值,如果成功,则返回成功,如果不是失败,则返回。 接下来,我们对结果进行flatMap处理(仅在结果为Success时适用),并尝试使用标量提供的parseInt函数将找到的值转换为整数,该函数又返回一个Validation。 该函数的结果传递到toMessage函数,该函数将Validation [Throwable,S]转换为Validation [String,s]。 最后,在返回之前,我们使用toValidationNel函数将Validation [String,Int]转换为ValidationNel [String,Int]。 我们这样做是为了更轻松地将所有故障汇总在一起。
创建新的验证仅意味着创建一个返回ValidationNel [String,T]的新读取器。
撰写验证
现在让我们看看如何将验证组合在一起。 为此,我们可以像这样使用Scalaz的ApplicativeBuilder:
// This reader doesn't accumulate the validations yet, just returns them as a tuple of Validations
val toTupleReader = (as[String]("first") |@|
as[String]("last") |@|
as[Int]("age")).tupled // automatically convert to tuple
使用| @ | 符号,我们使用scalaz ApplicativeBuilder结合读者。 使用元组函数,我们会返回一个元组列表,其中包含读取器运行后的各个结果。 要运行此阅读器,我们需要向其提供一个请求:
// our sample requests. This first one is invalid,
val invalidRequest = Request(Seq(
"first" -> "Amber",
"las" -> "Dirksen",
"age" -> "20 Months"
))
// another sample request. This request is valid
val validRequest = Request(Seq(
"first" -> "Sophie",
"last" -> "Dirksen",
"age" -> "5"
))
// now we can run our readers by supplying a request.
val tuple3Invalid = toTupleReader.run(invalidRequest)
val tuple3Valid = toTupleReader.run(validRequest)
println(s"tuple3Invalid:\n $tuple3Invalid ")
println(s"tuple3valid:\n $tuple3Valid ")
这些调用的结果如下所示:
tuple3Invalid:
(Success(Amber),Failure(NonEmptyList(Key: last Not found)),Failure(NonEmptyList(Exception: class java.lang.NumberFormatException msg: For input string: "20 Months")))
tuple3valid:
(Success(Sophie),Success(Dirksen),Success(5))
尽管这种方法已经允许我们创建和编写验证并返回单个成功和失败的信息,但是仍然需要花费一些工作来获取失败或将值转换为我们的案例类。 不过幸运的是,由于我们使用ValidationNel对象,因此我们也可以轻松收集成功和失败的信息:
// This reader converts to a Success(person) or a failure Nel
val toPersonReader = (as[String]("first") |@|
as[String]("last") |@|
as[Int]("age"))
.apply((a, b, c) => (a |@| b |@| c ).apply(Person) ) // manually convert to case class
运行此阅读器时,将应用每个单独的验证,并将其传递到提供的apply函数中。 在此功能中,我们使用| @ |收集验证。 构造函数。 这将返回包含收集的错误的“失败”,或者如果所有验证均成功,则返回实例化的人:
val personInvalid = toPersonReader.run(invalidRequest)
val personValid = toPersonReader.run(validRequest)
println(s"personInvalid:\n $personInvalid ")
println(s"personValid:\n $personValid ")
结果是:
personInvalid:
Failure(NonEmptyList(Key: last Not found, Exception: class java.lang.NumberFormatException msg: For input string: "20 Months"))
personValid:
Success(Person(Sophie,Dirksen,5))
酷吧! 这样,我们要么获得包含域对象的成功,要么获得包含错误列表的单个Failure对象。
我想展示的最后一部分是收集验证的另一种方法。 在前面的示例中,我们使用了| @ |。 语法,您也可以直接创建一个Applicative并将其用于收集验证:
// we can further process the tuple using an applicative builder |@|, or
// we can use the Applicative.apply function like this:
// we need to use a type lambda, since we use a higher order function
val V = Applicative[({type λ[α]=ValidationNel[String, α]})#λ]
val appValid: ValidationNel[String, Person] = V.apply3(tuple3Valid._1, tuple3Valid._2, tuple3Valid._3)(Person)
val appInvalid: ValidationNel[String, Person] = V.apply3(tuple3Invalid._1, tuple3Invalid._2, tuple3Invalid._3)(Person)
println(s"applicativeInvalid:\n $appInvalid")
println(s"applicativeValid:\n $appValid")
这个函数的输出是这样的:
applicativeInvalid:
Failure(NonEmptyList(Key: last Not found, Exception: class java.lang.NumberFormatException msg: For input string: "20 Months"))
applicativeValid:
Success(Person(Sophie,Dirksen,5))
就是这样。 总结要点:
- 使用阅读器模式,很容易将一些上下文传递给函数进行处理。
- 与应用程序构建器或| @ | 符号,即使第一个失败了,您也可以轻松组成继续阅读的读者。
- 通过使用validationNel,我们可以轻松地收集各种验证结果,并返回收集的错误或返回一次成功。