diff --git a/docs/book/ReleaseNotes.html b/docs/book/ReleaseNotes.html new file mode 100644 index 00000000..6ad4d7e7 --- /dev/null +++ b/docs/book/ReleaseNotes.html @@ -0,0 +1,429 @@ + + + +
+Validation 2.0 supports Scala.js, which allows compiling validation logic for JavaScript to run it directly in the browser. Let's begin by playing with it. Try to change the tryMe
variable in the following editor. The result is automatically outputted.
Using validation from Scala.js is no different than any other Scala library. There is, however, some friction to integrate Scala.js into an existing Play + JavaScript, which we try to address in this document. Assuming no prior knowledge on Scala.js, we explain how to cross-compile and integrate validation logic into an existing Play/JavaScript application.
+You will first need to add two SBT plugins, Scala.js itself and sbt-play-scalajs
to make it Scala.js and Play coexist nicely:
scala> cat("project/plugins.sbt")
+addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.2")
+addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.9")
+addSbtPlugin("com.vmunier" % "sbt-play-scalajs" % "0.3.0")
+
+Scala.js uses a separate compilation pass to transform Scala sources to a single .js
file. Specifying which part of a Scala codebase should be processed by Scala.js is done by splitting the code in different SBT projects. This is usually done with 3 projects, one targeting the JVM, another one targeting JS, and a third one for code shared between the two. In case of a Play application it could look like the following:
<project root>
+ +- build.sbt
+ +- jvm
+ | +- app
+ | +- conf
+ | +- public
+ | +- test
+ +- js
+ | +- src/main/scala
+ +- shared
+ +- src/main/scala
+
Now let's look at a minimal build.sbt
reflecting this structure. Information on the sbt settings are available on the Scala.js documentation on cross build, and on sbt-play-scalajs
documentation.
scala> cat("build.sbt")
+val scalaV = "2.11.8"
+
+val validationVersion = "2.0"
+
+lazy val jvm = project
+ .in(file("jvm"))
+ .settings(
+ scalaVersion := scalaV,
+ scalaJSProjects := Seq(js),
+ pipelineStages := Seq(scalaJSProd),
+ libraryDependencies ++= Seq(
+ "com.vmunier" %% "play-scalajs-scripts" % "0.5.0",
+ "io.github.jto" %% "validation-core" % validationVersion,
+ "io.github.jto" %% "validation-playjson" % validationVersion,
+ "io.github.jto" %% "validation-jsonast" % validationVersion))
+ .enablePlugins(PlayScala)
+ .aggregate(js)
+ .dependsOn(sharedJVM)
+
+lazy val js = project
+ .in(file("js"))
+ .settings(
+ scalaVersion := scalaV,
+ persistLauncher := true,
+ libraryDependencies ++= Seq(
+ "io.github.jto" %%% "validation-core" % validationVersion,
+ "io.github.jto" %%% "validation-jsjson" % validationVersion,
+ "io.github.jto" %%% "validation-jsonast" % validationVersion))
+ .enablePlugins(ScalaJSPlugin, ScalaJSPlay)
+ .dependsOn(sharedJS)
+
+lazy val sharedJVM = shared.jvm
+lazy val sharedJS = shared.js
+lazy val shared = crossProject.crossType(CrossType.Pure)
+ .in(file("shared"))
+ .settings(
+ scalaVersion := scalaV,
+ libraryDependencies ++= Seq(
+ "io.github.jto" %%% "validation-core" % validationVersion,
+ "io.github.jto" %%% "validation-jsonast" % validationVersion))
+ .jsConfigure(_.enablePlugins(ScalaJSPlay))
+
+onLoad in Global := (Command.process("project jvm", _: State)) compose (onLoad in Global).value
+
+In addition to the validation
dependency, we also included play-scalajs-scripts
, which provides a convenient way to link the output of Scala.js compilation from a Play template:
scala> cat("jvm/app/views/main.scala.html")
+@(title: String)(content: Html)(implicit environment: play.api.Environment)
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>@title</title>
+ </head>
+ <body>
+ @content
+ @* Outputs a <script></script> tag to include the output of Scala.js compilation. *@
+ @playscalajs.html.scripts(projectName = "js")
+ </body>
+</html>
+
+Let's define a simple case class for our example inside of the shared
project to make it available to both JVM and JV platforms. We collocate a simple validation for this case class in its companion object:
scala> cat("shared/src/main/scala/User.scala")
+package model
+
+import jto.validation._
+import jto.validation.jsonast._
+import scala.Function.unlift
+
+case class User(
+ name: String,
+ age: Int,
+ email: Option[String],
+ isAlive: Boolean
+)
+
+object User {
+ import Rules._, Writes._
+ implicit val format: Format[JValue, JObject, User] =
+ Formatting[JValue, JObject] { __ =>
+ (
+ (__ \ "name").format(notEmpty) ~
+ (__ \ "age").format(min(0) |+| max(130)) ~
+ (__ \ "email").format(optionR(email), optionW(stringW)) ~
+ (__ \ "isAlive").format[Boolean]
+ )(User.apply, unlift(User.unapply))
+ }
+}
+
+Note the use of jto.validation.jsonast
here. This project implements in just a few lines of code an immutable version of the JSON specification based on Scala collections: (It might eventually be replaced with an external abstract syntax tree (AST), see discussion in https://github.com/scala/slip/pull/28)
scala> cat("../validation-jsonast/shared/src/main/scala/JValue.scala")
+package jto.validation
+package jsonast
+
+sealed trait JValue
+case object JNull extends JValue
+case class JObject (value: Map[String, JValue] = Map.empty) extends JValue
+case class JArray (value: Seq[JValue] = Seq.empty) extends JValue
+case class JBoolean(value: Boolean) extends JValue
+case class JString (value: String) extends JValue
+case class JNumber (value: String) extends JValue {
+ require(JNumber.regex.matcher(value).matches)
+}
+
+object JNumber {
+ val regex = """-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?""".r.pattern
+ def apply(i: Int): JNumber = JNumber(i.toString)
+ def apply(l: Long): JNumber = JNumber(l.toString)
+ def apply(d: Double): JNumber = JNumber(d.toString)
+}
+
+This AST has the same capabilities than other JSON representations, but it does no provide a parser nor a pretty printer. The suggested approach here is to use conversions from this cross compiled AST to platform specific ones to take advantage of existing platform specific serialization. To do so, Validation provides the following Rule
s and Write
s, defined in jto.validation.jsonast
:
Ast.from: Rule[play.api.libs.json.JsValue, JValue]
Ast.to: Write[JValue, play.api.libs.json.JsValue]
Ast.from: Rule[scala.scalajs.jsDynamic, JValue]
Ast.to: Write[JValue, scala.scalajs.jsDynamic]
To use our previously defined validation, we could compose what we defined targeting the cross compiling JSON AST with the above Rule
s / Write
s to finally obtain platform-specific validation.
One last technicality about Scala.js is the @JSExport
annotation, which is used to explicitly expose Scala objects and methods to the javascript world. To complete our example, we define and expose a single method taking a JSON representation of our case class and returning the output of our validation, also a JSON:
scala> cat("js/src/main/scala/Validate.scala")
+package client
+
+import jto.validation._
+import jto.validation.jsonast.Ast
+import jto.validation.jsjson._
+import scala.scalajs.js
+import js.annotation.JSExport
+import model.User
+import scala.Function.{unlift, const}
+
+@JSExport
+object Validate {
+ @JSExport
+ def user(json: js.Dynamic): js.Dynamic = {
+ import Writes._
+
+ implicit val format: Format[js.Dynamic, js.Dynamic, User] = Format(
+ Ast.from andThen User.format,
+ Write.toWrite(User.format) andThen Ast.to
+ )
+
+ To[VA[User], js.Dynamic](format.validate(json))
+ }
+}
+
+Finally, we can create a simple view with a textarea which validates it's content on every keystroke:
+scala> cat("jvm/app/views/index.scala.html")
+@(json: String)(implicit environment: play.api.Environment)
+
+@main("Play Scala.js Validation") {
+ <textarea id="json-form" rows="10" cols="40">@json</textarea>
+ <pre id="validation-output"></pre>
+}
+
+<script type="text/javascript">
+ var validationOutputPre = document.getElementById("validation-output")
+ var jsonFormTextarea = document.getElementById("json-form")
+
+ var demo = function() {
+ try {
+ var json = JSON.parse(jsonFormTextarea.value);
+ validationOutputPre.innerHTML =
+ JSON.stringify(client.Validate().user(json), null, 2);
+ } catch(err) {
+ validationOutputPre.innerHTML = err.message;
+ }
+ };
+
+ jsonFormTextarea.addEventListener('input', demo, false);
+ demo();
+</script>
+
+This complete code of this example is available in the play-scalajs-example subproject. The binary used to power the editor at the beginning of this page was generated by running Play in production mode, which fully optimizes the output of Scala.js compilation using the Google Closure Compiler to obtain a final .js file under 100KB once gzipped.
+ + +++All the examples below are validating Json objects. The API is not dedicated only to Json, it can be used on any type. Please refer to Validating Json, Validating Forms, and Supporting new types for more information.
+
Rule
import jto.validation._
+import play.api.libs.json._
+
+case class Creature(
+ name: String,
+ isDead: Boolean,
+ weight: Float)
+
+implicit val creatureRule: Rule[JsValue, Creature] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ ((__ \ "name").read[String] ~
+ (__ \ "isDead").read[Boolean] ~
+ (__ \ "weight").read[Float])(Creature.apply)
+}
+
+scala> val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0f)
+js: play.api.libs.json.JsObject = {"name":"gremlins","isDead":false,"weight":1}
+
+scala> From[JsValue, Creature](js)
+res2: jto.validation.VA[Creature] = Valid(Creature(gremlins,false,1.0))
+
+scala> From[JsValue, Creature](Json.obj())
+res3: jto.validation.VA[Creature] = Invalid(List((/name,List(ValidationError(List(error.required),WrappedArray()))), (/isDead,List(ValidationError(List(error.required),WrappedArray()))), (/weight,List(ValidationError(List(error.required),WrappedArray())))))
+
+A common example of this use case is the validation of password
and password confirmation
fields in a signup form.
import jto.validation._
+import play.api.libs.json._
+
+val passRule: Rule[JsValue, String] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ // This code creates a `Rule[JsValue, (String, String)]` each of of the String must be non-empty
+ ((__ \ "password").read(notEmpty) ~
+ (__ \ "verify").read(notEmpty)).tupled
+ // We then create a `Rule[(String, String), String]` validating that given a `(String, String)`,
+ // both strings are equals. Those rules are then composed together.
+ .andThen(Rule.uncurry(equalTo[String])
+ // In case of `Invalid`, we want to control the field holding the errors.
+ // We change the `Path` of errors using `repath`
+ .repath(_ => (Path \ "verify"))
+ )
+}
+
+Let's test it:
+scala> passRule.validate(Json.obj("password" -> "foo", "verify" -> "foo"))
+res5: jto.validation.VA[String] = Valid(foo)
+
+scala> passRule.validate(Json.obj("password" -> "", "verify" -> "foo"))
+res6: jto.validation.VA[String] = Invalid(List((/password,List(ValidationError(List(error.required),WrappedArray())))))
+
+scala> passRule.validate(Json.obj("password" -> "foo", "verify" -> ""))
+res7: jto.validation.VA[String] = Invalid(List((/verify,List(ValidationError(List(error.required),WrappedArray())))))
+
+scala> passRule.validate(Json.obj("password" -> "", "verify" -> ""))
+res8: jto.validation.VA[String] = Invalid(List((/password,List(ValidationError(List(error.required),WrappedArray()))), (/verify,List(ValidationError(List(error.required),WrappedArray())))))
+
+scala> passRule.validate(Json.obj("password" -> "foo", "verify" -> "bar"))
+res9: jto.validation.VA[String] = Invalid(List((/verify,List(ValidationError(List(error.equals),WrappedArray(foo))))))
+
+When validating recursive types:
+lazy
keyword to allow forward reference.Rule
must be explicitly given.case class User(
+ name: String,
+ age: Int,
+ email: Option[String],
+ isAlive: Boolean,
+ friend: Option[User])
+
+import jto.validation._
+import play.api.libs.json._
+
+// Note the lazy keyword
+implicit lazy val userRule: Rule[JsValue, User] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+
+ ((__ \ "name").read[String] ~
+ (__ \ "age").read[Int] ~
+ (__ \ "email").read[Option[String]] ~
+ (__ \ "isAlive").read[Boolean] ~
+ (__ \ "friend").read[Option[User]])(User.apply)
+}
+
+or using macros:
+scala> import jto.validation._
+import jto.validation._
+
+scala> import play.api.libs.json._
+import play.api.libs.json._
+
+scala> import jto.validation.playjson.Rules._
+import jto.validation.playjson.Rules._
+
+scala> // Note the lazy keyword, and the explicit typing
+ | implicit lazy val userRule: Rule[JsValue, User] = Rule.gen[JsValue, User]
+userRule: jto.validation.Rule[play.api.libs.json.JsValue,User] = <lazy>
+
+import jto.validation._
+import play.api.libs.json._
+
+val js = Json.parse("""
+{
+ "values": [
+ { "foo": "bar" },
+ { "bar": "baz" }
+ ]
+}
+""")
+
+val r: Rule[JsValue, Seq[(String, String)]] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+
+ val tupleR: Rule[JsValue, (String, String)] = Rule.fromMapping[JsValue, (String, String)] {
+ case JsObject(Seq((key, JsString(value)))) => Valid(key.toString -> value)
+ case _ => Invalid(Seq(ValidationError("BAAAM")))
+ }
+
+ (__ \ "values").read(seqR(tupleR))
+}
+
+scala> r.validate(js)
+res15: jto.validation.VA[Seq[(String, String)]] = Invalid(List((/values[0],List(ValidationError(List(BAAAM),WrappedArray()))), (/values[1],List(ValidationError(List(BAAAM),WrappedArray())))))
+
+Consider the following class definitions:
+trait A
+case class B(foo: Int) extends A
+case class C(bar: Int) extends A
+
+val b = Json.obj("name" -> "B", "foo" -> 4)
+val c = Json.obj("name" -> "C", "bar" -> 6)
+val e = Json.obj("name" -> "E", "eee" -> 6)
+
+import cats.syntax.cartesian._
+
+val rb: Rule[JsValue, A] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ (__ \ "name").read(equalTo("B")) *> (__ \ "foo").read[Int].map(B.apply)
+}
+
+val rc: Rule[JsValue, A] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ (__ \ "name").read(equalTo("C")) *> (__ \ "bar").read[Int].map(C.apply)
+}
+
+val typeInvalid = Invalid(Seq(Path -> Seq(ValidationError("validation.unknownType"))))
+val rule = rb orElse rc orElse Rule(_ => typeInvalid)
+
+scala> rule.validate(b)
+res20: jto.validation.VA[A] = Valid(B(4))
+
+scala> rule.validate(c)
+res21: jto.validation.VA[A] = Valid(C(6))
+
+scala> rule.validate(e)
+res22: jto.validation.VA[A] = Invalid(List((/,List(ValidationError(List(validation.unknownType),WrappedArray())))))
+
+val typeInvalid = Invalid(Seq(Path -> Seq(ValidationError("validation.unknownType"))))
+
+val rule: Rule[JsValue, A] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ (__ \ "name").read[String].flatMap[A] {
+ case "B" => (__ \ "foo").read[Int].map(B.apply _)
+ case "C" => (__ \ "bar").read[Int].map(C.apply _)
+ case _ => Rule(_ => typeInvalid)
+ }
+}
+
+scala> rule.validate(b)
+res24: jto.validation.VA[A] = Valid(B(4))
+
+scala> rule.validate(c)
+res25: jto.validation.VA[A] = Valid(C(6))
+
+scala> rule.validate(e)
+res26: jto.validation.VA[A] = Invalid(List((/,List(ValidationError(List(validation.unknownType),WrappedArray())))))
+
+Write
Write
import jto.validation._
+import play.api.libs.json._
+import scala.Function.unlift
+
+case class Creature(
+ name: String,
+ isDead: Boolean,
+ weight: Float)
+
+implicit val creatureWrite = To[JsObject] { __ =>
+ import jto.validation.playjson.Writes._
+ ((__ \ "name").write[String] ~
+ (__ \ "isDead").write[Boolean] ~
+ (__ \ "weight").write[Float])(unlift(Creature.unapply))
+}
+
+scala> To[Creature, JsObject](Creature("gremlins", false, 1f))
+res29: play.api.libs.json.JsObject = {"name":"gremlins","isDead":false,"weight":1}
+
+Write
import jto.validation._
+import play.api.libs.json._
+
+case class LatLong(lat: Float, long: Float)
+
+implicit val latLongWrite = {
+ import jto.validation.playjson.Writes._
+ To[JsObject] { __ =>
+ ((__ \ "lat").write[Float] ~
+ (__ \ "long").write[Float])(unlift(LatLong.unapply))
+ }
+}
+
+case class Point(coords: LatLong)
+
+implicit val pointWrite = {
+ import jto.validation.playjson.Writes._
+ To[JsObject] { __ =>
+ ((__ \ "coords").write[LatLong] ~
+ (__ \ "type").write[String]) ((_: Point).coords -> "point")
+ }
+}
+
+scala> val p = Point(LatLong(123.3F, 334.5F))
+p: Point = Point(LatLong(123.3,334.5))
+
+scala> pointWrite.writes(p)
+res34: play.api.libs.json.JsObject = {"coords":{"lat":123.3,"long":334.5},"type":"point"}
+
+
+
+ The validation API is designed to be easily extensible. Supporting new types is just a matter of providing the appropriate set of Rules and Writes.
+In this documentation, we'll study the implementation of the Json support. All extensions are to be defined in a similar fashion. The total amount of code needed is rather small, but there're best practices you need to follow.
+The first step is to define what we call primitive rules. Primitive rules is a set of rules on which you could build any complex validation.
+The base of all Rules is the capacity to extract a subset of some input data.
+For the type JsValue
, we need to be able to extract a JsValue
at a given Path
:
scala> import jto.validation._
+import jto.validation._
+
+scala> import play.api.libs.json.{KeyPathNode => JSKeyPathNode, IdxPathNode => JIdxPathNode, _}
+import play.api.libs.json.{KeyPathNode=>JSKeyPathNode, IdxPathNode=>JIdxPathNode, _}
+
+scala> object Ex1 {
+ |
+ | def pathToJsPath(p: Path) =
+ | play.api.libs.json.JsPath(p.path.map{
+ | case KeyPathNode(key) => JSKeyPathNode(key)
+ | case IdxPathNode(i) => JIdxPathNode(i)
+ | })
+ |
+ | implicit def pickInJson(p: Path): Rule[JsValue, JsValue] =
+ | Rule[JsValue, JsValue] { json =>
+ | pathToJsPath(p)(json) match {
+ | case Nil => Invalid(Seq(Path -> Seq(ValidationError("error.required"))))
+ | case js :: _ => Valid(js)
+ | }
+ | }
+ | }
+defined object Ex1
+
+Now we are able to do this:
+scala> {
+ | import Ex1._
+ |
+ | val js = Json.obj(
+ | "field1" -> "alpha",
+ | "field2" -> 123L,
+ | "field3" -> Json.obj(
+ | "field31" -> "beta",
+ | "field32"-> 345))
+ |
+ | val pick: Rule[JsValue, JsValue] = From[JsValue] { __ =>
+ | (__ \ "field2").read[JsValue]
+ | }
+ |
+ | pick.validate(js)
+ | }
+res0: jto.validation.VA[play.api.libs.json.JsValue] = Valid(123)
+
+Which is nice, but is would be much more convenient if we could extract that value as an Int
.
One solution is to write the following method:
+implicit def pickIntInJson[O](p: Path): Rule[JsValue, JsValue] = ???
+
+But we would end up copying 90% of the code we already wrote.
+Instead of doing so, we're going to make pickInJson
a bit smarter by adding an implicit parameter:
implicit def pickInJson[O](p: Path)(implicit r: Rule[JsValue, O]): Rule[JsValue, O] =
+ Rule[JsValue, JsValue] { json =>
+ pathToJsPath(p)(json) match {
+ case Nil => Invalid(Seq(Path -> Seq(ValidationError("error.required"))))
+ case js :: _ => Valid(js)
+ }
+ }.andThen(r)
+
+The now all we have to do is to write a Rule[JsValue, O]
, and we automatically get the Path => Rule[JsValue, O]
we're interested in. The rest is just a matter of defining all the primitives rules, for example:
scala> def jsonAs[T](f: PartialFunction[JsValue, Validated[Seq[ValidationError], T]])(args: Any*) =
+ | Rule.fromMapping[JsValue, T](
+ | f.orElse{ case j => Invalid(Seq(ValidationError("validation.invalid", args: _*)))
+ | })
+jsonAs: [T](f: PartialFunction[play.api.libs.json.JsValue,jto.validation.Validated[Seq[jto.validation.ValidationError],T]])(args: Any*)jto.validation.Rule[play.api.libs.json.JsValue,T]
+
+scala> def stringRule = jsonAs[String] {
+ | case JsString(v) => Valid(v)
+ | }("String")
+stringRule: jto.validation.Rule[play.api.libs.json.JsValue,String]
+
+scala> def booleanRule = jsonAs[Boolean]{
+ | case JsBoolean(v) => Valid(v)
+ | }("Boolean")
+booleanRule: jto.validation.Rule[play.api.libs.json.JsValue,Boolean]
+
+The types you generally want to support natively are:
+Supporting primitives is nice, but not enough. Users are going to deal with Seq
and Option
. We need to support those types too.
What we want to do is to implement a function that takes a Path => Rule[JsValue, O]
, an lift it into a Path => Rule[JsValue, Option[O]]
for any type O
. The reason we're working on the fully defined Path => Rule[JsValue, O]
and not just Rule[JsValue, O]
is because a non-existent Path
must be validated as a Valid(None)
. If we were to use pickInJson
on a Rule[JsValue, Option[O]]
, we would end up with an Invalid
in the case of non-existing Path
.
The play.api.data.mapping.DefaultRules[I]
traits provides a helper for building the desired method. It's signature is:
protected def opt[J, O](r: => Rule[J, O], noneValues: Rule[I, I]*)(implicit pick: Path => Rule[I, I], coerce: Rule[I, J]): Path = Rule[I, O]
+
+noneValues
is a List of all the values we should consider to be None
. For Json that would be JsNull
.pick
is an extractor. It's going to extract a subtree.coerce
is a type conversion Rule
r
is a Rule
to be applied to the data if they are foundAll you have to do is to use this method to implement a specialized version for your type. +For example it's defined this way for Json:
+def optionR[J, O](r: => Rule[J, O], noneValues: Rule[JsValue, JsValue]*)(implicit pick: Path => Rule[JsValue, JsValue], coerce: Rule[JsValue, J]): Path => Rule[JsValue, Option[O]]
+ = super.opt[J, O](r, (jsNull.map(n => n: JsValue) +: noneValues):_*)
+
+Basically it's just the same, but we are now only supporting JsValue
. We are also adding JsNull is the list of None-ish values.
Despite the type signature funkiness, this function is actually really simple to use:
+val maybeEmail: Rule[JsValue, Option[String]] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ (__ \ "email").read(optionR(email))
+}
+
+scala> maybeEmail.validate(Json.obj("email" -> "foo@bar.com"))
+res1: jto.validation.VA[Option[String]] = Valid(Some(foo@bar.com))
+
+scala> maybeEmail.validate(Json.obj("email" -> "baam!"))
+res2: jto.validation.VA[Option[String]] = Invalid(List((/email,List(ValidationError(List(error.email),WrappedArray())))))
+
+scala> maybeEmail.validate(Json.obj("email" -> JsNull))
+res3: jto.validation.VA[Option[String]] = Valid(None)
+
+scala> maybeEmail.validate(Json.obj())
+res4: jto.validation.VA[Option[String]] = Valid(None)
+
+Alright, so now we can explicitly define rules for optional data.
+But what if we write (__ \ "age").read[Option[Int]]
? It does not compile!
+We need to define an implicit rule for that:
implicit def option[O](p: Path)(implicit pick: Path => Rule[JsValue, JsValue], coerce: Rule[JsValue, O]): Rule[JsValue, Option[O]] =
+ option(Rule.zero[O])(pick, coerce)(p)
+
+val maybeAge: Rule[JsValue, Option[Int]] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ (__ \ "age").read[Option[Int]]
+}
+
+It's very important that every Rule is completely lazily evaluated . The reason for that is that you may be validating recursive types:
+scala> case class RecUser(name: String, friends: Seq[RecUser] = Nil)
+defined class RecUser
+
+scala> val u = RecUser(
+ | "bob",
+ | Seq(RecUser("tom")))
+u: RecUser = RecUser(bob,List(RecUser(tom,List())))
+
+scala> lazy val w: Rule[JsValue, RecUser] = From[JsValue] { __ =>
+ | import jto.validation.playjson.Rules._
+ | ((__ \ "name").read[String] ~
+ | (__ \ "friends").read(seqR(w)))(RecUser.apply) // !!! recursive rule definition
+ | }
+w: jto.validation.Rule[play.api.libs.json.JsValue,RecUser] = <lazy>
+
+Writes are implemented in a similar fashion, but a generally easier to implement. You start by defining a function for writing at a given path:
+scala> {
+ | implicit def writeJson[I](path: Path)(implicit w: Write[I, JsValue]): Write[I, JsObject] = ???
+ | }
+
+And you then define all the primitive writes:
+scala> {
+ | implicit def anyval[T <: AnyVal] = ???
+ | }
+
+In order to be able to use writes combinators, you also need to create an implementation of Monoid
for your output type. For example, to create a complex write of JsObject
, we had to implement a Monoid[JsObject]
:
scala> {
+ | import cats.Monoid
+ | implicit def jsonMonoid = new Monoid[JsObject] {
+ | def combine(a1: JsObject, a2: JsObject) = a1 deepMerge a2
+ | def empty = Json.obj()
+ | }
+ | }
+
+from there you're able to create complex writes like:
+import jto.validation._
+import play.api.libs.json._
+import scala.Function.unlift
+
+case class User(
+ name: String,
+ age: Int,
+ email: Option[String],
+ isAlive: Boolean)
+
+val userWrite = To[JsObject] { __ =>
+ import jto.validation.playjson.Writes._
+ ((__ \ "name").write[String] ~
+ (__ \ "age").write[Int] ~
+ (__ \ "email").write[Option[String]] ~
+ (__ \ "isAlive").write[Boolean])(unlift(User.unapply))
+}
+
+We highly recommend you to test your rules as much as possible. There're a few tricky cases you need to handle properly. You should port the tests in RulesSpec.scala
and use them on your rules.
The validation API provides macro-based helpers to generate Rule
and Write
for case classes (or any class with a companion object providing apply
/ and unapply
methods).
The generated code:
+Traditionally, for a given case class Person
we would define a Rule
like this:
scala> case class Person(name: String, age: Int, lovesChocolate: Boolean)
+defined class Person
+
+import jto.validation._
+import play.api.libs.json._
+
+implicit val personRule: Rule[JsValue, Person] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ ((__ \ "name").read[String] ~
+ (__ \ "age").read[Int] ~
+ (__ \ "lovesChocolate").read[Boolean])(Person.apply)
+}
+
+Let's test it:
+scala> val json = Json.parse("""{
+ | "name": "Julien",
+ | "age": 28,
+ | "lovesChocolate": true
+ | }""")
+json: play.api.libs.json.JsValue = {"name":"Julien","age":28,"lovesChocolate":true}
+
+scala> personRule.validate(json)
+res1: jto.validation.VA[Person] = Valid(Person(Julien,28,true))
+
+The exact same Rule
can be generated using Rule.gen
:
import jto.validation._
+import play.api.libs.json._
+
+implicit val personRule = {
+ import jto.validation.playjson.Rules._ // let's not leak implicits everywhere
+ Rule.gen[JsValue, Person]
+}
+
+The validation result is identical :
+scala> val json = Json.parse("""{
+ | "name": "Julien",
+ | "age": 28,
+ | "lovesChocolate": true
+ | }""")
+json: play.api.libs.json.JsValue = {"name":"Julien","age":28,"lovesChocolate":true}
+
+scala> personRule.validate(json)
+res3: jto.validation.VA[Person] = Valid(Person(Julien,28,true))
+
+Similarly we can generate a Write
:
import jto.validation._
+import play.api.libs.json._
+
+implicit val personWrite = {
+ import jto.validation.playjson.Writes._ // let's no leak implicits everywhere
+ Write.gen[Person, JsObject]
+}
+
+scala> personWrite.writes(Person("Julien", 28, true))
+res5: play.api.libs.json.JsObject = {"name":"Julien","age":28,"lovesChocolate":true}
+
+apply
method to generate Rule
/Write
. Overloading the apply
method creates an ambiguity the compiler will complain about.apply
and unapply
have corresponding input/output types. This is naturally true for case classes. However if you want to validate a trait, you must implement the same apply
/unapply
you would have in a case class.Option
/Seq
/List
/Set
& Map[String, _]
. For other generic types, you'll have to test and possibly write your Rule
/Write
if it's not working out of the box.Although the new Validation API differs significantly from the Form
API, migrating to new API is straightforward.
+This example is a case study of the migration of one of play sample application: "computer database".
We'll consider Application.scala
. This controller takes care of Computer creation, and edition. The models are defined in Models.scala
case class Company(id: Pk[Long] = NotAssigned, name: String)
+case class Computer(id: Pk[Long] = NotAssigned, name: String, introduced: Option[Date], discontinued: Option[Date], companyId: Option[Long])
+
+Here's the Application
controller, before migration:
package controllers
+
+import play.api._
+import play.api.mvc._
+import play.api.data._
+import play.api.data.Forms._
+import anorm._
+import views._
+import models._
+
+object Application extends Controller {
+
+ /** Describe the computer form (used in both edit and create screens). */
+ val computerForm = Form(
+ mapping(
+ "id" -> ignored(NotAssigned:Pk[Long]),
+ "name" -> nonEmptyText,
+ "introduced" -> optional(date("yyyy-MM-dd")),
+ "discontinued" -> optional(date("yyyy-MM-dd")),
+ "company" -> optional(longNumber)
+ )(Computer.apply)(Computer.unapply)
+ )
+
+ def index = // ...
+
+ def list(page: Int, orderBy: Int, filter: String) = // ...
+
+ def edit(id: Long) = Action {
+ Computer.findById(id).map { computer =>
+ Ok(html.editForm(id, computerForm.fill(computer), Company.options))
+ }.getOrElse(NotFound)
+ }
+
+ def update(id: Long) = Action { implicit request =>
+ computerForm.bindFromRequest.fold(
+ formWithErrors => BadRequest(html.editForm(id, formWithErrors, Company.options)),
+ computer => {
+ Computer.update(id, computer)
+ Home.flashing("success" -> "Computer %s has been updated".format(computer.name))
+ }
+ )
+ }
+
+ def create = Action {
+ Ok(html.createForm(computerForm, Company.options))
+ }
+
+ def save = Action { implicit request =>
+ computerForm.bindFromRequest.fold(
+ formWithErrors => BadRequest(html.createForm(formWithErrors, Company.options)),
+ computer => {
+ Computer.insert(computer)
+ Home.flashing("success" -> "Computer %s has been created".format(computer.name))
+ }
+ )
+ }
+
+ def delete(id: Long) = // ...
+
+}
+
+The first thing we must change is the definition of the Computer
validations.
+Instead of using play.api.data.Form
, we must define a Rule[UrlFormEncoded, Computer]
.
UrlFormEncoded
is simply an alias for Map[String, Seq[String]]
, which is the type used by play for form encoded request bodies.
Even though the syntax looks different, the logic is basically the same.
+import java.util.Date
+
+case class Computer(id: Option[Long] = None, name: String, introduced: Option[Date], discontinued: Option[Date], companyId: Option[Long])
+
+import jto.validation._
+import jto.validation.forms.UrlFormEncoded
+
+implicit val computerValidated = From[UrlFormEncoded] { __ =>
+ import jto.validation.forms.Rules._
+ ((__ \ "id").read(ignored[UrlFormEncoded, Option[Long]](None)) ~
+ (__ \ "name").read(notEmpty) ~
+ (__ \ "introduced").read(optionR(dateR("yyyy-MM-dd"))) ~
+ (__ \ "discontinued").read(optionR(dateR("yyyy-MM-dd"))) ~
+ (__ \ "company").read[Option[Long]])(Computer.apply)
+}
+
+You start by defining a simple validation for each field.
+For example "name" -> nonEmptyText
now becomes (__ \ "name").read(notEmpty)
+The next step is to compose these validations together, to get a new validation.
The old api does that using a function called mapping
, the validation api uses a method called ~
or and
(and
is an alias).
mapping(
+ "name" -> nonEmptyText,
+ "introduced" -> optional(date("yyyy-MM-dd"))
+
+now becomes
+(__ \ "name").read(notEmpty) ~
+(__ \ "introduced").read(optionR(dateR("yyyy-MM-dd")))
+
+A few built-in validations have a slightly different name than in the Form api, like optional
that became option
. You can find all the built-in rules in the scaladoc.
++Be careful with your imports. Some rules have the same names than form mapping, which could make the implicit parameters resolution fail silently.
+
Form
with an objectThe new validation API comes with a Form
class. This class is fully compatible with the existing form input helpers.
+You can use the Form.fill
method to create a Form
from a class.
Form.fill
needs an instance of Write[T, UrlFormEncoded]
, where T
is your class type.
import scala.Function.unlift
+
+implicit val computerW = To[UrlFormEncoded] { __ =>
+ import jto.validation.forms.Writes._
+ ((__ \ "id").write[Option[Long]] ~
+ (__ \ "name").write[String] ~
+ (__ \ "introduced").write(optionW(dateW("yyyy-MM-dd"))) ~
+ (__ \ "discontinued").write(optionW(dateW("yyyy-MM-dd"))) ~
+ (__ \ "company").write[Option[Long]])(unlift(Computer.unapply))
+}
+
+++Note that this
+Write
takes care of formatting.
Handling validation errors is vastly similar to the old api, the main difference is that bindFromRequest
does not exist anymore.
def save = Action(parse.urlFormEncoded) { implicit request =>
+ val r = computerValidated.validate(request.body)
+ r.fold(
+ err => BadRequest(html.createForm((request.body, r), Company.options)),
+ computer => {
+ Computer.insert(computer)
+ Home.flashing("success" -> "Computer %s has been updated".format(computer.name))
+ }
+ )
+}
+
+
+
+ The Json API and the new validation API are really similar. One could see the new Validation API as just an evolution of the Json API.
+++The json validation API still works just fine but we recommend you use the new validation API for new code, and to port your old code whenever it's possible.
+
Reads
migrationThe equivalent of a Json Reads
is a Rule
. The key difference is that Reads
assumes Json input, while Rule
is more generic, and therefore has one more type parameter.
Basically Reads[String]
== Rule[JsValue, String]
.
Migrating a Json Reads
to a Rule
is just a matter of modifying imports and specifying the input type.
Let's take a typical example from the Json API documentation:
+case class Creature(
+ name: String,
+ isDead: Boolean,
+ weight: Float)
+
+Using the json API, you would have defined something like:
+scala> {
+ | import play.api.libs.json._
+ | import play.api.libs.functional.syntax._
+ |
+ | implicit val creatureReads = (
+ | (__ \ "name").read[String] and
+ | (__ \ "isDead").read[Boolean] and
+ | (__ \ "weight").read[Float]
+ | )(Creature.apply _)
+ |
+ | val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0F)
+ | Json.fromJson[Creature](js)
+ | }
+res0: play.api.libs.json.JsResult[Creature] = JsSuccess(Creature(gremlins,false,1.0),)
+
+Using the new API, this code becomes:
+import jto.validation._
+import play.api.libs.json._
+
+implicit val creatureRule: Rule[JsValue, Creature] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ ((__ \ "name").read[String] ~
+ (__ \ "isDead").read[Boolean] ~
+ (__ \ "weight").read[Float])(Creature.apply)
+}
+
+scala> val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0F)
+js: play.api.libs.json.JsObject = {"name":"gremlins","isDead":false,"weight":1}
+
+scala> From[JsValue, Creature](js)
+res2: jto.validation.VA[Creature] = Valid(Creature(gremlins,false,1.0))
+
+Which apart from the extra imports is very similar. Notice the From[JsValue]{...}
block, that's one of the nice features of the new validation API. Not only it avoids type repetition, but it also scopes the implicits.
++Important: Note that we're importing
+Rules._
inside theFrom[JsValue]{...}
block. +It is recommended to always follow this pattern, as it nicely scopes the implicits, avoiding conflicts and accidental shadowing.
The readNullable method does not exists anymore. Just use a Rule[JsValue, Option[T]]
instead. null
and non existing Path will be handled correctly and give you a None
:
val nullableStringRule: Rule[JsValue, Option[String]] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ (__ \ "foo").read[Option[String]]
+}
+
+val js1 = Json.obj("foo" -> "bar")
+val js2 = Json.obj("foo" -> JsNull)
+val js3 = Json.obj()
+
+scala> nullableStringRule.validate(js1)
+res4: jto.validation.VA[Option[String]] = Valid(Some(bar))
+
+scala> nullableStringRule.validate(js2)
+res5: jto.validation.VA[Option[String]] = Valid(None)
+
+scala> nullableStringRule.validate(js3)
+res6: jto.validation.VA[Option[String]] = Valid(None)
+
+The general use for keepAnd
is to apply two validation on the same JsValue
, for example:
{
+ import play.api.libs.json._
+ import Reads._
+ import play.api.libs.functional.syntax._
+ (JsPath \ "key1").read[String](email keepAnd minLength[String](5))
+}
+
+You can achieve the same thing in the Validation API using Rules composition
+From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ (__ \ "key1").read(email |+| minLength(5))
+}
+
+Reads are always lazy in the new validation API, therefore, you don't need to use any specific function, even for recursive types:
+import play.api.libs.json._
+import play.api.libs.functional.syntax._
+
+case class User(id: Long, name: String, friend: Option[User] = None)
+
+implicit lazy val UserReads: Reads[User] = (
+ (__ \ 'id).read[Long] and
+ (__ \ 'name).read[String] and
+ (__ \ 'friend).lazyReadNullable(UserReads)
+)(User.apply _)
+
+val js = Json.obj(
+ "id" -> 123L,
+ "name" -> "bob",
+ "friend" -> Json.obj("id" -> 124L, "name" -> "john", "friend" -> JsNull))
+
+scala> Json.fromJson[User](js)
+res12: play.api.libs.json.JsResult[User] = JsSuccess(User(123,bob,Some(User(124,john,None))),)
+
+becomes:
+case class User(id: Long, name: String, friend: Option[User] = None)
+
+implicit lazy val userRule: Rule[JsValue, User] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ ((__ \ "id").read[Long] ~
+ (__ \ "name").read[String] ~
+ (__ \ "friend").read(optionR(userRule)))(User.apply)
+}
+
+val js = Json.obj(
+ "id" -> 123L,
+ "name" -> "bob",
+ "friend" -> Json.obj("id" -> 124L, "name" -> "john", "friend" -> JsNull))
+
+scala> From[JsValue, User](js)
+res15: jto.validation.VA[User] = Valid(User(123,bob,Some(User(124,john,None))))
+
+You should be aware that numeric type coercion is a bit stricter in the validation API.
+For example:
+scala> val js = Json.obj("n" -> 42.5f)
+js: play.api.libs.json.JsObject = {"n":42.5}
+
+scala> js.validate((__ \ "n").read[Int]) // JsSuccess(42, /n)
+res16: play.api.libs.json.JsResult[Int] = JsError(List((/n,List(ValidationError(List(error.expected.int),WrappedArray())))))
+
+whereas with the validation API, an Int
must really be an Int
:
import json.Rules._
+val js = Json.obj("n" -> 42.5f)
+(Path \ "n").read[JsValue, Int].validate(js)
+
+json.apply
and path.as[T]
Those methods do not exist in the validation API. Even in the json API, it is generally recommended not to use them as they are "unsafe".
+The preferred solution is to use path.read[T]
and to handle failure properly.
{
+ val js = Json.obj("foo" -> "bar")
+ (js \ "foo").as[String]
+}
+
+becomes
+{
+ import jto.validation.playjson.Rules._
+ (Path \ "foo").read[JsValue, String]
+}
+
+JsPath
has a prickBranch
method, that creates a Reads
extracting a subtree in a Json object:
For example, given the following json object, we can extract a sub tree:
+{
+ import play.api.libs.json._
+
+ val js = Json.obj(
+ "field1" -> "alpha",
+ "field2" -> 123L,
+ "field3" -> Json.obj(
+ "field31" -> "beta",
+ "field32"-> 345
+ ))
+
+ val pick = (__ \ "field3").json.pickBranch
+ pick.reads(js) // Valid({"field3":{"field31":"beta","field32":345}})
+}
+
+In the validation API, you simply use read
to create a rule picking a branch:
import jto.validation._
+import play.api.libs.json._
+
+val js = Json.obj(
+ "field1" -> "alpha",
+ "field2" -> 123L,
+ "field3" -> Json.obj(
+ "field31" -> "beta",
+ "field32"-> 345
+ ))
+
+val pick: Rule[JsValue, JsValue] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ (__ \ "field3").read[JsValue]
+}
+
+scala> pick.validate(js)
+res22: jto.validation.VA[play.api.libs.json.JsValue] = Valid({"field31":"beta","field32":345})
+
+Writes
migrationWrites
are really easy to port. Just like Reads
, it's basically a matter of adding imports.
For example, you would have defined a Writes
for the Creature
case class this way:
import play.api.libs.json._
+import scala.Function.unlift
+
+case class Creature(
+ name: String,
+ isDead: Boolean,
+ weight: Float)
+
+implicit val creatureWrite = (
+ (__ \ "name").write[String] and
+ (__ \ "isDead").write[Boolean] and
+ (__ \ "weight").write[Float]
+)(unlift(Creature.unapply))
+
+Json.toJson(Creature("gremlins", false, 1f))
+
+With the validation API:
+import jto.validation._
+import play.api.libs.json._
+import scala.Function.unlift
+
+case class Creature(
+ name: String,
+ isDead: Boolean,
+ weight: Float)
+
+implicit val creatureWrite = To[JsObject]{ __ =>
+ import jto.validation.playjson.Writes._
+ ((__ \ "name").write[String] ~
+ (__ \ "isDead").write[Boolean] ~
+ (__ \ "weight").write[Float])(unlift(Creature.unapply))
+}
+
+scala> val c = To[Creature, JsObject](Creature("gremlins", false, 1f))
+c: play.api.libs.json.JsObject = {"name":"gremlins","isDead":false,"weight":1}
+
+Format
migrationThe validation API does not have an equivalent for Format
. We find that generally Format
is not really convenient since validation and serialization are rarely symmetrical, and you quite often end up having multiple Reads
for a given type, making Format
rather unsettling.
Macros are also available for the validation API. See Validation Inception.
+ + +The API is designed around the concept of Rule
. A Rule[I, O]
defines a way to validate and coerce data, from type I
to type O
. It's basically a function I => Validated[O]
, where I
is the type of the input to validate, and O
is the expected output type.
Let's say you want to coerce a String
into an Float
.
+All you need to do is to define a Rule
from String to Float:
import jto.validation._
+def isFloat: Rule[String, Float] = ???
+
+When a String
is parsed into an Float
, two scenarios are possible, either:
String
can be parsed as a Float
.String
can NOT be parsed as a Float
In a typical Scala application, you would use Float.parseFloat
to parse a String
. On an "invalid" value, this method throws a NumberFormatException
.
When validating data, we'd certainly prefer to avoid exceptions, as the failure case is expected to happen quite often.
+Furthermore, your application should handle it properly, for example by sending a nice error message to the end user. The execution flow of the application should not be altered by a parsing failure, but rather be part of the process. Exceptions are definitely not the appropriate tool for the job.
+Back, to our Rule
. For now we'll not implement isFloat
, actually, the validation API comes with a number of built-in Rules, including the Float
parsing Rule[String, Float]
.
All you have to do is import the default Rules.
+import jto.validation._
+object Rules extends GenericRules with ParsingRules
+Rules.floatR
+
+Let's now test it against different String values:
+scala> Rules.floatR.validate("1")
+res1: jto.validation.VA[Float] = Valid(1.0)
+
+scala> Rules.floatR.validate("-13.7")
+res2: jto.validation.VA[Float] = Valid(-13.7)
+
+scala> Rules.floatR.validate("abc")
+res3: jto.validation.VA[Float] = Invalid(List((/,List(ValidationError(List(error.number),WrappedArray(Float))))))
+
++++
Rule
is typesafe. You can't apply aRule
on an unsupported type, the compiler won't let you:+
scala> Rules.floatR.validate(Seq(32)) +<console>:20: error: type mismatch; + found : Seq[Int] + required: String + Rules.floatR.validate(Seq(32)) + ^ +
"abc" is not a valid Float
but no exception was thrown. Instead of relying on exceptions, validate
is returning an object of type Validated
(here VA
is just a fancy alias for a special kind of validation).
Validated
represents possible outcomes of Rule application, it can be either :
Valid
, holding the value being validated
+When we use Rule.float
on "1", since "1" is a valid representation of a Float
, it returns Valid(1.0)
Invalid
, containing all the errors.
+When we use Rule.float
on "abc", since "abc" is not a valid representation of a Float
, it returns Invalid(List((/,List(ValidationError(validation.type-mismatch,WrappedArray(Float))))))
. That Invalid
tells us all there is to know: it give us a nice message explaining what has failed, and even gives us a parameter "Float"
, indicating which type the Rule
expected to find.++Note that
+Validated
is a parameterized type. Just likeRule
, it keeps track of the input and output types. +The methodvalidate
of aRule[I, O]
always return aVA[I, O]
Creating a new Rule
is almost as simple as creating a new function.
+All there is to do is to pass a function I => Validated[I, O]
to Rule.fromMapping
.
This example creates a new Rule
trying to get the first element of a List[Int]
.
+In case of an empty List[Int]
, the rule should return a Invalid
.
val headInt: Rule[List[Int], Int] = Rule.fromMapping {
+ case Nil => Invalid(Seq(ValidationError("error.emptyList")))
+ case head :: _ => Valid(head)
+}
+
+scala> headInt.validate(List(1, 2, 3, 4, 5))
+res5: jto.validation.VA[Int] = Valid(1)
+
+scala> headInt.validate(Nil)
+res6: jto.validation.VA[Int] = Invalid(List((/,List(ValidationError(List(error.emptyList),WrappedArray())))))
+
+We can make this rule a bit more generic:
+def head[T]: Rule[List[T], T] = Rule.fromMapping {
+ case Nil => Invalid(Seq(ValidationError("error.emptyList")))
+ case head :: _ => Valid(head)
+}
+
+scala> head.validate(List('a', 'b', 'c', 'd'))
+res7: jto.validation.VA[Char] = Valid(a)
+
+scala> head.validate(List[Char]())
+res8: jto.validation.VA[Char] = Invalid(List((/,List(ValidationError(List(error.emptyList),WrappedArray())))))
+
+Rules composition is very important in this API. Rule
composition means that given two Rule
a
and b
, we can easily create a new Rule c
.
There two different types of composition
+Sequential composition means that given two rules a: Rule[I, J]
and b: Rule[J, O]
, we can create a new rule c: Rule[I, O]
.
Consider the following example: We want to write a Rule
that given a List[String]
, takes the first String
in that List
, and try to parse it as a Float
.
We already have defined:
+head: Rule[List[T], T]
returns the first element of a List
float: Rule[String, Float]
parses a String
into a Float
We've done almost all the work already. We just have to create a new Rule
the applies the first Rule
and if it returns a Valid
, apply the second Rule
.
It would be fairly easy to create such a Rule
"manually", but we don't have to. A method doing just that is already available:
val firstFloat: Rule[List[String], Float] = head.andThen(Rules.floatR)
+
+scala> firstFloat.validate(List("1", "2"))
+res9: jto.validation.VA[Float] = Valid(1.0)
+
+scala> firstFloat.validate(List("1.2", "foo"))
+res10: jto.validation.VA[Float] = Valid(1.2)
+
+If the list is empty, we get the error from head
scala> firstFloat.validate(List())
+res11: jto.validation.VA[Float] = Invalid(List((/,List(ValidationError(List(error.emptyList),WrappedArray())))))
+
+If the first element is not parseable, we get the error from Rules.float
.
scala> firstFloat.validate(List("foo", "2"))
+res12: jto.validation.VA[Float] = Invalid(List((/,List(ValidationError(List(error.number),WrappedArray(Float))))))
+
+Of course everything is still typesafe:
+scala> firstFloat.validate(List(1, 2, 3))
+<console>:22: error: type mismatch;
+ found : Int(1)
+ required: String
+ firstFloat.validate(List(1, 2, 3))
+ ^
+<console>:22: error: type mismatch;
+ found : Int(2)
+ required: String
+ firstFloat.validate(List(1, 2, 3))
+ ^
+<console>:22: error: type mismatch;
+ found : Int(3)
+ required: String
+ firstFloat.validate(List(1, 2, 3))
+ ^
+
+All is fine with our new Rule
but the error reporting when we parse an element is not perfect yet.
+When a parsing error happens, the Invalid
does not tell us that it happened on the first element of the List
.
To fix that, we can pass an additionnal parameter to andThen
:
val firstFloat2: Rule[List[String],Float] = head.andThen(Path \ 0)(Rules.floatR)
+
+scala> firstFloat2.validate(List("foo", "2"))
+res14: jto.validation.VA[Float] = Invalid(List(([0],List(ValidationError(List(error.number),WrappedArray(Float))))))
+
+Parallel composition means that given two rules a: Rule[I, O]
and b: Rule[I, O]
, we can create a new rule c: Rule[I, O]
.
This form of composition is almost exclusively used for the particular case of rules that are purely constraints, that is, a Rule[I, I]
checking a value of type I
satisfies a predicate, but does not transform that value.
Consider the following example: We want to write a Rule
that given an Int
, check that this Int
is positive and even.
+The validation API already provides Rules.min
, we have to define even
ourselves:
val positive: Rule[Int,Int] = Rules.min(0)
+val even: Rule[Int,Int] = Rules.validateWith[Int]("error.even"){ _ % 2 == 0 }
+
+Now we can compose those rules using |+|
val positiveAndEven: Rule[Int,Int] = positive |+| even
+
+Let's test our new Rule
:
scala> positiveAndEven.validate(12)
+res15: jto.validation.VA[Int] = Valid(12)
+
+scala> positiveAndEven.validate(-12)
+res16: jto.validation.VA[Int] = Invalid(ArrayBuffer((/,List(ValidationError(List(error.min),WrappedArray(0))))))
+
+scala> positiveAndEven.validate(13)
+res17: jto.validation.VA[Int] = Invalid(ArrayBuffer((/,List(ValidationError(List(error.even),WrappedArray())))))
+
+scala> positiveAndEven.validate(-13)
+res18: jto.validation.VA[Int] = Invalid(ArrayBuffer((/,List(ValidationError(List(error.min),WrappedArray(0)), ValidationError(List(error.even),WrappedArray())))))
+
+Note that both rules are applied. If both fail, we get two ValidationError
.
We've already explained what a Rule
is in the previous chapter.
+Those examples were only covering simple rules. However most of the time, rules are used to validate and transform complex hierarchical objects, like Json, or Forms.
The validation API allows complex object rules creation by combining simple rules together. This chapter explains how to create complex rules.
+++Despite examples below are validating Json objects, the API is not dedicated only to Json and can be used on any type. +Please refer to Validating Json, Validating Forms, and Supporting new types for more information.
+
The validation API defines a class named Path
. A Path
represents the location of a data among a complex object.
+Unlike JsPath
it is not related to any specific type. It's just a location in some data.
+Most of the time, a Path
is our entry point into the Validation API.
A Path
is declared using this syntax:
scala> import jto.validation.Path
+import jto.validation.Path
+
+scala> val path = Path \ "foo" \ "bar"
+path: jto.validation.Path = /foo/bar
+
+Path
here is the empty Path
object. One may call it the root path.
A path can also reference indexed data, such as a Seq
scala> val pi = Path \ "foo" \ 0
+pi: jto.validation.Path = /foo[0]
+
+Path
Consider the following json:
+scala> import play.api.libs.json._
+import play.api.libs.json._
+
+scala> val js: JsValue = Json.parse("""{
+ | "user": {
+ | "name" : "toto",
+ | "age" : 25,
+ | "email" : "toto@jmail.com",
+ | "isAlive" : true,
+ | "friend" : {
+ | "name" : "tata",
+ | "age" : 20,
+ | "email" : "tata@coldmail.com"
+ | }
+ | }
+ | }""")
+js: play.api.libs.json.JsValue = {"user":{"name":"toto","age":25,"email":"toto@jmail.com","isAlive":true,"friend":{"name":"tata","age":20,"email":"tata@coldmail.com"}}}
+
+The first step before validating anything is to be able to access a fragment of the complex object.
+Assuming you'd like to validate that friend
exists and is valid in this json, you first need to access the object located at user.friend
(Javascript notation).
read
methodWe start by creating a Path
representing the location of the data we're interested in:
scala> import jto.validation._
+import jto.validation._
+
+scala> val location: Path = Path \ "user" \ "friend"
+location: jto.validation.Path = /user/friend
+
+Path
has a read[I, O]
method, where I
represents the input we're trying to parse, and O
the output type. For example, (Path \ "foo").read[JsValue, Int]
, will try to read a value located at path /foo
in a JsValue
as an Int
.
But let's try something much easier for now:
+scala> import jto.validation._
+import jto.validation._
+
+scala> import play.api.libs.json._
+import play.api.libs.json._
+
+scala> val location: Path = Path \ "user" \ "friend"
+location: jto.validation.Path = /user/friend
+
+scala> val findFriend: Rule[JsValue, JsValue] = location.read[JsValue, JsValue]
+<console>:26: error: No implicit view available from jto.validation.Path => jto.validation.RuleLike[play.api.libs.json.JsValue,play.api.libs.json.JsValue].
+ val findFriend: Rule[JsValue, JsValue] = location.read[JsValue, JsValue]
+ ^
+
+location.read[JsValue, JsValue]
means we're trying to lookup at location
in a JsValue
, and we expect to find a JsValue
there.
+In fact, we're defining a Rule
that is picking a subtree in a JsValue
.
If you try to run that code, the compiler gives you the following error:
+scala> val findFriend: Rule[JsValue, JsValue] = location.read[JsValue, JsValue]
+<console>:26: error: No implicit view available from jto.validation.Path => jto.validation.RuleLike[play.api.libs.json.JsValue,play.api.libs.json.JsValue].
+ val findFriend: Rule[JsValue, JsValue] = location.read[JsValue, JsValue]
+ ^
+
+The Scala compiler is complaining about not finding an implicit function of type Path => Rule[JsValue, JsValue]
. Indeed, unlike the Json API, you have to provide a method to lookup into the data you expect to validate.
Fortunately, such method already exists. All you have to do is to import it:
+scala> import jto.validation.playjson.Rules._
+import jto.validation.playjson.Rules._
+
+++By convention, all useful validation methods for a given type are to be found in an object called
+Rules
. That object contains a bunch of implicits defining how to lookup in the data, and how to coerce some of the possible values of those data into Scala types.
With those implicits in scope, we can finally create our Rule
:
val findFriend: Rule[JsValue, JsValue] = location.read[JsValue, JsValue]
+
+Alright, so far we've defined a Rule
looking for some data of type JsValue
, located at /user/friend
in an object of type JsValue
.
Now we need to apply this Rule
to our data:
scala> findFriend.validate(js)
+res0: jto.validation.VA[play.api.libs.json.JsValue] = Valid({"name":"tata","age":20,"email":"tata@coldmail.com"})
+
+If we can't find anything, applying a Rule
leads to a Invalid
:
scala> (Path \ "foobar").read[JsValue, JsValue].validate(js)
+res1: jto.validation.VA[play.api.libs.json.JsValue] = Invalid(List((/foobar,List(ValidationError(List(error.required),WrappedArray())))))
+
+We now are capable of extracting data at a given Path
. Let's do it again on a different sub-tree:
val age = (Path \ "user" \ "age").read[JsValue, JsValue]
+
+Let's apply this new Rule
:
scala> age.validate(js)
+res2: jto.validation.VA[play.api.libs.json.JsValue] = Valid(25)
+
+Again, if the json is invalid:
+scala> age.validate(Json.obj())
+res3: jto.validation.VA[play.api.libs.json.JsValue] = Invalid(List((/user/age,List(ValidationError(List(error.required),WrappedArray())))))
+
+The Invalid
informs us that it could not find /user/age
in that JsValue
.
That example is nice, but we'd certainly prefer to extract age
as an Int
rather than a JsValue
.
+All we have to do is to change the output type in our Rule
definition:
val age = (Path \ "user" \ "age").read[JsValue, Int]
+
+And apply it:
+scala> age.validate(js)
+res4: jto.validation.VA[Int] = Valid(25)
+
+If we try to parse something that is not an Int
, we get a Invalid
with the appropriate Path and error:
scala> (Path \ "user" \ "name").read[JsValue, Int].validate(js)
+res5: jto.validation.VA[Int] = Invalid(List((/user/name,List(ValidationError(List(error.number),WrappedArray(Int))))))
+
+So scala automagically figures out how to transform a JsValue
into an Int
. How does this happen?
It's fairly simple. The definition of read
looks like this:
{
+ def read[I, O](implicit r: Path => Rule[I, O]): Rule[I, O] = ???
+}
+
+So when use (Path \ "user" \ "age").read[JsValue, Int]
, the compiler looks for an implicit Path => Rule[JsValue, Int]
, which happens to exist in play.api.data.mapping.json.Rules
.
So far we've managed to lookup for a JsValue
and transform that JsValue
into an Int
. Problem is: not every Int
is a valid age. An age should always be a positive Int
.
val js = Json.parse("""{
+ "user": {
+ "age" : -33
+ }
+}""")
+
+val age = (Path \ "user" \ "age").read[JsValue, Int]
+
+Our current implementation of age
is rather unsatisfying...
scala> age.validate(js)
+res8: jto.validation.VA[Int] = Valid(-33)
+
+We can fix that very simply using from
, and a built-in Rule
:
val positiveAge = (Path \ "user" \ "age").from[JsValue](min(0))
+
+Let's try that again:
+scala> positiveAge.validate(js)
+res9: jto.validation.VA[Int] = Invalid(List((/user/age,List(ValidationError(List(error.min),WrappedArray(0))))))
+
+That's better, but still not perfect: 8765 is considered valid:
+scala> val js2 = Json.parse("""{ "user": { "age" : 8765 } }""")
+js2: play.api.libs.json.JsValue = {"user":{"age":8765}}
+
+scala> positiveAge.validate(js2)
+res10: jto.validation.VA[Int] = Valid(8765)
+
+Let's fix our age
Rule
:
val properAge = (Path \ "user" \ "age").from[JsValue](min(0) |+| max(130))
+
+and test it:
+val jsBig = Json.parse("""{ "user": { "age" : 8765 } }""")
+properAge.validate(jsBig)
+
+import jto.validation._
+import jto.validation.playjson.Rules._
+import play.api.libs.json._
+
+val js = Json.parse("""{
+ "user": {
+ "name" : "toto",
+ "age" : 25,
+ "email" : "toto@jmail.com",
+ "isAlive" : true,
+ "friend" : {
+ "name" : "tata",
+ "age" : 20,
+ "email" : "tata@coldmail.com"
+ }
+ }
+}""")
+
+val age = (Path \ "user" \ "age").from[JsValue](min(0) |+| max(130))
+
+scala> age.validate(js)
+res14: jto.validation.VA[Int] = Valid(25)
+
+So far we've validated only fragments of our json object.
+Now we'd like to validate the entire object, and turn it into an instance of the User
class defined below:
scala> case class User(
+ | name: String,
+ | age: Int,
+ | email: Option[String],
+ | isAlive: Boolean)
+defined class User
+
+We need to create a Rule[JsValue, User]
. Creating this Rule is simply a matter of combining together the rules parsing each field of the json.
import jto.validation._
+import play.api.libs.json._
+
+val userRule: Rule[JsValue, User] = From[JsValue] { __ =>
+ import jto.validation.playjson.Rules._
+ ((__ \ "name").read[String] ~
+ (__ \ "age").read[Int] ~
+ (__ \ "email").read[Option[String]] ~
+ (__ \ "isAlive").read[Boolean])(User.apply)
+}
+
+++Important: Note that we're importing
+Rules._
inside theFrom[I]{...}
block. +It is recommended to always follow this pattern, as it nicely scopes the implicits, avoiding conflicts and accidental shadowing.
From[JsValue]
defines the I
type of the rules we're combining. We could have written:
(Path \ "name").read[JsValue, String] ~
+(Path \ "age").read[JsValue, Int] ~
+//...
+
+but repeating JsValue
all over the place is just not very DRY.
To serialize data, the validation API provides the Write
type. A Write[I, O]
defines a way to transform data, from type I
to type O
. It's basically a function I => O
, where I
is the type of the input to serialize, and O
is the expected output type.
Let's say you want to serialize a Float
to String
.
+All you need to do is to define a Write
from Float
to String
:
import jto.validation._
+def floatToString: Write[Float, String] = ???
+
+For now we'll not implement floatToString
, actually, the validation API comes with a number of built-in Writes, including Writes.floatW[T]
.
All you have to do is import the default Writes.
+object Writes extends NumericTypes2StringWrites
+Writes.floatW
+
+Let's now test it against different Float
values:
scala> Writes.floatW.writes(12.8F)
+res1: String = 12.8
+
+scala> Writes.floatW.writes(12F)
+res2: String = 12.0
+
+Write
Creating a new Write
is almost as simple as creating a new function.
+This example creates a new Write
serializing a Float with a custom format.
val currency = Write[Double, String]{ money =>
+ import java.text.NumberFormat
+ import java.util.Locale
+ val f = NumberFormat.getCurrencyInstance(Locale.FRANCE)
+ f.format(money)
+}
+
+Testing it:
+scala> currency.writes(9.99)
+res3: String = 9,99 €
+
+Writes composition is very important in this API. Write
composition means that given two writes a: Write[I, J]
and b: Write[J, O]
, we can create a new write c: Write[I, O]
.
Let's see we're working on an e-commerce website. We have defined a Product
class.
+Each product has a name and a price:
case class Product(name: String, price: Double)
+
+Now we'd like to create a Write[Product, String]
that serializes a product to a String
of it price: Product("demo", 123)
becomes 123,00 €
We have already defined currency: Write[Double, String]
, so we'd like to reuse that.
+First, we'll create a Write[Product, Double]
extracting the price of the product:
val productPrice: Write[Product, Double] = Write[Product, Double](_.price)
+
+Now we just have to compose it with currency
:
val productAsPrice: Write[Product,String] = productPrice andThen currency
+
+Let's test our new Write
:
scala> productAsPrice.writes(Product("Awesome product", 9.99))
+res4: String = 9,99 €
+
+
+
+ We've already explained what a Write
is in the previous chapter. Those examples were only covering simple writes. Most of the time, writes are used to transform complex hierarchical objects.
In the validation API, we create complex object writes by combining simple writes. This chapter details the creation of those complex writes.
+++All the examples below are transforming classes to Json objects. The API is not dedicated only to Json, it can be used on any type. Please refer to Serializing Json, Serializing Forms, and Supporting new types for more information.
+
Path
write
methodWe start by creating a Path representing the location at which we'd like to serialize our data:
+import jto.validation._
+val location: Path = Path \ "user" \ "friend"
+
+Path
has a write[I, O]
method, where I
represents the input we’re trying to serialize, and O
is the output type. For example, (Path \ "foo").write[Int, JsObject]
, means we want to try to serialize a value of type Int
into a JsObject
at /foo
.
But let's try something much easier for now:
+scala> import jto.validation._
+import jto.validation._
+
+scala> import play.api.libs.json._
+import play.api.libs.json._
+
+scala> val location: Path = Path \ "user" \ "friend"
+location: jto.validation.Path = /user/friend
+
+scala> val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
+<console>:22: error: No implicit view available from jto.validation.Path => jto.validation.WriteLike[play.api.libs.json.JsValue,play.api.libs.json.JsObject].
+ val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
+ ^
+
+location.write[JsValue, JsObject]
means the we're trying to serialize a JsValue
to location
in a JsObject
. Effectively, we're just defining a Write
that is putting a JsValue
into a JsObject
at the given location.
If you try to run that code, the compiler gives you the following error:
+scala> val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
+<console>:22: error: No implicit view available from jto.validation.Path => jto.validation.WriteLike[play.api.libs.json.JsValue,play.api.libs.json.JsObject].
+ val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
+ ^
+
+The Scala compiler is complaining about not finding an implicit function of type Path => Write[JsValue, JsObject]
. Indeed, unlike the Json API, you have to provide a method to transform the input type into the output type.
Fortunately, such method already exists. All you have to do is import it:
+import jto.validation.playjson.Writes._
+
+++By convention, all useful serialization methods for a given type are to be found in an object called
+Writes
. That object contains a bunch of implicits defining how to serialize primitives Scala types into the expected output types.
With those implicits in scope, we can finally create our Write
:
val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
+
+Alright, so far we've defined a Write
looking for some data of type JsValue
, located at /user/friend
in a JsObject
.
Now we need to apply this Write
on our data:
scala> serializeFriend.writes(JsString("Julien"))
+res0: play.api.libs.json.JsObject = {"user":{"friend":"Julien"}}
+
+We now are capable of serializing data to a given Path
. Let's do it again on a different sub-tree:
val agejs: Write[JsValue, JsObject] = (Path \ "user" \ "age").write[JsValue, JsObject]
+
+And if we apply this new Write
:
scala> agejs.writes(JsNumber(28))
+res1: play.api.libs.json.JsObject = {"user":{"age":28}}
+
+That example is nice, but chances are age
in not a JsNumber
, but an Int
.
+All we have to do is to change the input type in our Write
definition:
val age: Write[Int, JsObject] = (Path \ "user" \ "age").write[Int, JsObject]
+
+And apply it:
+scala> age.writes(28)
+res2: play.api.libs.json.JsObject = {"user":{"age":28}}
+
+So scala automagically figures out how to transform a Int
into a JsObject
. How does this happen?
It's fairly simple. The definition of write
looks like this:
def write[I, O](implicit w: Path => Write[I, O]): Write[I, O] = ???
+
+So when you use (Path \ "user" \ "age").write[Int, JsObject]
, the compiler looks for an implicit Path => Write[Int, JsObject]
, which happens to exist in play.api.data.mapping.json.Writes
.
import jto.validation._
+import jto.validation.playjson.Writes._
+import play.api.libs.json._
+
+val age: Write[Int, JsObject] = (Path \ "user" \ "age").write[Int, JsObject]
+
+scala> age.writes(28)
+res4: play.api.libs.json.JsObject = {"user":{"age":28}}
+
+So far we've serialized only primitives types.
+Now we'd like to serialize an entire User
object defined below, and transform it into a JsObject
:
case class User(
+ name: String,
+ age: Int,
+ email: Option[String],
+ isAlive: Boolean
+)
+
+We need to create a Write[User, JsValue]
. Creating this Write
is simply a matter of combining together the writes serializing each field of the class.
import jto.validation._
+import jto.validation.playjson.Writes._
+import play.api.libs.json._
+import scala.Function.unlift
+
+val userWrite: Write[User, JsObject] = To[JsObject] { __ =>
+ import jto.validation.playjson.Writes._
+ ((__ \ "name").write[String] ~
+ (__ \ "age").write[Int] ~
+ (__ \ "email").write[Option[String]] ~
+ (__ \ "isAlive").write[Boolean])(unlift(User.unapply))
+}
+
+++Important: Note that we're importing
+Writes._
inside theTo[I]{...}
block. +It is recommended to always follow this pattern, as it nicely scopes the implicits, avoiding conflicts and accidental shadowing.
To[JsObject]
defines the O
type of the writes we're combining. We could have written:
(Path \ "name").write[String, JsObject] ~
+(Path \ "age").write[Int, JsObject] ~
+//...
+
+but repeating JsObject
all over the place is just not very DRY.
Let's test it now:
+scala> userWrite.writes(User("Julien", 28, None, true))
+res6: play.api.libs.json.JsObject = {"name":"Julien","age":28,"isAlive":true}
+
+
+
+ Version 2.x breaks back compatibility with the 1.x version. The migration has been tested on production code making heavy use of validation for json (based on play-json) and xml. Even for big projects, migrating to 2.x should not take more than 30 min.
+The best method is just to update validation in your dependencies, and let the compiler figure out what's broken. The following changes list should cover everything needed.
+The project name for play-json based validation has changed.
+"io.github.jto" %% "validation-json" % validationVersion
+
+becomes
+"io.github.jto" %% "validation-playjson" % validationVersion
+
+play.api.mapping
now becomes jto.validation
. A simple search and replace in your project should work.play.api.mapping.json
becomes play.api.mapping.playjson
The following Rule
and Write
were renamed to better match the naming convention in all subprojects.
Rules.jodaDate
becomes Rules.jodaDateR
Writes.jodaDate
becomes Writes.jodaDateW
If you encounter implicit resolution problem, you probably have a name clash. Make sure none of your Rule
/ Write
uses those names.
Since validation does not uses play-functional anymore, unlift
should be imported directly as scala.Function.unlift
instead of play.api.libs.functional.unlift
.
Since we removed all the dependencies on Play, play.api.mapping.ValidationError
is re-defined in validation. If you're using this class, make sure to replace it by jto.validation.ValidationError
.
').html(content);
+
+ $link.appendTo($title);
+ $title.appendTo($li);
+ $content.appendTo($li);
+ $li.appendTo($searchList);
+ });
+ }
+
+ function launchSearch(q) {
+ // Add class for loading
+ $body.addClass('with-search');
+ $body.addClass('search-loading');
+
+ // Launch search query
+ throttle(gitbook.search.query(q, 0, MAX_RESULTS)
+ .then(function(results) {
+ displayResults(results);
+ })
+ .always(function() {
+ $body.removeClass('search-loading');
+ }), 1000);
+ }
+
+ function closeSearch() {
+ $body.removeClass('with-search');
+ $bookSearchResults.removeClass('open');
+ }
+
+ function launchSearchFromQueryString() {
+ var q = getParameterByName('q');
+ if (q && q.length > 0) {
+ // Update search input
+ $searchInput.val(q);
+
+ // Launch search
+ launchSearch(q);
+ }
+ }
+
+ function bindSearch() {
+ // Bind DOM
+ $searchInput = $('#book-search-input input');
+ $bookSearchResults = $('#book-search-results');
+ $searchList = $bookSearchResults.find('.search-results-list');
+ $searchTitle = $bookSearchResults.find('.search-results-title');
+ $searchResultsCount = $searchTitle.find('.search-results-count');
+ $searchQuery = $searchTitle.find('.search-query');
+
+ // Launch query based on input content
+ function handleUpdate() {
+ var q = $searchInput.val();
+
+ if (q.length == 0) {
+ closeSearch();
+ }
+ else {
+ launchSearch(q);
+ }
+ }
+
+ // Detect true content change in search input
+ // Workaround for IE < 9
+ var propertyChangeUnbound = false;
+ $searchInput.on('propertychange', function(e) {
+ if (e.originalEvent.propertyName == 'value') {
+ handleUpdate();
+ }
+ });
+
+ // HTML5 (IE9 & others)
+ $searchInput.on('input', function(e) {
+ // Unbind propertychange event for IE9+
+ if (!propertyChangeUnbound) {
+ $(this).unbind('propertychange');
+ propertyChangeUnbound = true;
+ }
+
+ handleUpdate();
+ });
+
+ // Push to history on blur
+ $searchInput.on('blur', function(e) {
+ // Update history state
+ if (usePushState) {
+ var uri = updateQueryString('q', $(this).val());
+ history.pushState({ path: uri }, null, uri);
+ }
+ });
+ }
+
+ gitbook.events.on('page.change', function() {
+ bindSearch();
+ closeSearch();
+
+ // Launch search based on query parameter
+ if (gitbook.search.isInitialized()) {
+ launchSearchFromQueryString();
+ }
+ });
+
+ gitbook.events.on('search.ready', function() {
+ bindSearch();
+
+ // Launch search from query param at start
+ launchSearchFromQueryString();
+ });
+
+ function getParameterByName(name) {
+ var url = window.location.href;
+ name = name.replace(/[\[\]]/g, '\\$&');
+ var regex = new RegExp('[?&]' + name + '(=([^]*)|&|#|$)', 'i'),
+ results = regex.exec(url);
+ if (!results) return null;
+ if (!results[2]) return '';
+ return decodeURIComponent(results[2].replace(/\+/g, ' '));
+ }
+
+ function updateQueryString(key, value) {
+ value = encodeURIComponent(value);
+
+ var url = window.location.href;
+ var re = new RegExp('([?&])' + key + '=.*?(&|#|$)(.*)', 'gi'),
+ hash;
+
+ if (re.test(url)) {
+ if (typeof value !== 'undefined' && value !== null)
+ return url.replace(re, '$1' + key + '=' + value + '$2$3');
+ else {
+ hash = url.split('#');
+ url = hash[0].replace(re, '$1$3').replace(/(&|\?)$/, '');
+ if (typeof hash[1] !== 'undefined' && hash[1] !== null)
+ url += '#' + hash[1];
+ return url;
+ }
+ }
+ else {
+ if (typeof value !== 'undefined' && value !== null) {
+ var separator = url.indexOf('?') !== -1 ? '&' : '?';
+ hash = url.split('#');
+ url = hash[0] + separator + key + '=' + value;
+ if (typeof hash[1] !== 'undefined' && hash[1] !== null)
+ url += '#' + hash[1];
+ return url;
+ }
+ else
+ return url;
+ }
+ }
+});
diff --git a/docs/book/gitbook/gitbook-plugin-sharing/buttons.js b/docs/book/gitbook/gitbook-plugin-sharing/buttons.js
new file mode 100644
index 00000000..709a4e4c
--- /dev/null
+++ b/docs/book/gitbook/gitbook-plugin-sharing/buttons.js
@@ -0,0 +1,90 @@
+require(['gitbook', 'jquery'], function(gitbook, $) {
+ var SITES = {
+ 'facebook': {
+ 'label': 'Facebook',
+ 'icon': 'fa fa-facebook',
+ 'onClick': function(e) {
+ e.preventDefault();
+ window.open('http://www.facebook.com/sharer/sharer.php?s=100&p[url]='+encodeURIComponent(location.href));
+ }
+ },
+ 'twitter': {
+ 'label': 'Twitter',
+ 'icon': 'fa fa-twitter',
+ 'onClick': function(e) {
+ e.preventDefault();
+ window.open('http://twitter.com/home?status='+encodeURIComponent(document.title+' '+location.href));
+ }
+ },
+ 'google': {
+ 'label': 'Google+',
+ 'icon': 'fa fa-google-plus',
+ 'onClick': function(e) {
+ e.preventDefault();
+ window.open('https://plus.google.com/share?url='+encodeURIComponent(location.href));
+ }
+ },
+ 'weibo': {
+ 'label': 'Weibo',
+ 'icon': 'fa fa-weibo',
+ 'onClick': function(e) {
+ e.preventDefault();
+ window.open('http://service.weibo.com/share/share.php?content=utf-8&url='+encodeURIComponent(location.href)+'&title='+encodeURIComponent(document.title));
+ }
+ },
+ 'instapaper': {
+ 'label': 'Instapaper',
+ 'icon': 'fa fa-instapaper',
+ 'onClick': function(e) {
+ e.preventDefault();
+ window.open('http://www.instapaper.com/text?u='+encodeURIComponent(location.href));
+ }
+ },
+ 'vk': {
+ 'label': 'VK',
+ 'icon': 'fa fa-vk',
+ 'onClick': function(e) {
+ e.preventDefault();
+ window.open('http://vkontakte.ru/share.php?url='+encodeURIComponent(location.href));
+ }
+ }
+ };
+
+
+
+ gitbook.events.bind('start', function(e, config) {
+ var opts = config.sharing;
+
+ // Create dropdown menu
+ var menu = $.map(opts.all, function(id) {
+ var site = SITES[id];
+
+ return {
+ text: site.label,
+ onClick: site.onClick
+ };
+ });
+
+ // Create main button with dropdown
+ if (menu.length > 0) {
+ gitbook.toolbar.createButton({
+ icon: 'fa fa-share-alt',
+ label: 'Share',
+ position: 'right',
+ dropdown: [menu]
+ });
+ }
+
+ // Direct actions to share
+ $.each(SITES, function(sideId, site) {
+ if (!opts[sideId]) return;
+
+ gitbook.toolbar.createButton({
+ icon: site.icon,
+ label: site.text,
+ position: 'right',
+ onClick: site.onClick
+ });
+ });
+ });
+});
diff --git a/docs/book/gitbook/gitbook.js b/docs/book/gitbook/gitbook.js
new file mode 100644
index 00000000..e22cf4e9
--- /dev/null
+++ b/docs/book/gitbook/gitbook.js
@@ -0,0 +1,4 @@
+!function e(t,n,r){function o(s,a){if(!n[s]){if(!t[s]){var u="function"==typeof require&&require;if(!a&&u)return u(s,!0);if(i)return i(s,!0);var l=new Error("Cannot find module '"+s+"'");throw l.code="MODULE_NOT_FOUND",l}var c=n[s]={exports:{}};t[s][0].call(c.exports,function(e){var n=t[s][1][e];return o(n?n:e)},c,c.exports,e,t,n,r)}return n[s].exports}for(var i="function"==typeof require&&require,s=0;s","
"],col:[2,"
"],tr:[2,"","
"],td:[3,"
"],_default:[0,"",""]};Ie.optgroup=Ie.option,Ie.tbody=Ie.tfoot=Ie.colgroup=Ie.caption=Ie.thead,Ie.th=Ie.td,Z.extend({clone:function(e,t,n){var r,o,i,s,a=e.cloneNode(!0),u=Z.contains(e.ownerDocument,e);if(!(J.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||Z.isXMLDoc(e)))for(s=v(a),i=v(e),r=0,o=i.length;o>r;r++)y(i[r],s[r]);if(t)if(n)for(i=i||v(e),s=s||v(a),r=0,o=i.length;o>r;r++)m(i[r],s[r]);else m(e,a);return s=v(a,"script"),s.length>0&&g(s,!u&&v(e,"script")),a},buildFragment:function(e,t,n,r){for(var o,i,s,a,u,l,c=t.createDocumentFragment(),f=[],p=0,h=e.length;h>p;p++)if(o=e[p],o||0===o)if("object"===Z.type(o))Z.merge(f,o.nodeType?[o]:o);else if(Oe.test(o)){for(i=i||c.appendChild(t.createElement("div")),s=(qe.exec(o)||["",""])[1].toLowerCase(),a=Ie[s]||Ie._default,i.innerHTML=a[1]+o.replace(De,"<$1>$2>")+a[2],l=a[0];l--;)i=i.lastChild;Z.merge(f,i.childNodes),i=c.firstChild,i.textContent=""}else f.push(t.createTextNode(o));for(c.textContent="",p=0;o=f[p++];)if((!r||-1===Z.inArray(o,r))&&(u=Z.contains(o.ownerDocument,o),i=v(c.appendChild(o),"script"),u&&g(i),n))for(l=0;o=i[l++];)Fe.test(o.type||"")&&n.push(o);return c},cleanData:function(e){for(var t,n,r,o,i=Z.event.special,s=0;void 0!==(n=e[s]);s++){if(Z.acceptData(n)&&(o=n[ve.expando],o&&(t=ve.cache[o]))){if(t.events)for(r in t.events)i[r]?Z.event.remove(n,r):Z.removeEvent(n,r,t.handle);ve.cache[o]&&delete ve.cache[o]}delete ye.cache[n[ye.expando]]}}}),Z.fn.extend({text:function(e){return me(this,function(e){return void 0===e?Z.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=e)})},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=p(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=p(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){for(var n,r=e?Z.filter(e,this):this,o=0;null!=(n=r[o]);o++)t||1!==n.nodeType||Z.cleanData(v(n)),n.parentNode&&(t&&Z.contains(n.ownerDocument,n)&&g(v(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(Z.cleanData(v(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return Z.clone(this,e,t)})},html:function(e){return me(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Le.test(e)&&!Ie[(qe.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(De,"<$1>$2>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(Z.cleanData(v(t,!1)),t.innerHTML=e);t=0}catch(o){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=arguments[0];return this.domManip(arguments,function(t){e=this.parentNode,Z.cleanData(v(this)),e&&e.replaceChild(t,this)}),e&&(e.length||e.nodeType)?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t){e=U.apply([],e);var n,r,o,i,s,a,u=0,l=this.length,c=this,f=l-1,p=e[0],g=Z.isFunction(p);if(g||l>1&&"string"==typeof p&&!J.checkClone&&He.test(p))return this.each(function(n){var r=c.eq(n);g&&(e[0]=p.call(this,n,r.html())),r.domManip(e,t)});if(l&&(n=Z.buildFragment(e,this[0].ownerDocument,!1,this),r=n.firstChild,1===n.childNodes.length&&(n=r),r)){for(o=Z.map(v(n,"script"),h),i=o.length;l>u;u++)s=n,u!==f&&(s=Z.clone(s,!0,!0),i&&Z.merge(o,v(s,"script"))),t.call(this[u],s,u);if(i)for(a=o[o.length-1].ownerDocument,Z.map(o,d),u=0;i>u;u++)s=o[u],Fe.test(s.type||"")&&!ve.access(s,"globalEval")&&Z.contains(a,s)&&(s.src?Z._evalUrl&&Z._evalUrl(s.src):Z.globalEval(s.textContent.replace(Pe,"")))}return this}}),Z.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){Z.fn[e]=function(e){for(var n,r=[],o=Z(e),i=o.length-1,s=0;i>=s;s++)n=s===i?this:this.clone(!0),Z(o[s])[t](n),z.apply(r,n.get());return this.pushStack(r)}});var Me,$e={},We=/^margin/,Be=new RegExp("^("+we+")(?!px)[a-z%]+$","i"),_e=function(t){return t.ownerDocument.defaultView.opener?t.ownerDocument.defaultView.getComputedStyle(t,null):e.getComputedStyle(t,null)};!function(){function t(){s.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",s.innerHTML="",o.appendChild(i);var t=e.getComputedStyle(s,null);n="1%"!==t.top,r="4px"===t.width,o.removeChild(i)}var n,r,o=Q.documentElement,i=Q.createElement("div"),s=Q.createElement("div");s.style&&(s.style.backgroundClip="content-box",s.cloneNode(!0).style.backgroundClip="",J.clearCloneStyle="content-box"===s.style.backgroundClip,i.style.cssText="border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;position:absolute",i.appendChild(s),e.getComputedStyle&&Z.extend(J,{pixelPosition:function(){return t(),n},boxSizingReliable:function(){return null==r&&t(),r},reliableMarginRight:function(){var t,n=s.appendChild(Q.createElement("div"));return n.style.cssText=s.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",n.style.marginRight=n.style.width="0",s.style.width="1px",o.appendChild(i),t=!parseFloat(e.getComputedStyle(n,null).marginRight),o.removeChild(i),s.removeChild(n),t}}))}(),Z.swap=function(e,t,n,r){var o,i,s={};for(i in t)s[i]=e.style[i],e.style[i]=t[i];o=n.apply(e,r||[]);for(i in t)e.style[i]=s[i];return o};var Ue=/^(none|table(?!-c[ea]).+)/,ze=new RegExp("^("+we+")(.*)$","i"),Xe=new RegExp("^([+-])=("+we+")","i"),Ve={position:"absolute",visibility:"hidden",display:"block"},Ye={letterSpacing:"0",fontWeight:"400"},Ge=["Webkit","O","Moz","ms"];Z.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=w(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,i,s,a=Z.camelCase(t),u=e.style;return t=Z.cssProps[a]||(Z.cssProps[a]=C(u,a)),s=Z.cssHooks[t]||Z.cssHooks[a],void 0===n?s&&"get"in s&&void 0!==(o=s.get(e,!1,r))?o:u[t]:(i=typeof n,"string"===i&&(o=Xe.exec(n))&&(n=(o[1]+1)*o[2]+parseFloat(Z.css(e,t)),i="number"),null!=n&&n===n&&("number"!==i||Z.cssNumber[a]||(n+="px"),J.clearCloneStyle||""!==n||0!==t.indexOf("background")||(u[t]="inherit"),s&&"set"in s&&void 0===(n=s.set(e,n,r))||(u[t]=n)),void 0)}},css:function(e,t,n,r){var o,i,s,a=Z.camelCase(t);return t=Z.cssProps[a]||(Z.cssProps[a]=C(e.style,a)),s=Z.cssHooks[t]||Z.cssHooks[a],s&&"get"in s&&(o=s.get(e,!0,n)),void 0===o&&(o=w(e,t,r)),"normal"===o&&t in Ye&&(o=Ye[t]),""===n||n?(i=parseFloat(o),n===!0||Z.isNumeric(i)?i||0:o):o}}),Z.each(["height","width"],function(e,t){Z.cssHooks[t]={get:function(e,n,r){return n?Ue.test(Z.css(e,"display"))&&0===e.offsetWidth?Z.swap(e,Ve,function(){return N(e,t,r)}):N(e,t,r):void 0},set:function(e,n,r){var o=r&&_e(e);return j(e,n,r?k(e,t,r,"border-box"===Z.css(e,"boxSizing",!1,o),o):0)}}}),Z.cssHooks.marginRight=T(J.reliableMarginRight,function(e,t){return t?Z.swap(e,{display:"inline-block"},w,[e,"marginRight"]):void 0}),Z.each({margin:"",padding:"",border:"Width"},function(e,t){Z.cssHooks[e+t]={expand:function(n){for(var r=0,o={},i="string"==typeof n?n.split(" "):[n];4>r;r++)o[e+Te[r]+t]=i[r]||i[r-2]||i[0];return o}},We.test(e)||(Z.cssHooks[e+t].set=j)}),Z.fn.extend({css:function(e,t){return me(this,function(e,t,n){var r,o,i={},s=0;if(Z.isArray(t)){for(r=_e(e),o=t.length;o>s;s++)i[t[s]]=Z.css(e,t[s],!1,r);return i}return void 0!==n?Z.style(e,t,n):Z.css(e,t)},e,t,arguments.length>1)},show:function(){return E(this,!0)},hide:function(){return E(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){Ce(this)?Z(this).show():Z(this).hide()})}}),Z.Tween=S,S.prototype={constructor:S,init:function(e,t,n,r,o,i){this.elem=e,this.prop=n,this.easing=o||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=i||(Z.cssNumber[n]?"":"px")},cur:function(){var e=S.propHooks[this.prop];return e&&e.get?e.get(this):S.propHooks._default.get(this)},run:function(e){var t,n=S.propHooks[this.prop];return this.options.duration?this.pos=t=Z.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):S.propHooks._default.set(this),this}},S.prototype.init.prototype=S.prototype,S.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=Z.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){Z.fx.step[e.prop]?Z.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[Z.cssProps[e.prop]]||Z.cssHooks[e.prop])?Z.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},S.propHooks.scrollTop=S.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},Z.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},Z.fx=S.prototype.init,Z.fx.step={};var Je,Qe,Ke=/^(?:toggle|show|hide)$/,Ze=new RegExp("^(?:([+-])=|)("+we+")([a-z%]*)$","i"),et=/queueHooks$/,tt=[O],nt={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),o=Ze.exec(t),i=o&&o[3]||(Z.cssNumber[e]?"":"px"),s=(Z.cssNumber[e]||"px"!==i&&+r)&&Ze.exec(Z.css(n.elem,e)),a=1,u=20;if(s&&s[3]!==i){i=i||s[3],o=o||[],s=+r||1;do a=a||".5",s/=a,Z.style(n.elem,e,s+i);while(a!==(a=n.cur()/r)&&1!==a&&--u)}return o&&(s=n.start=+s||+r||0,n.unit=i,n.end=o[1]?s+(o[1]+1)*o[2]:+o[2]),n}]};Z.Animation=Z.extend(H,{tweener:function(e,t){Z.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");for(var n,r=0,o=e.length;o>r;r++)n=e[r],nt[n]=nt[n]||[],nt[n].unshift(t)},prefilter:function(e,t){t?tt.unshift(e):tt.push(e)}}),Z.speed=function(e,t,n){var r=e&&"object"==typeof e?Z.extend({},e):{complete:n||!n&&t||Z.isFunction(e)&&e,duration:e,easing:n&&t||t&&!Z.isFunction(t)&&t};return r.duration=Z.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in Z.fx.speeds?Z.fx.speeds[r.duration]:Z.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){Z.isFunction(r.old)&&r.old.call(this),r.queue&&Z.dequeue(this,r.queue)},r},Z.fn.extend({fadeTo:function(e,t,n,r){return this.filter(Ce).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var o=Z.isEmptyObject(e),i=Z.speed(t,n,r),s=function(){var t=H(this,Z.extend({},e),i);(o||ve.get(this,"finish"))&&t.stop(!0)};return s.finish=s,o||i.queue===!1?this.each(s):this.queue(i.queue,s)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,o=null!=e&&e+"queueHooks",i=Z.timers,s=ve.get(this);
+if(o)s[o]&&s[o].stop&&r(s[o]);else for(o in s)s[o]&&s[o].stop&&et.test(o)&&r(s[o]);for(o=i.length;o--;)i[o].elem!==this||null!=e&&i[o].queue!==e||(i[o].anim.stop(n),t=!1,i.splice(o,1));(t||!n)&&Z.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=ve.get(this),r=n[e+"queue"],o=n[e+"queueHooks"],i=Z.timers,s=r?r.length:0;for(n.finish=!0,Z.queue(this,e,[]),o&&o.stop&&o.stop.call(this,!0),t=i.length;t--;)i[t].elem===this&&i[t].queue===e&&(i[t].anim.stop(!0),i.splice(t,1));for(t=0;s>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),Z.each(["toggle","show","hide"],function(e,t){var n=Z.fn[t];Z.fn[t]=function(e,r,o){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(D(t,!0),e,r,o)}}),Z.each({slideDown:D("show"),slideUp:D("hide"),slideToggle:D("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){Z.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),Z.timers=[],Z.fx.tick=function(){var e,t=0,n=Z.timers;for(Je=Z.now();t","