├── project
├── build.properties
├── plugins.sbt
└── Boilerplate.scala
├── version.sbt
├── play-scalajs-example
├── jvm
│ ├── public
│ │ └── stylesheets
│ │ │ └── main.css
│ ├── conf
│ │ ├── application.conf
│ │ └── routes
│ └── app
│ │ ├── views
│ │ ├── main.scala.html
│ │ └── index.scala.html
│ │ ├── controllers
│ │ └── Application.scala
│ │ └── ExampleApplicationLoader.scala
├── project
│ ├── build.properties
│ └── plugins.sbt
├── js
│ └── src
│ │ └── main
│ │ └── scala
│ │ ├── Main.scala
│ │ └── Validate.scala
├── shared
│ └── src
│ │ └── main
│ │ └── scala
│ │ └── User.scala
├── LICENSE
└── build.sbt
├── validation-xml
└── src
│ ├── main
│ └── scala
│ │ ├── package.scala
│ │ ├── Writes.scala
│ │ └── Rules.scala
│ └── test
│ └── scala
│ └── WritesSpec.scala
├── validation-delimited
└── src
│ ├── main
│ └── scala
│ │ ├── package.scala
│ │ └── Rules.scala
│ └── test
│ └── scala
│ └── RulesSpec.scala
├── validation-jsjson
└── src
│ ├── test
│ └── scala
│ │ └── JsAnyEquality.scala
│ └── main
│ └── scala
│ ├── Writes.scala
│ └── Rules.scala
├── validation-form
└── src
│ └── main
│ └── scala
│ ├── package.scala
│ ├── Writes.scala
│ └── Rules.scala
├── validation-core
└── src
│ ├── main
│ └── scala
│ │ ├── ValidationError.scala
│ │ ├── SyntaxObs.scala
│ │ ├── package.scala
│ │ ├── Format.scala
│ │ ├── Write.scala
│ │ ├── Path.scala
│ │ ├── DefaultWrites.scala
│ │ ├── backcompat.scala
│ │ ├── Rule.scala
│ │ ├── Formatter.scala
│ │ └── MappingMacros.scala
│ └── test
│ └── scala
│ ├── PathSpec.scala
│ ├── DefaultRulesSpec.scala
│ └── ValidationSpec.scala
├── .gitignore
├── scripts
├── ci.sh
└── build-book.sh
├── PUBLISH.md
├── .travis.yml
├── docs
└── src
│ └── main
│ └── tut
│ ├── SUMMARY.md
│ ├── README.md
│ ├── V2MigrationGuide.md
│ ├── ReleaseNotes.md
│ ├── ScalaValidationWrite.md
│ ├── ScalaValidationMacros.md
│ ├── ScalaValidationMigrationForm.md
│ ├── ScalaValidationWriteCombinators.md
│ ├── ScalaJsValidation.md
│ ├── ScalaValidationRule.md
│ ├── ScalaValidationCookbook.md
│ ├── ScalaValidationExtensions.md
│ ├── ScalaValidationMigrationJson.md
│ └── ScalaValidationRuleCombinators.md
├── validation-jsonast
├── shared
│ └── src
│ │ ├── test
│ │ └── scala
│ │ │ └── AstSpec.scala
│ │ └── main
│ │ └── scala
│ │ ├── JValue.scala
│ │ ├── Writes.scala
│ │ └── Rules.scala
├── jvm
│ └── src
│ │ └── main
│ │ └── scala
│ │ └── Ast.scala
└── js
│ └── src
│ └── main
│ └── scala
│ └── Ast.scala
├── validation-playjson
└── src
│ └── main
│ └── scala
│ ├── Writes.scala
│ └── Rules.scala
└── README.md
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.2.1
2 |
--------------------------------------------------------------------------------
/version.sbt:
--------------------------------------------------------------------------------
1 | version in ThisBuild := "2.1.1"
2 |
--------------------------------------------------------------------------------
/play-scalajs-example/jvm/public/stylesheets/main.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/play-scalajs-example/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.11
2 |
--------------------------------------------------------------------------------
/play-scalajs-example/jvm/conf/application.conf:
--------------------------------------------------------------------------------
1 | play.application.loader=ExampleApplicationLoader
2 | play.crypto.secret="secret"
3 |
--------------------------------------------------------------------------------
/validation-xml/src/main/scala/package.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | package object xml {
4 |
5 | type XmlWriter = scala.xml.Elem => scala.xml.Elem
6 | }
7 |
--------------------------------------------------------------------------------
/play-scalajs-example/jvm/conf/routes:
--------------------------------------------------------------------------------
1 | GET / controllers.Application.index
2 | GET /assets/*file controllers.Assets.at(path="/public", file)
3 |
--------------------------------------------------------------------------------
/validation-delimited/src/main/scala/package.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | package object delimited {
4 | type Delimited = Array[String]
5 | type DelimitedVA[O] = Validated[(IdxPathNode, Seq[ValidationError]), O]
6 | }
7 |
--------------------------------------------------------------------------------
/play-scalajs-example/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.2")
2 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.9")
3 | addSbtPlugin("com.vmunier" % "sbt-play-scalajs" % "0.3.0")
4 |
--------------------------------------------------------------------------------
/play-scalajs-example/js/src/main/scala/Main.scala:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import scala.scalajs.js
4 |
5 | object Main extends js.JSApp {
6 | def main(): Unit = {
7 | println("Hello console!")
8 | throw new Exception("Check out my stack trace")
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/validation-jsjson/src/test/scala/JsAnyEquality.scala:
--------------------------------------------------------------------------------
1 | import scala.scalajs.js
2 | import org.scalatest._
3 |
4 | trait JsAnyEquality {
5 | this: Matchers =>
6 | implicit class ShouldBeEqualAfterStringify(val dynamic: js.Any) {
7 | def shouldBe(otherDynamic: js.Any): Assertion =
8 | js.JSON.stringify(dynamic) shouldBe js.JSON.stringify(otherDynamic)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/play-scalajs-example/jvm/app/views/main.scala.html:
--------------------------------------------------------------------------------
1 | @(title: String)(content: Html)(implicit environment: play.api.Environment)
2 |
3 |
4 |
5 |
6 | @title
7 |
8 |
9 | @content
10 | @* Outputs a tag to include the output of Scala.js compilation. *@
11 | @playscalajs.html.scripts(projectName = "js")
12 |
13 |
14 |
--------------------------------------------------------------------------------
/validation-form/src/main/scala/package.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | /**
4 | * Contains the validation API used by `Form`.
5 | *
6 | * For example, to define a custom constraint:
7 | * {{{
8 | * val negative = Constraint[Int] {
9 | * case i if i < 0 => Valid
10 | * case _ => Invalid("Must be a negative number.")
11 | * }
12 | * }}}
13 | */
14 | package object forms {
15 | type UrlFormEncoded = Map[String, Seq[String]]
16 | }
17 |
--------------------------------------------------------------------------------
/validation-core/src/main/scala/ValidationError.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | /**
4 | * A validation error.
5 | *
6 | * @param message the error message
7 | * @param args the error message arguments
8 | */
9 | case class ValidationError(messages: Seq[String], args: Any*) {
10 | lazy val message = messages.last
11 | }
12 |
13 | object ValidationError {
14 | def apply(message: String, args: Any*) =
15 | new ValidationError(Seq(message), args: _*)
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | target/
3 | logs/
4 | repository/
5 | *.lock
6 | *.komodoproject
7 | .DS_Store
8 | project/boot/
9 | framework/project/boot/
10 | documentation/api
11 | workspace/
12 | framework/sbt/boot
13 | .history
14 | .idea
15 | RUNNING_PID
16 | .classpath
17 | .project
18 | .settings/
19 | .target/
20 | .cache
21 | *.iml
22 | documentation/*.pdf
23 | framework/test/integrationtest-java/conf/evolutions/
24 | generated.keystore
25 | node_modules
26 | npm-debug.log
27 | docs/tut*
28 |
--------------------------------------------------------------------------------
/scripts/ci.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -eux
3 |
4 | sbt_cmd="sbt ++$TRAVIS_SCALA_VERSION"
5 |
6 | test_cmd="$sbt_cmd clean test"
7 |
8 | coverage="$sbt_cmd clean coverage validationJVM/test coverageReport && sbt coverageAggregate && sbt coveralls"
9 |
10 | compile_example="$sbt_cmd publish-local && (cd play-scalajs-example && $sbt_cmd compile)"
11 |
12 | compile_doc="bash scripts/build-book.sh"
13 |
14 | run_cmd="$coverage && $test_cmd && $compile_example && $compile_doc"
15 |
16 | eval $run_cmd
17 |
--------------------------------------------------------------------------------
/scripts/build-book.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -eux
3 |
4 | gitbook="node_modules/gitbook-cli/bin/gitbook.js"
5 |
6 | if ! test -e $gitbook; then
7 | npm install gitbook
8 | npm install gitbook-cli
9 | fi
10 |
11 | sbt tut
12 |
13 | (
14 | cd play-scalajs-example
15 | sbt js/fullOptJS
16 | )
17 |
18 | $gitbook build docs/target/tut docs/book
19 |
20 | cp play-scalajs-example/js/target/scala-2.11/js-opt.js docs/book
21 | cp play-scalajs-example/js/target/scala-2.11/js-launcher.js docs/book
22 |
23 | exit 0
24 |
--------------------------------------------------------------------------------
/PUBLISH.md:
--------------------------------------------------------------------------------
1 | # Publish instructions
2 |
3 | - Update [version.sbt](version.sbt)
4 |
5 | - Update `libraryDependencies` in [README.md](README.md)
6 |
7 | - Update comment in `play-scalajs-example/build.sbt`
8 |
9 | - Publish book:
10 |
11 | ```sh
12 | git checkout gh-pages
13 | git checkout master .
14 | sh scripts/build-book.sh
15 | git add .
16 | git commit -am "Update book"
17 | git push
18 | ```
19 |
20 | - Publish library:
21 |
22 | ```sh
23 | sbt publishSigned
24 | sbt sonatypeReleaseAll
25 | ```
26 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 | scala: 2.11.8
3 | jdk: oraclejdk8
4 | sbt_args: "-J-Xmx2G"
5 |
6 | notifications:
7 | email:
8 | false
9 |
10 | script: bash scripts/ci.sh
11 |
12 | cache:
13 | directories:
14 | - $HOME/.sbt/0.13/dependency
15 | - $HOME/.sbt/boot/scala*
16 | - $HOME/.sbt/launchers
17 | - $HOME/.ivy2/cache
18 | - $HOME/.nvm
19 |
20 | before_cache:
21 | - du -h -d 1 $HOME/.ivy2/cache
22 | - du -h -d 2 $HOME/.sbt/
23 | - find $HOME/.sbt -name "*.lock" -type f -delete
24 | - find $HOME/.ivy2/cache -name "ivydata-*.properties" -type f -delete
25 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | resolvers += Resolver.url(
2 | "tpolecat-sbt-plugin-releases",
3 | url("http://dl.bintray.com/content/tpolecat/sbt-plugin-releases"))(
4 | Resolver.ivyStylePatterns)
5 |
6 | addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.6.7")
7 |
8 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0")
9 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.24")
10 |
11 | addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.15")
12 |
13 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1")
14 |
15 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.5")
--------------------------------------------------------------------------------
/play-scalajs-example/jvm/app/controllers/Application.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import jto.validation._
4 | import jto.validation.jsonast._
5 | import play.api.Environment
6 | import play.api.libs.json._
7 | import play.api.mvc._
8 |
9 | import model.User
10 |
11 | class Application()(implicit environment: Environment) extends Controller {
12 | def index = Action {
13 | val write: Write[User, JsValue] = Write.toWrite(User.format) andThen Ast.to
14 | val user: User = User("supercat", 20, Some("e@mail.com"), true)
15 | val json: String = Json.prettyPrint(write.writes(user))
16 | Ok(views.html.index(json))
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/play-scalajs-example/js/src/main/scala/Validate.scala:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import jto.validation._
4 | import jto.validation.jsonast.Ast
5 | import jto.validation.jsjson._
6 | import scala.scalajs.js
7 | import js.annotation.JSExport
8 | import model.User
9 | import scala.Function.{unlift, const}
10 |
11 | @JSExport
12 | object Validate {
13 | @JSExport
14 | def user(json: js.Dynamic): js.Dynamic = {
15 | import Writes._
16 |
17 | implicit val format: Format[js.Dynamic, js.Dynamic, User] = Format(
18 | Ast.from andThen User.format,
19 | Write.toWrite(User.format) andThen Ast.to
20 | )
21 |
22 | To[VA[User], js.Dynamic](format.validate(json))
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/play-scalajs-example/jvm/app/ExampleApplicationLoader.scala:
--------------------------------------------------------------------------------
1 | import controllers.{Application, Assets}
2 | import play.api.ApplicationLoader.Context
3 | import play.api.{ApplicationLoader, BuiltInComponentsFromContext}
4 | import router.Routes
5 |
6 | class ExampleApplicationLoader() extends ApplicationLoader {
7 | def load(context: Context) = new ApplicationComponents(context).application
8 | }
9 |
10 | class ApplicationComponents(context: Context) extends BuiltInComponentsFromContext(context) {
11 | lazy val applicationController = new Application()(environment)
12 | lazy val assets = new Assets(httpErrorHandler)
13 | override lazy val router = new Routes(httpErrorHandler, applicationController, assets)
14 | }
15 |
--------------------------------------------------------------------------------
/play-scalajs-example/shared/src/main/scala/User.scala:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import jto.validation._
4 | import jto.validation.jsonast._
5 | import scala.Function.unlift
6 |
7 | case class User(
8 | name: String,
9 | age: Int,
10 | email: Option[String],
11 | isAlive: Boolean
12 | )
13 |
14 | object User {
15 | import Rules._, Writes._
16 | implicit val format: Format[JValue, JObject, User] =
17 | Formatting[JValue, JObject] { __ =>
18 | (
19 | (__ \ "name").format(notEmpty) ~
20 | (__ \ "age").format(min(0) |+| max(130)) ~
21 | (__ \ "email").format(optionR(email), optionW(stringW)) ~
22 | (__ \ "isAlive").format[Boolean]
23 | )(User.apply, unlift(User.unapply))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/docs/src/main/tut/SUMMARY.md:
--------------------------------------------------------------------------------
1 | - Features
2 |
3 | - [Validating and transforming data](ScalaValidationRule.md)
4 | - [Combining Rules](ScalaValidationRuleCombinators.md)
5 | - [Serializing data with Write](ScalaValidationWrite.md)
6 | - [Combining Writes](ScalaValidationWriteCombinators.md)
7 | - [Validation Inception](ScalaValidationMacros.md)
8 | - [Exporting Validations to Javascript using Scala.js](ScalaJsValidation.md)
9 | - [Extensions: Supporting new types](ScalaValidationExtensions.md)
10 | - [Cookbook](ScalaValidationCookbook.md)
11 |
12 | - Migration
13 |
14 | - [v2.0 Migration guide](V2MigrationGuide.md)
15 | - [Play's Form API migration](ScalaValidationMigrationForm.md)
16 | - [Play's Json API migration](ScalaValidationMigrationJson.md)
17 |
18 | - [Release notes](ReleaseNotes.md)
19 |
--------------------------------------------------------------------------------
/play-scalajs-example/jvm/app/views/index.scala.html:
--------------------------------------------------------------------------------
1 | @(json: String)(implicit environment: play.api.Environment)
2 |
3 | @main("Play Scala.js Validation") {
4 |
5 |
6 | }
7 |
8 |
25 |
--------------------------------------------------------------------------------
/validation-jsonast/shared/src/test/scala/AstSpec.scala:
--------------------------------------------------------------------------------
1 | import jto.validation.Valid
2 | import jto.validation.jsonast._
3 | import org.scalatest._
4 |
5 | class AstSpec extends WordSpec with Matchers {
6 | "Ast" should {
7 | def prop(ast: JValue): Boolean =
8 | Ast.from.validate(Ast.to.writes(ast)) == Valid(ast)
9 |
10 | "be a bijection" in {
11 | val aNull = JNull
12 | val aString = JString("string")
13 | val aBoolean = JBoolean(true)
14 | val anArray = JArray(Vector(aBoolean, aString))
15 | val anObject = JObject(Map("a" -> anArray, "b" -> aBoolean))
16 |
17 | assert(prop(aNull))
18 | assert(prop(aString))
19 | assert(prop(aBoolean))
20 | assert(prop(anArray))
21 | assert(prop(anObject))
22 | assert(prop(JNumber("123.4")))
23 | assert(prop(JNumber("123")))
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/validation-jsonast/shared/src/main/scala/JValue.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package jsonast
3 |
4 | sealed trait JValue
5 | case object JNull extends JValue
6 | case class JObject (value: Map[String, JValue] = Map.empty) extends JValue
7 | case class JArray (value: Seq[JValue] = Seq.empty) extends JValue
8 | case class JBoolean(value: Boolean) extends JValue
9 | case class JString (value: String) extends JValue
10 | case class JNumber (value: String) extends JValue {
11 | require(JNumber.regex.matcher(value).matches)
12 | }
13 |
14 | object JNumber {
15 | val regex = """-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?""".r.pattern
16 | def apply(i: Int): JNumber = JNumber(i.toString)
17 | def apply(l: Long): JNumber = JNumber(l.toString)
18 | def apply(d: Double): JNumber = JNumber(d.toString)
19 | }
20 |
--------------------------------------------------------------------------------
/validation-core/src/main/scala/SyntaxObs.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | case class ~[A, B](_1: A, _2: B)
4 |
5 | trait SyntaxCombine[M[_]] {
6 | def apply[A, B](ma: M[A], mb: M[B]): M[A ~ B]
7 | }
8 |
9 | class InvariantSyntaxObs[M[_], A](ma: M[A])(implicit combine: SyntaxCombine[M]) {
10 | def ~[B](mb: M[B]): InvariantSyntax[M]#InvariantSyntax2[A, B] = {
11 | val b = new InvariantSyntax(combine)
12 | new b.InvariantSyntax2[A, B](ma, mb)
13 | }
14 | }
15 |
16 | class FunctorSyntaxObs[M[_], A](ma: M[A])(implicit combine: SyntaxCombine[M]) {
17 | def ~[B](mb: M[B]): FunctorSyntax[M]#FunctorSyntax2[A, B] = {
18 | val b = new FunctorSyntax(combine)
19 | new b.FunctorSyntax2[A, B](ma, mb)
20 | }
21 | }
22 |
23 | class ContravariantSyntaxObs[M[_], A](ma: M[A])(
24 | implicit combine: SyntaxCombine[M]) {
25 | def ~[B](mb: M[B]): ContravariantSyntax[M]#ContravariantSyntax2[A, B] = {
26 | val b = new ContravariantSyntax(combine)
27 | new b.ContravariantSyntax2[A, B](ma, mb)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/validation-jsonast/jvm/src/main/scala/Ast.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package jsonast
3 |
4 | import play.api.libs.json._
5 |
6 | object Ast {
7 | val to: Write[JValue, JsValue] = Write[JValue, JsValue] {
8 | case JNull => JsNull
9 | case JObject (value) => JsObject(value.mapValues(to.writes))
10 | case JArray (value) => JsArray(value.map(to.writes))
11 | case JBoolean(value) => JsBoolean(value)
12 | case JString (value) => JsString(value)
13 | case JNumber (value) => JsNumber(BigDecimal(value))
14 | }
15 |
16 | private def totalFrom(jsValue: JsValue): JValue = jsValue match {
17 | case JsNull => JNull
18 | case JsObject (value) => JObject(value.mapValues(totalFrom).toMap)
19 | case JsArray (value) => JArray(value.map(totalFrom).toVector)
20 | case JsBoolean(value) => JBoolean(value)
21 | case JsString (value) => JString(value)
22 | case JsNumber (value) => JNumber(value.toString)
23 | }
24 |
25 | val from: Rule[JsValue, JValue] = Rule(x => Valid(totalFrom(x)))
26 | }
27 |
--------------------------------------------------------------------------------
/validation-core/src/test/scala/PathSpec.scala:
--------------------------------------------------------------------------------
1 | import jto.validation._
2 |
3 | import org.scalatest._
4 |
5 | class PathSpec extends WordSpec with Matchers {
6 | "Path" should {
7 | "be compareable" in {
8 | (Path \ "foo" \ "bar") shouldBe ((Path \ "foo" \ "bar"))
9 | (Path \ "foo" \ "bar").hashCode shouldBe ((Path \ "foo" \ "bar").hashCode)
10 | (Path \ "foo" \ "bar") should not equal ((Path \ "foo"))
11 | (Path \ "foo" \ "bar").hashCode should not equal ((Path \ "foo").hashCode)
12 | }
13 |
14 | "compose" in {
15 | val c = (Path \ "foo" \ "bar") compose (Path \ "baz")
16 | val c2 = (Path \ "foo" \ "bar") ++ (Path \ "baz")
17 | c shouldBe (Path \ "foo" \ "bar" \ "baz")
18 | c2 shouldBe (Path \ "foo" \ "bar" \ "baz")
19 | }
20 |
21 | "have deconstructors" in {
22 | val path = Path \ "foo" \ "bar" \ "baz"
23 |
24 | val (h \: t) = path
25 | h shouldBe (Path \ "foo")
26 | t shouldBe (Path \ "bar" \ "baz")
27 |
28 | val (h1 \: h2 \: t2) = path
29 | h1 shouldBe (Path \ "foo")
30 | h2 shouldBe (Path \ "bar")
31 | t2 shouldBe (Path \ "baz")
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/play-scalajs-example/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Olivier Blanvillain
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/validation-core/src/test/scala/DefaultRulesSpec.scala:
--------------------------------------------------------------------------------
1 | import jto.validation._
2 |
3 | import org.scalatest._
4 |
5 | class DefaultRulesSpec extends WordSpec with Matchers {
6 |
7 | object R extends GenericRules
8 | import R._
9 |
10 | "DefaultRules" should {
11 |
12 | def failure(m: String, args: Any*) =
13 | Invalid(Seq(Path -> Seq(ValidationError(m, args: _*))))
14 |
15 | "validate non emptyness" in {
16 | notEmpty.validate("foo") shouldBe (Valid("foo"))
17 | notEmpty.validate("") shouldBe (failure("error.required"))
18 | }
19 |
20 | "validate min" in {
21 | min(4).validate(5) shouldBe (Valid(5))
22 | min(4).validate(4) shouldBe (Valid(4))
23 | min(4).validate(1) shouldBe (failure("error.min", 4))
24 | min(4).validate(-10) shouldBe (failure("error.min", 4))
25 |
26 | min("a").validate("b") shouldBe (Valid("b"))
27 | }
28 |
29 | "validate max" in {
30 | max(8).validate(5) shouldBe (Valid(5))
31 | max(5).validate(5) shouldBe (Valid(5))
32 | max(0).validate(1) shouldBe (failure("error.max", 0))
33 | max(-30).validate(-10) shouldBe (failure("error.max", -30))
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/validation-core/src/main/scala/package.scala:
--------------------------------------------------------------------------------
1 | package jto
2 |
3 | import cats.Monoid
4 |
5 | /**
6 | * Contains the validation API used by `Form`.
7 | *
8 | * For example, to define a custom constraint:
9 | * {{{
10 | * val negative = Constraint[Int] {
11 | * case i if i < 0 => Valid
12 | * case _ => Invalid("Must be a negative number.")
13 | * }
14 | * }}}
15 | */
16 | package object validation {
17 | @annotation.implicitNotFound(
18 | "No implicit Mapping found from ${I} to ${O}. Try to define an implicit Mapping[${E}, ${I}, ${O}].")
19 | type Mapping[E, I, O] = I => Validated[Seq[E], O]
20 | type Constraint[T] = Mapping[ValidationError, T, T]
21 | type VA[O] = Validated[Seq[(Path, Seq[ValidationError])], O]
22 |
23 | type Validated[+E, +A] = cats.data.Validated[E, A]
24 | val Validated = cats.data.Validated
25 | type Valid[+A] = cats.data.Validated.Valid[A]
26 | val Valid = cats.data.Validated.Valid
27 | type Invalid[+E] = cats.data.Validated.Invalid[E]
28 | val Invalid = cats.data.Validated.Invalid
29 |
30 | implicit def validatedBackcompat[E, A](
31 | va: Validated[Seq[E], A]): VABackCompat[E, A] =
32 | new VABackCompat[E, A] {
33 | val v = va
34 | }
35 |
36 | implicit def seqAlgebra[A]: Monoid[Seq[A]] =
37 | new Monoid[Seq[A]] {
38 | def empty: Seq[A] = Seq.empty[A]
39 | def combine(x: Seq[A], y: Seq[A]): Seq[A] = x ++ y
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/play-scalajs-example/build.sbt:
--------------------------------------------------------------------------------
1 | val scalaV = "2.11.8"
2 |
3 | val validationVersion = "2.1.0"
4 |
5 | lazy val jvm = project
6 | .in(file("jvm"))
7 | .settings(
8 | scalaVersion := scalaV,
9 | scalaJSProjects := Seq(js),
10 | pipelineStages := Seq(scalaJSProd),
11 | libraryDependencies ++= Seq(
12 | "com.vmunier" %% "play-scalajs-scripts" % "0.5.0",
13 | "io.github.jto" %% "validation-core" % validationVersion,
14 | "io.github.jto" %% "validation-playjson" % validationVersion,
15 | "io.github.jto" %% "validation-jsonast" % validationVersion))
16 | .enablePlugins(PlayScala)
17 | .aggregate(js)
18 | .dependsOn(sharedJVM)
19 |
20 | lazy val js = project
21 | .in(file("js"))
22 | .settings(
23 | scalaVersion := scalaV,
24 | persistLauncher := true,
25 | libraryDependencies ++= Seq(
26 | "io.github.jto" %%% "validation-core" % validationVersion,
27 | "io.github.jto" %%% "validation-jsjson" % validationVersion,
28 | "io.github.jto" %%% "validation-jsonast" % validationVersion))
29 | .enablePlugins(ScalaJSPlugin, ScalaJSPlay)
30 | .dependsOn(sharedJS)
31 |
32 | lazy val sharedJVM = shared.jvm
33 | lazy val sharedJS = shared.js
34 | lazy val shared = crossProject.crossType(CrossType.Pure)
35 | .in(file("shared"))
36 | .settings(
37 | scalaVersion := scalaV,
38 | libraryDependencies ++= Seq(
39 | "io.github.jto" %%% "validation-core" % validationVersion,
40 | "io.github.jto" %%% "validation-jsonast" % validationVersion))
41 | .jsConfigure(_.enablePlugins(ScalaJSPlay))
42 |
43 | onLoad in Global := (Command.process("project jvm", _: State)) compose (onLoad in Global).value
44 |
--------------------------------------------------------------------------------
/validation-core/src/main/scala/Format.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | import scala.annotation.implicitNotFound
4 | import cats.Monoid
5 | import cats.Invariant
6 |
7 | @implicitNotFound(
8 | "No Format found for types ${IR},${IW}, ${O}. Try to implement an implicit Format[${IR}, ${IW}, ${O}].")
9 | trait Format[IR, +IW, O] extends RuleLike[IR, O] with WriteLike[O, IW]
10 |
11 | /**
12 | * Default formatters.
13 | */
14 | object Format {
15 | def gen[IR, IW, O]: Format[IR, IW, O] = macro MappingMacros.format[IR, IW, O]
16 |
17 | def apply[IR, IW, O](
18 | r: RuleLike[IR, O], w: WriteLike[O, IW]): Format[IR, IW, O] =
19 | new Format[IR, IW, O] {
20 | def validate(i: IR) = r.validate(i)
21 | def writes(o: O): IW = w.writes(o)
22 | }
23 |
24 | implicit def invariantFormat[IR, IW]: Invariant[Format[IR, IW, ?]] =
25 | new Invariant[Format[IR, IW, ?]] {
26 | def imap[A, B](
27 | fa: Format[IR, IW, A])(f1: A => B)(f2: B => A): Format[IR, IW, B] =
28 | Format[IR, IW, B](
29 | Rule.toRule(fa).map(f1), Write.toWrite(fa).contramap(f2))
30 | }
31 |
32 | implicit def formatSyntaxCombine[IR, IW: Monoid](
33 | implicit rcb: SyntaxCombine[Rule[IR, ?]],
34 | wcb: SyntaxCombine[Write[?, IW]]): SyntaxCombine[Format[IR, IW, ?]] =
35 | new SyntaxCombine[Format[IR, IW, ?]] {
36 | def apply[A, B](fa: Format[IR, IW, A],
37 | fb: Format[IR, IW, B]): Format[IR, IW, A ~ B] =
38 | Format[IR, IW, A ~ B](rcb(Rule.toRule(fa), Rule.toRule(fb)),
39 | wcb(Write.toWrite(fa), Write.toWrite(fb)))
40 | }
41 |
42 | implicit def formatInvariantSyntaxObs[IR, IW: Monoid, O](
43 | f: Format[IR, IW, O])(implicit fcb: SyntaxCombine[Format[IR, IW, ?]])
44 | : InvariantSyntaxObs[Format[IR, IW, ?], O] =
45 | new InvariantSyntaxObs[Format[IR, IW, ?], O](f)(fcb)
46 | }
47 |
--------------------------------------------------------------------------------
/docs/src/main/tut/README.md:
--------------------------------------------------------------------------------
1 | # [The Play data validation library](https://github.com/jto/validation)
2 |
3 | ## Overview
4 |
5 | The Play validation API aims to provide a comprehensive toolkit to validate data from any format against user defined rules, and transform them to other types.
6 |
7 | Basically, assuming you have this:
8 |
9 | ```tut:silent
10 | case class Person(
11 | name: String,
12 | age: Int,
13 | lovesChocolate: Boolean
14 | )
15 | ```
16 |
17 | ```tut:silent
18 | import play.api.libs.json._
19 | import jto.validation.Rule
20 |
21 | val json = Json.parse("""{
22 | "name": "Julien",
23 | "age": 28,
24 | "lovesChocolate": true
25 | }""")
26 |
27 | implicit val personRule = {
28 | import jto.validation.playjson.Rules._
29 | Rule.gen[JsValue, Person]
30 | }
31 | ```
32 |
33 | It can do this:
34 |
35 | ```tut
36 | personRule.validate(json)
37 | ```
38 |
39 | It's also a unification of the [Form Validation API](https://www.playframework.com/documentation/2.3.x/ScalaForms), and the [Json validation API](https://www.playframework.com/documentation/2.3.x/ScalaJsonCombinators).
40 |
41 | Being based on the same concepts as the Json validation API available in previous versions, it should feel very similar to any developer already working with it. The validation API is, rather than a totally new design, a simple generalization of those concepts.
42 |
43 | ## Design
44 |
45 | The validation API is designed around a core defined in package `jto.validation`, and "extensions". Each extension provides primitives to validate and serialize data from / to a particular format ([Json](ScalaValidationJson.md), [form encoded request body](ScalaValidationMigrationForm.md), etc.). See [the extensions documentation](ScalaValidationExtensions.md) for more information.
46 |
47 | To learn more about data validation, please consult [Validation and transformation with Rule](ScalaValidationRule.md), for data serialization read [Serialization with Write](ScalaValidationWrite.md). If you just want to figure all this out by yourself, please see the [Cookbook](ScalaValidationCookbook.md).
48 |
--------------------------------------------------------------------------------
/validation-delimited/src/test/scala/RulesSpec.scala:
--------------------------------------------------------------------------------
1 | import jto.validation._
2 | import jto.validation.delimited._
3 | import jto.validation.delimited.Rules._
4 | import org.scalatest._
5 |
6 | class RulesSpec extends WordSpec with Matchers {
7 | "Rules" should {
8 | "demonstrate typical usage" in {
9 | case class Contact(name: String, email: String, birthday: Option[String])
10 |
11 | val contactReads = From[Delimited] { __ =>
12 | (
13 | (__ \ 0).read[String] ~
14 | (__ \ 1).read(email) ~
15 | (__ \ 2).read(optionR[String](Rules.equalTo("N/A")))
16 | )(Contact)
17 | }
18 |
19 | val csv1 = "Ian Hummel,ian@example.com,1981-07-24"
20 | val csv2 = "Jane Doe,jane@example.com,N/A"
21 |
22 | contactReads.validate(csv1.split(",")) shouldBe Valid(
23 | Contact("Ian Hummel", "ian@example.com", Some("1981-07-24")))
24 | contactReads.validate(csv2.split(",")) shouldBe Valid(
25 | Contact("Jane Doe", "jane@example.com", None))
26 | }
27 |
28 | "read optional values" in {
29 | val str = Array("John Doe", "", "foo", "9393.12")
30 | (Path \ 0).read[Delimited, String].validate(str) shouldBe Valid(
31 | "John Doe")
32 | (Path \ 1).read[Delimited, Option[String]].validate(str) shouldBe Valid(
33 | None)
34 | (Path \ 2).read[Delimited, Option[String]].validate(str) shouldBe Valid(
35 | Some("foo"))
36 | (Path \ 3).read[Delimited, Double].validate(str) shouldBe Valid(9393.12)
37 | }
38 |
39 | "read optional values using a different rule" in {
40 | val str = Array("John Doe", "\\N", "", "9393.12")
41 |
42 | (Path \ 0).read[Delimited, String].validate(str) shouldBe Valid(
43 | "John Doe")
44 | (Path \ 1)
45 | .read[Delimited, Option[String]](optionR(Rules.equalTo("\\N")))
46 | .validate(str) shouldBe Valid(None)
47 | (Path \ 2)
48 | .read[Delimited, Option[String]](optionR(Rules.equalTo("\\N")))
49 | .validate(str) shouldBe Valid(Some(""))
50 | (Path \ 3).read[Delimited, Double].validate(str) shouldBe Valid(9393.12)
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/docs/src/main/tut/V2MigrationGuide.md:
--------------------------------------------------------------------------------
1 | # v2.0 Migration guide
2 |
3 | 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.
4 |
5 | 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.
6 |
7 | #### Build file.
8 |
9 | The project name for play-json based validation has changed.
10 |
11 | ```scala
12 | "io.github.jto" %% "validation-json" % validationVersion
13 | ```
14 |
15 | becomes
16 |
17 | ```scala
18 | "io.github.jto" %% "validation-playjson" % validationVersion
19 | ```
20 |
21 | #### Package name
22 |
23 | - Since the library does not depend on Play anymore and is not planned to be integrated into Play, the package names have changed. Basically `play.api.data.mapping` now becomes `jto.validation`. A simple search and replace in your project should work.
24 | - The validation api support several json representations. Therefore, the package name for play json changes. `play.api.data.mapping.json` becomes `play.api.mapping.playjson`
25 |
26 | #### Rule renaming
27 |
28 | The following `Rule` and `Write` were renamed to better match the naming convention in all subprojects.
29 |
30 | - `Rules.jodaDate` becomes `Rules.jodaDateR`
31 | - `Writes.jodaDate` becomes `Writes.jodaDateW`
32 |
33 | If you encounter implicit resolution problem, you probably have a name clash. Make sure none of your `Rule` / `Write` uses those names.
34 |
35 | #### unlift
36 |
37 | Since validation does not uses play-functional anymore, `unlift` should be imported [directly](https://github.com/playframework/playframework/blob/2.5.3/framework/src/play-functional/src/main/scala/play/api/libs/functional/syntax/package.scala#L31) as `scala.Function.unlift` instead of `play.api.libs.functional.unlift`.
38 |
39 | #### ValidationError
40 |
41 | 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`.
42 |
--------------------------------------------------------------------------------
/docs/src/main/tut/ReleaseNotes.md:
--------------------------------------------------------------------------------
1 | # Release notes
2 |
3 | ### 2.0.1:
4 |
5 | - Add Read / Write utilities [1](https://github.com/jto/validation/commit/c7f223ce65a8b55c6a2a4839364f5559fdeed7a2)
6 |
7 | ### 2.0:
8 |
9 | - Replace `play/functional` with `typelevel/cats` ([#38](https://github.com/jto/validation/pull/38) [1](https://github.com/jto/validation/commit/49110067caea0a483f840ad2334ad05ae379f1cf), [2](https://github.com/jto/validation/commit/a9108a00cbef8b6de668bfb7c7bee44c1b974537), [3](https://github.com/jto/validation/commit/5ccc7895672412da189f4e9efbea97ce7be467be), [4](https://github.com/jto/validation/commit/0ebcd973e24a33f87d280fc8565419d8ad8b9829), [5](https://github.com/jto/validation/commit/6505afe98972f8cf3b356f334db2a9a39b806961), [6](https://github.com/jto/validation/commit/b5aacbe711e59a7fe35d55ec1a63d0b633646ddc), [7](https://github.com/jto/validation/commit/cbe0d5b0310038840af3e1f6fa5668963ae32773), [8](https://github.com/jto/validation/commit/09aca48b858afb20845c21bf7e25faf2ad611cc7), [9](https://github.com/jto/validation/commit/86f63d0cc547d41c6dce36e2877d5b0de8a8cbac))
10 |
11 | - Impove error reporting ([#40](https://github.com/jto/validation/pull/40) [1](https://github.com/jto/validation/commit/357b87778f19fbbc06a49da08cb2dccf9e0a40e3), [2](https://github.com/jto/validation/commit/c1bdac7fcff098b1d85c6881c731d5fd4ee2ac2e))
12 |
13 | - Rework project structure: `json` → `playjson`, `json4s` → `jsonast`, `jsjson` ([#50](https://github.com/jto/validation/pull/50) [0](https://github.com/jto/validation/commit/f95ac30b1d1346a27e26c08841ee06c00340891f) [1](https://github.com/jto/validation/commit/5b36f606334a5fe26715cf0d7c47ebf861acb811), [2](https://github.com/jto/validation/commit/3f31f4917d01b8f6fdefe4adaca70ddc823722db))
14 |
15 | - Add Scala.js support ([#42](https://github.com/jto/validation/pull/42) [1](https://github.com/jto/validation/commit/db359abfbe90d2b3b853beabcbabe88ecd1cfddb), [2](https://github.com/jto/validation/commit/568aa1fa1df06d775abb583cac8da679c1301227), [3](https://github.com/jto/validation/commit/d67d6dee7d99d27d6cf751cc69b83235b57a8246), [4](https://github.com/jto/validation/commit/67499a823ff463860b72d6697cf45b5764c475b2), [5](https://github.com/jto/validation/commit/d720ba265541a90f225c388043b5430a68e9fff3))
16 |
--------------------------------------------------------------------------------
/validation-jsonast/js/src/main/scala/Ast.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package jsonast
3 |
4 | import scala.scalajs.js
5 | import scala.scalajs.js.JSConverters._
6 |
7 | object Ast {
8 | val to: Write[JValue, js.Dynamic] = Write[JValue, js.Any] {
9 | case JNull => null
10 | case JObject (value) => value.mapValues(to.writes).toJSDictionary
11 | case JArray (value) => value.map(to.writes).toJSArray
12 | case JBoolean(value) => value
13 | case JString (value) => value
14 | case JNumber (value) =>
15 | val d = value.toDouble
16 | if (d.isNaN || d.isInfinity) null else d
17 | }.map(_.asInstanceOf[js.Dynamic])
18 |
19 | private val undefined = scala.scalajs.js.undefined
20 | private case class FunctionInJsonException(path: Path) extends Exception
21 |
22 | private def unsafeAny2JValue(input: Any, path: Path): JValue = input match {
23 | case null => JNull
24 | case s: String => JString(s)
25 | case b: Boolean => JBoolean(b)
26 | case d: Double => JNumber(d.toString)
27 | case `undefined` => JNull
28 |
29 | case a: js.Array[js.Dynamic @unchecked] =>
30 | JArray(a.map(v => unsafeAny2JValue(v, path \ 0)))
31 |
32 | case o: js.Object =>
33 | JObject(o.asInstanceOf[js.Dictionary[js.Dynamic]]
34 | .map { case (k, v) => k -> unsafeAny2JValue(v, path \ k) }.toMap)
35 |
36 | case _ =>
37 | // This is a trade off between the various option to handle js.Function in json objects.
38 | // We could also go one step further and return all the paths which contain functions,
39 | // but this would imply sequence over Validated, which would throw away the perfs in
40 | // the general case.
41 | //
42 | // This is what other are doing:
43 | // - The native JSON.stringity is completely silent.
44 | // - Circe parses then as nulls https://goo.gl/iQ0ANV.
45 | throw new FunctionInJsonException(path)
46 | }
47 |
48 | val from: Rule[js.Dynamic, JValue] = Rule { j =>
49 | try {
50 | Valid(unsafeAny2JValue(j, Path))
51 | } catch {
52 | case FunctionInJsonException(path) =>
53 | Invalid(Seq(path -> Seq(ValidationError("Json cannot contain functions."))))
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/validation-form/src/main/scala/Writes.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package forms
3 |
4 | import cats.Monoid
5 |
6 | trait DefaultMonoids {
7 | implicit def mapMonoid = new Monoid[UrlFormEncoded] {
8 | def combine(a1: UrlFormEncoded, a2: UrlFormEncoded) = a1 ++ a2
9 | def empty = Map.empty
10 | }
11 | }
12 |
13 | trait Writes
14 | extends DefaultWrites
15 | with GenericWrites[PM.PM]
16 | with DefaultMonoids {
17 | import PM._
18 |
19 | implicit val intW: Write[Int, String] = Write(_.toString)
20 | implicit val shortW: Write[Short, String] = Write(_.toString)
21 | implicit val booleanW: Write[Boolean, String] = Write(_.toString)
22 | implicit val longW: Write[Long, String] = Write(_.toString)
23 | implicit val floatW: Write[Float, String] = Write(_.toString)
24 | implicit val doubleW: Write[Double, String] = Write(_.toString)
25 | implicit val bigDecimalW: Write[BigDecimal, String] = Write(_.toString)
26 | implicit def scalanumber[T <: scala.math.ScalaNumber] =
27 | Write((i: T) => i.toString)
28 | implicit def javanumber[T <: java.lang.Number] = Write((i: T) => i.toString)
29 |
30 | implicit def opm[O](implicit w: WriteLike[O, UrlFormEncoded]) =
31 | Write[O, PM] { o =>
32 | toPM(w.writes(o))
33 | }
34 |
35 | implicit def mapW[I](implicit w: WriteLike[I, Seq[String]]) =
36 | Write[Map[String, I], PM] { m =>
37 | toPM(m.mapValues(w.writes))
38 | }
39 |
40 | implicit def spm[O](implicit w: WriteLike[O, PM]) =
41 | Write[Seq[O], PM] { os =>
42 | os.zipWithIndex.map(_.swap).toMap.flatMap {
43 | case (i, o) =>
44 | repathPM(w.writes(o), (Path \ i) ++ _)
45 | }
46 | }
47 |
48 | implicit def writeM[I](path: Path)(implicit w: WriteLike[I, PM]) =
49 | Write[I, UrlFormEncoded] { i =>
50 | toM(repathPM(w.writes(i), path ++ _))
51 | }
52 |
53 | implicit def ospm[I](implicit w: WriteLike[I, String]) = Write[I, PM] { i =>
54 | Map(Path -> w.writes(i))
55 | }
56 |
57 | implicit def optW[I](implicit w: Path => WriteLike[I, UrlFormEncoded])
58 | : Path => Write[Option[I], UrlFormEncoded] =
59 | optionW[I, I](Write.zero[I])
60 |
61 | def optionW[I, J](r: => WriteLike[I, J])(
62 | implicit w: Path => WriteLike[J, UrlFormEncoded]) =
63 | super.optionW[I, J, UrlFormEncoded](r, Map.empty)
64 | }
65 |
66 | object Writes extends Writes
67 |
--------------------------------------------------------------------------------
/validation-core/src/main/scala/Write.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | import cats.Monoid
4 | import cats.Contravariant
5 |
6 | trait WriteLike[I, +O] {
7 |
8 | /**
9 | * "Serialize" `i` to the output type
10 | */
11 | def writes(i: I): O
12 | }
13 |
14 | object WriteLike {
15 | implicit def zero[I]: WriteLike[I, I] = Write(identity[I] _)
16 | }
17 |
18 | trait Write[I, +O] extends WriteLike[I, O] {
19 |
20 | /**
21 | * returns a new Write that applies function `f` to the result of this write.
22 | * {{{
23 | * val w = Writes.int.map("Number: " + _)
24 | * w.writes(42) == "Number: 42"
25 | * }}}
26 | */
27 | def map[B](f: O => B): Write[I, B] =
28 | Write[I, B] {
29 | f.compose(x => this.writes(x))
30 | }
31 |
32 | @deprecated("use andThen instead.", "2.0")
33 | def compose[OO >: O, P](w: WriteLike[OO, P]): Write[I, P] = andThen(w)
34 |
35 | /**
36 | * Returns a new Write that applies `this` Write, and then applies `w` to its result
37 | */
38 | def andThen[OO >: O, P](w: WriteLike[OO, P]): Write[I, P] =
39 | this.map(o => w.writes(o))
40 |
41 | def contramap[B](f: B => I): Write[B, O] =
42 | Write[B, O]((b: B) => writes(f(b)))
43 | }
44 |
45 | object Write {
46 | def gen[I, O]: Write[I, O] = macro MappingMacros.write[I, O]
47 |
48 | def apply[I, O](w: I => O): Write[I, O] =
49 | new Write[I, O] {
50 | def writes(i: I) = w(i)
51 | }
52 |
53 | sealed trait Deferred[O] {
54 | def apply[I](i: I)(implicit w: WriteLike[I, O]) = w.writes(i)
55 | }
56 |
57 | def apply[O] = new Deferred[O]{}
58 |
59 | def of[I, O](implicit w: Write[I, O]): Write[I, O] = w
60 |
61 | def toWrite[I, O](r: WriteLike[I, O]): Write[I, O] =
62 | new Write[I, O] {
63 | def writes(data: I): O = r.writes(data)
64 | }
65 |
66 | implicit def zero[I]: Write[I, I] =
67 | toWrite(WriteLike.zero[I])
68 |
69 | implicit def contravariantWrite[O]: Contravariant[Write[?, O]] =
70 | new Contravariant[Write[?, O]] {
71 | def contramap[A, B](wa: Write[A, O])(f: B => A): Write[B, O] =
72 | wa.contramap(f)
73 | }
74 |
75 | implicit def writeSyntaxCombine[O](
76 | implicit m: Monoid[O]): SyntaxCombine[Write[?, O]] =
77 | new SyntaxCombine[Write[?, O]] {
78 | def apply[A, B](wa: Write[A, O], wb: Write[B, O]): Write[A ~ B, O] =
79 | Write[A ~ B, O] {
80 | case a ~ b => m.combine(wa.writes(a), wb.writes(b))
81 | }
82 | }
83 |
84 | implicit def writeContravariantSyntaxObs[I, O: Monoid](
85 | w: Write[I, O])(implicit fcb: SyntaxCombine[Write[?, O]])
86 | : ContravariantSyntaxObs[Write[?, O], I] =
87 | new ContravariantSyntaxObs[Write[?, O], I](w)(fcb)
88 | }
89 |
--------------------------------------------------------------------------------
/docs/src/main/tut/ScalaValidationWrite.md:
--------------------------------------------------------------------------------
1 | # Serializing data
2 |
3 | ## Introduction
4 |
5 | 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.
6 |
7 | ## A simple example
8 |
9 | Let's say you want to serialize a `Float` to `String`.
10 | All you need to do is to define a `Write` from `Float` to `String`:
11 |
12 | ```tut:silent
13 | import jto.validation._
14 | def floatToString: Write[Float, String] = ???
15 | ```
16 |
17 | For now we'll not implement `floatToString`, actually, the validation API comes with a number of built-in Writes, including `Writes.floatW[T]`.
18 |
19 | All you have to do is import the default Writes.
20 |
21 | ```tut:silent
22 | object Writes extends NumericTypes2StringWrites
23 | Writes.floatW
24 | ```
25 |
26 | Let's now test it against different `Float` values:
27 |
28 | ```tut
29 | Writes.floatW.writes(12.8F)
30 | Writes.floatW.writes(12F)
31 | ```
32 |
33 | ## Defining your own `Write`
34 |
35 | Creating a new `Write` is almost as simple as creating a new function.
36 | This example creates a new `Write` serializing a Float with a custom format.
37 |
38 | ```tut:silent
39 | val currency = Write[Double, String]{ money =>
40 | import java.text.NumberFormat
41 | import java.util.Locale
42 | val f = NumberFormat.getCurrencyInstance(Locale.FRANCE)
43 | f.format(money)
44 | }
45 | ```
46 |
47 | Testing it:
48 |
49 | ```tut
50 | currency.writes(9.99)
51 | ```
52 |
53 | ## Composing Writes
54 |
55 | 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]`.
56 |
57 | ### Example
58 |
59 | Let's see we're working on an e-commerce website. We have defined a `Product` class.
60 | Each product has a name and a price:
61 |
62 | ```tut:silent
63 | case class Product(name: String, price: Double)
64 | ```
65 |
66 | 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 €`
67 |
68 | We have already defined `currency: Write[Double, String]`, so we'd like to reuse that.
69 | First, we'll create a `Write[Product, Double]` extracting the price of the product:
70 |
71 | ```tut:silent
72 | val productPrice: Write[Product, Double] = Write[Product, Double](_.price)
73 | ```
74 |
75 | Now we just have to compose it with `currency`:
76 |
77 | ```tut:silent
78 | val productAsPrice: Write[Product,String] = productPrice andThen currency
79 | ```
80 |
81 | Let's test our new `Write`:
82 |
83 | ```tut
84 | productAsPrice.writes(Product("Awesome product", 9.99))
85 | ```
86 |
--------------------------------------------------------------------------------
/docs/src/main/tut/ScalaValidationMacros.md:
--------------------------------------------------------------------------------
1 | # Validation Inception
2 |
3 | ## Introduction
4 |
5 | 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).
6 |
7 | The generated code:
8 |
9 | - is completely typesafe
10 | - is compiled
11 | - does not rely on runtime introspection **at all**
12 | - is strictly equivalent to a hand-written definition
13 |
14 | ## Example
15 |
16 | Traditionally, for a given case class `Person` we would define a `Rule` like this:
17 |
18 | ```tut
19 | case class Person(name: String, age: Int, lovesChocolate: Boolean)
20 | ```
21 |
22 | ```tut:silent
23 | import jto.validation._
24 | import play.api.libs.json._
25 |
26 | implicit val personRule: Rule[JsValue, Person] = From[JsValue] { __ =>
27 | import jto.validation.playjson.Rules._
28 | ((__ \ "name").read[String] ~
29 | (__ \ "age").read[Int] ~
30 | (__ \ "lovesChocolate").read[Boolean])(Person.apply)
31 | }
32 | ```
33 |
34 | Let's test it:
35 |
36 | ```tut
37 | val json = Json.parse("""{
38 | "name": "Julien",
39 | "age": 28,
40 | "lovesChocolate": true
41 | }""")
42 |
43 | personRule.validate(json)
44 | ```
45 |
46 | The exact same `Rule` can be generated using `Rule.gen`:
47 |
48 | ```tut:silent
49 | import jto.validation._
50 | import play.api.libs.json._
51 |
52 | implicit val personRule = {
53 | import jto.validation.playjson.Rules._ // let's not leak implicits everywhere
54 | Rule.gen[JsValue, Person]
55 | }
56 | ```
57 |
58 | The validation result is identical :
59 |
60 | ```tut
61 | val json = Json.parse("""{
62 | "name": "Julien",
63 | "age": 28,
64 | "lovesChocolate": true
65 | }""")
66 |
67 | personRule.validate(json)
68 | ```
69 |
70 | Similarly we can generate a `Write`:
71 |
72 | ```tut:silent
73 | import jto.validation._
74 | import play.api.libs.json._
75 |
76 | implicit val personWrite = {
77 | import jto.validation.playjson.Writes._ // let's no leak implicits everywhere
78 | Write.gen[Person, JsObject]
79 | }
80 | ```
81 | ```tut
82 | personWrite.writes(Person("Julien", 28, true))
83 | ```
84 |
85 | ## Known limitations
86 |
87 | - **Don’t override the apply method of the companion object.** The macro inspects the `apply` method to generate `Rule`/`Write`. Overloading the `apply` method creates an ambiguity the compiler will complain about.
88 | - **Macros only work when `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.
89 | - **Validation Macros accept `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.
90 |
--------------------------------------------------------------------------------
/validation-xml/src/main/scala/Writes.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package xml
3 |
4 | import cats.Monoid
5 | import scala.xml._
6 |
7 | trait DefaultMonoids {
8 | // We define a monoid of the endofunctor xml.Elem => xml.Elem (alias XmlWriter)
9 | // Monoid[XmlWriter] thus has the propriety of a Monad in xml.Elem (being a monoid in the category of endofunctor)
10 | implicit def xmlMonoid = new Monoid[XmlWriter] {
11 | def combine(a1: XmlWriter, a2: XmlWriter): XmlWriter = a1 andThen a2
12 | def empty: XmlWriter = identity
13 | }
14 | }
15 |
16 | trait Writes
17 | extends DefaultWrites
18 | with NumericTypes2StringWrites
19 | with DefaultMonoids
20 | with GenericWrites[XmlWriter] {
21 |
22 | implicit def nodeW[I](
23 | implicit w: WriteLike[I, String]): Write[I, XmlWriter] = Write {
24 | i => node =>
25 | node.copy(child = node.child :+ new Text(w.writes(i)))
26 | }
27 |
28 | def attributeW[I](name: String)(
29 | implicit w: WriteLike[I, String]): Write[I, XmlWriter] = Write {
30 | i => node =>
31 | node.copy(attributes = node.attributes.append(
32 | new UnprefixedAttribute(name, w.writes(i), Null)))
33 | }
34 |
35 | def optAttributeW[I](name: String)(
36 | implicit w: WriteLike[I, String]): Write[Option[I], XmlWriter] = Write {
37 | case Some(i) => attributeW(name)(w).writes(i)
38 | case None => xmlMonoid.empty
39 | }
40 |
41 | implicit def writeXml[I](path: Path)(
42 | implicit w: WriteLike[I, XmlWriter]): Write[I, XmlWriter] = Write { i =>
43 | val reversedPath = path.path.reverse
44 | reversedPath match {
45 | case Nil => w.writes(i)
46 |
47 | case KeyPathNode(key) :: tail =>
48 | val lastElem =
49 | w.writes(i).apply(new Elem(null, key, Null, TopScope, false))
50 | val newNode = tail.foldLeft(lastElem) {
51 | case (acc, IdxPathNode(_)) => acc
52 | case (acc, KeyPathNode(key)) =>
53 | new Elem(null, key, Null, TopScope, false, acc)
54 | }
55 | node =>
56 | node.copy(child = node.child :+ newNode)
57 |
58 | case IdxPathNode(_) :: _ =>
59 | throw new RuntimeException(
60 | "cannot write an attribute to a node with an index path")
61 | }
62 | }
63 |
64 | implicit def seqToNodeSeq[I](
65 | implicit w: WriteLike[I, XmlWriter]): Write[Seq[I], XmlWriter] = Write {
66 | is =>
67 | is.map(w.writes).foldLeft(xmlMonoid.empty)(xmlMonoid.combine)
68 | }
69 |
70 | def optionW[I, J](r: => WriteLike[I, J])(
71 | implicit w: Path => WriteLike[J, XmlWriter])
72 | : Path => Write[Option[I], XmlWriter] =
73 | super.optionW[I, J, XmlWriter](r, xmlMonoid.empty)
74 |
75 | implicit def optionW[I](implicit w: Path => WriteLike[I, XmlWriter])
76 | : Path => Write[Option[I], XmlWriter] =
77 | optionW(Write.zero[I])
78 | }
79 |
80 | object Writes extends Writes
81 |
--------------------------------------------------------------------------------
/validation-core/src/main/scala/Path.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | sealed trait PathNode
4 | case class KeyPathNode(key: String) extends PathNode {
5 | override def toString = key
6 | }
7 |
8 | case class IdxPathNode(idx: Int) extends PathNode {
9 | override def toString = s"[$idx]"
10 | }
11 |
12 | object \: {
13 | def unapply(path: Path): Option[(Path, Path)] = {
14 | path match {
15 | case Path(n :: ns) => Some((Path \ n) -> Path(ns))
16 | case Path(Nil) => None
17 | }
18 | }
19 | }
20 |
21 | case object Path extends Path(Nil) {
22 | def apply(path: String) = new Path(KeyPathNode(path) :: Nil)
23 | def apply(path: List[PathNode] = Nil) = new Path(path)
24 | def unapply(p: Path): Option[List[PathNode]] = Some(p.path)
25 | }
26 |
27 | class Path(val path: List[PathNode]) {
28 |
29 | def \(key: String): Path = this \ KeyPathNode(key)
30 | def \(idx: Int): Path = this \ IdxPathNode(idx)
31 | def \(child: PathNode): Path = Path(path :+ child)
32 |
33 | /**
34 | * Aggregate 2 paths
35 | * {{{
36 | * (Path \ "foo" \ "bar") .andThen(Path \ "baz") == (Path \ "foo" \ "bar" \ "baz")
37 | * }}}
38 | */
39 | def compose(p: Path): Path = Path(this.path ++ p.path)
40 | def ++(other: Path) = this compose other
41 |
42 | class Deferred[I] private[Path](reader: Reader[I]) {
43 | def apply[J, O](sub: => RuleLike[J, O])(
44 | implicit r: Path => RuleLike[I, J]): Rule[I, O] =
45 | reader.read(sub)
46 | }
47 |
48 | def from[I] = new Deferred(Reader[I](this))
49 |
50 | def read[I, J, O](sub: => RuleLike[J, O])(
51 | implicit r: Path => RuleLike[I, J]): Rule[I, O] =
52 | Reader[I](this).read(sub)
53 |
54 | def read[I, O](implicit r: Path => RuleLike[I, O]): Rule[I, O] =
55 | Reader[I](this).read[O]
56 |
57 | /**
58 | * Creates a Writes the serialize data to the desired output type
59 | * {{{
60 | * val contact = Contact("Julien", "Tournay")
61 | * implicit def contactWrite = (Path \ "firstname").write[String, UrlFormEncoded]
62 | * contactWrite.writes(contact) shouldBe Map("firstname" -> "Julien")
63 | * }}}
64 | */
65 | def write[I, O](implicit w: Path => WriteLike[I, O]): Write[I, O] =
66 | Writer[O](this).write(w)
67 |
68 | /**
69 | * Creates a Writes the serialize data to the desired output type using a provided format.
70 | * * {{{
71 | * val w = (Path \ "date").write(date("yyyy-MM-dd""))
72 | * w.writes(new Date()) == Json.obj("date" -> "2013-10-3")
73 | * }}}
74 | */
75 | def write[I, J, O](format: => WriteLike[I, J])(
76 | implicit w: Path => WriteLike[J, O]): Write[I, O] =
77 | Writer[O](this).write(format)
78 |
79 | override def toString = this.path match {
80 | case Nil => "/"
81 | case hs =>
82 | hs.foldLeft("") {
83 | case (path, IdxPathNode(i)) => path + s"[$i]"
84 | case (path, KeyPathNode(k)) => path + "/" + k
85 | }
86 | }
87 |
88 | override def hashCode = path.hashCode
89 | override def equals(o: Any) = {
90 | if (canEqual(o)) {
91 | val j = o.asInstanceOf[Path]
92 | this.path == j.path
93 | } else
94 | false
95 | }
96 | def canEqual(o: Any) = o.isInstanceOf[Path]
97 | }
98 |
--------------------------------------------------------------------------------
/validation-core/src/test/scala/ValidationSpec.scala:
--------------------------------------------------------------------------------
1 | import jto.validation._
2 |
3 | import org.scalatest._
4 |
5 | class ValidatedSpec extends WordSpec with Matchers {
6 |
7 | "Validated" should {
8 |
9 | val success: Validated[Seq[String], Int] = Valid[Int](5)
10 | val failure: Validated[Seq[String], Int] =
11 | Invalid[Seq[String]]("err" :: Nil)
12 |
13 | "be a Functor" in {
14 | // identity
15 | success.map(identity) shouldBe (success)
16 | failure.map(identity) shouldBe (failure)
17 | // composition
18 | val p = (_: Int) + 2
19 | val q = (_: Int) * 3
20 | success.map(p compose q) shouldBe (success.map(q).map(p))
21 | failure.map(p compose q) shouldBe (failure.map(q).map(p))
22 |
23 | success.map(_ + 2) shouldBe (Valid[Int](7))
24 | failure.map(_ + 2) shouldBe (failure)
25 | }
26 |
27 | "be foldable" in {
28 | success.fold(
29 | err => "err",
30 | identity
31 | ) shouldBe (5)
32 |
33 | failure.fold(
34 | err => "err",
35 | identity
36 | ) shouldBe ("err")
37 | }
38 |
39 | "have an Applicative" in {
40 | val app = implicitly[cats.Applicative[Validated[Seq[String], ?]]]
41 |
42 | val u: Validated[Seq[String], Int => Int] = Valid[Int => Int](_ + 2)
43 | val v: Validated[Seq[String], Int => Int] = Valid[Int => Int](_ * 3)
44 | val w: Validated[Seq[String], Int] = Valid[Int](5)
45 |
46 | app.ap(app.pure((_: Int) + 2))(app.pure(5)) shouldBe (app.pure(7))
47 |
48 | // identity
49 | app.ap(app.pure[Int => Int](identity _))(success) shouldBe (success)
50 | app.ap(app.pure[Int => Int](identity _))(failure) shouldBe (failure)
51 |
52 | // composition
53 | val p = app.pure((f: Int => Int) => f compose (_: Int => Int))
54 | app.ap(app.ap(app.ap(p)(u))(v))(w) shouldBe (app.ap(u)(app.ap(v)(w)))
55 |
56 | // homomorphism
57 | val f = (_: Int) + 2
58 | val x = 5
59 |
60 | app.ap(app.pure(f))(app.pure(x)) shouldBe (app.pure(f(x)))
61 |
62 | // interchange
63 | app.ap(u)(app.pure(x)) shouldBe (app.ap(
64 | app.pure((f: Int => Int) => f(x)))(u))
65 | }
66 |
67 | /*
68 | "implement filter" in {
69 | success.filter((_: Int) == 5) shouldBe(success)
70 | failure.filter((_: Int) == 5) shouldBe(failure)
71 | }
72 | */
73 |
74 | "have recovery methods" in {
75 | success.getOrElse(42) shouldBe (5)
76 | failure.getOrElse(42) shouldBe (42)
77 |
78 | success.orElse(Valid(42)) shouldBe (success)
79 | failure.getOrElse(Valid(42)) shouldBe (Valid(42))
80 | }
81 |
82 | "be easily convertible to scala standars API types" in {
83 | success.toOption shouldBe (Some(5))
84 | failure.toOption shouldBe (None)
85 |
86 | success.toEither shouldBe (Right(5))
87 | failure.toEither shouldBe (Left("err" :: Nil))
88 | }
89 |
90 | "sequence" in {
91 | val f1: Validated[List[String], String] = Invalid(List("err1"))
92 | val f2: Validated[List[String], String] = Invalid(List("err2"))
93 | val s1: Validated[List[String], String] = Valid("1")
94 | val s2: Validated[List[String], String] = Valid("2")
95 |
96 | import cats.instances.list._
97 | import cats.syntax.traverse._
98 | type VS[X] = Validated[List[String], X]
99 | List(s1, s2).sequence[VS, String] shouldBe (Valid(List("1", "2")))
100 | List(f1, f2).sequence[VS, String] shouldBe (Invalid(List("err1", "err2")))
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/validation-jsonast/shared/src/main/scala/Writes.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package jsonast
3 |
4 | import cats.Monoid
5 |
6 | trait DefaultMonoids {
7 | implicit def jsonMonoid = new Monoid[JObject] {
8 | // TODO: Should this be a deepMerge?
9 | def combine(a1: JObject, a2: JObject) = JObject(a1.value ++ a2.value)
10 | def empty = JObject()
11 | }
12 | }
13 |
14 | trait Writes
15 | extends DefaultWrites
16 | with DefaultMonoids
17 | with GenericWrites[JValue] {
18 | private def writeObj(j: JValue, n: PathNode) = n match {
19 | case IdxPathNode(_) => JArray(Vector(j))
20 | case KeyPathNode(key) => JObject(Map(key -> j))
21 | }
22 |
23 | implicit val validationErrorW = Write[ValidationError, JValue] { err =>
24 | JObject(
25 | Map("msg" -> JString(err.message),
26 | "args" -> err.args.foldLeft(JArray()) { (arr, arg) =>
27 | JArray((arr.value :+ JString(arg.toString)).toVector)
28 | }))
29 | }
30 |
31 | implicit def errorsW(
32 | implicit wErrs: WriteLike[Seq[ValidationError], JValue]) =
33 | Write[(Path, Seq[ValidationError]), JObject] {
34 | case (p, errs) =>
35 | JObject(Map(p.toString -> wErrs.writes(errs)))
36 | }
37 |
38 | implicit def failureW(
39 | implicit w: WriteLike[(Path, Seq[ValidationError]), JObject]) =
40 | Write[Invalid[Seq[(Path, Seq[ValidationError])]], JObject] {
41 | case Invalid(errs) =>
42 | errs.map(w.writes).reduce(jsonMonoid.combine)
43 | }
44 |
45 | implicit val stringW: Write[String, JValue] = Write(s => JString(s))
46 |
47 | private def tToJs[T]: Write[T, JValue] =
48 | Write[T, JValue](i => JNumber(i.toString))
49 |
50 | implicit val intW = tToJs[Int]
51 | implicit val shortW = tToJs[Short]
52 | implicit val longW = tToJs[Long]
53 | implicit val floatW = tToJs[Float]
54 | implicit val doubleW = tToJs[Double]
55 | implicit val bigDecimalW: Write[BigDecimal, JValue] =
56 | Write[BigDecimal, JValue](b => JNumber(b.toString))
57 | implicit def javanumberW[T <: java.lang.Number] = tToJs[T]
58 |
59 | implicit def booleanW = Write[Boolean, JValue](JBoolean.apply)
60 |
61 | implicit def seqToJsArray[I](
62 | implicit w: WriteLike[I, JValue]): Write[Seq[I], JValue] =
63 | Write(ss => JArray(ss.map(w.writes _).toVector))
64 |
65 | def optionW[I, J](r: => WriteLike[I, J])(
66 | implicit w: Path => WriteLike[J, JObject])
67 | : Path => Write[Option[I], JObject] =
68 | super.optionW[I, J, JObject](r, JObject())
69 |
70 | implicit def optionW[I](implicit w: Path => WriteLike[I, JObject])
71 | : Path => Write[Option[I], JObject] =
72 | optionW(Write.zero[I])
73 |
74 | implicit def mapW[I](implicit w: WriteLike[I, JValue]) =
75 | Write[Map[String, I], JObject] { m =>
76 | JObject(m.mapValues(w.writes))
77 | }
78 |
79 | implicit def vaW[I](implicit w: WriteLike[I, JValue]) =
80 | Write[VA[I], JObject] { va =>
81 | JObject(
82 | Map(
83 | "isValid" -> JBoolean(va.isValid),
84 | "output" -> va.fold(_ => JNull, w.writes),
85 | "errors" -> va.fold(e => failureW.writes(Invalid(e)), _ => JNull)
86 | ))
87 | }
88 |
89 | implicit def writeJson[I](path: Path)(
90 | implicit w: WriteLike[I, JValue]): Write[I, JObject] = Write { i =>
91 | path match {
92 | case Path(KeyPathNode(x) :: _) \: _ =>
93 | val ps = path.path.reverse
94 | val h = ps.head
95 | val o = writeObj(w.writes(i), h)
96 | ps.tail.foldLeft(o)(writeObj).asInstanceOf[JObject]
97 | case Path(Nil) =>
98 | w.writes(i).asInstanceOf[JObject]
99 | case _ =>
100 | throw new RuntimeException(s"path $path is not a path of JsObject") // XXX: should be a compile time error
101 | }
102 | }
103 | }
104 |
105 | object Writes extends Writes
106 |
--------------------------------------------------------------------------------
/validation-playjson/src/main/scala/Writes.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package playjson
3 |
4 | import play.api.libs.json.{JsValue, JsObject, Json, JsString, JsNumber, JsBoolean, JsArray}
5 |
6 | trait DefaultMonoids {
7 | import cats.Monoid
8 |
9 | implicit def jsonMonoid = new Monoid[JsObject] {
10 | def combine(a1: JsObject, a2: JsObject): JsObject = a1 deepMerge a2
11 | def empty: JsObject = Json.obj()
12 | }
13 | }
14 |
15 | trait Writes
16 | extends DefaultWrites
17 | with DefaultMonoids
18 | with GenericWrites[JsValue] {
19 |
20 | private def writeObj(j: JsValue, n: PathNode) = n match {
21 | case IdxPathNode(_) => Json.arr(j)
22 | case KeyPathNode(key) => Json.obj(key -> j)
23 | }
24 |
25 | implicit val validationErrorW = Write[ValidationError, JsValue] { err =>
26 | Json.obj("msg" -> JsString(err.message),
27 | "args" -> err.args.foldLeft(Json.arr()) { (arr, arg) =>
28 | arr :+
29 | (arg match {
30 | case s: String => JsString(s)
31 | case nb: Int => JsNumber(nb)
32 | case nb: Short => JsNumber(nb)
33 | case nb: Long => JsNumber(nb)
34 | case nb: Double => JsNumber(nb)
35 | case nb: Float => JsNumber(nb)
36 | case b: Boolean => JsBoolean(b)
37 | case js: JsValue => js
38 | case x => JsString(x.toString)
39 | })
40 | })
41 | }
42 |
43 | implicit def errorsW(
44 | implicit wErrs: WriteLike[Seq[ValidationError], JsValue]) =
45 | Write[(Path, Seq[ValidationError]), JsObject] {
46 | case (p, errs) =>
47 | Json.obj(p.toString -> wErrs.writes(errs))
48 | }
49 |
50 | implicit def failureW(
51 | implicit w: WriteLike[(Path, Seq[ValidationError]), JsObject]) =
52 | Write[Invalid[Seq[(Path, Seq[ValidationError])]], JsObject] {
53 | case Invalid(errs) =>
54 | errs.map(w.writes).reduce(_ ++ _)
55 | }
56 |
57 | implicit val string: Write[String, JsValue] = Write(s => JsString(s))
58 |
59 | private def tToJs[T] =
60 | Write[T, JsValue]((i: T) => JsNumber(BigDecimal(i.toString)))
61 | implicit def javanumber[T <: java.lang.Number] = tToJs[T]
62 |
63 | implicit val intW = tToJs[Int]
64 | implicit val shortW = tToJs[Short]
65 | implicit val longW = tToJs[Long]
66 | implicit val floatW = tToJs[Float]
67 | implicit val doubleW = tToJs[Double]
68 |
69 | implicit val bigDecimalW = Write[BigDecimal, JsValue](JsNumber.apply)
70 |
71 | implicit def booleanW = Write[Boolean, JsValue](JsBoolean.apply)
72 |
73 | implicit def seqToJsArray[I](
74 | implicit w: WriteLike[I, JsValue]): Write[Seq[I], JsValue] =
75 | Write(ss => JsArray(ss.map(w.writes _)))
76 |
77 | def optionW[I, J](r: => WriteLike[I, J])(
78 | implicit w: Path => WriteLike[J, JsObject])
79 | : Path => Write[Option[I], JsObject] =
80 | super.optionW[I, J, JsObject](r, Json.obj())
81 |
82 | implicit def optionW[I](implicit w: Path => WriteLike[I, JsObject])
83 | : Path => Write[Option[I], JsObject] =
84 | optionW(Write.zero[I])
85 |
86 | implicit def mapW[I](implicit w: WriteLike[I, JsValue]) =
87 | Write[Map[String, I], JsObject] { m =>
88 | JsObject(m.mapValues(w.writes).toSeq)
89 | }
90 |
91 | implicit def writeJson[I](path: Path)(
92 | implicit w: WriteLike[I, JsValue]): Write[I, JsObject] = Write { i =>
93 | path match {
94 | case Path(KeyPathNode(x) :: _) \: _ =>
95 | val ps = path.path.reverse
96 | val h = ps.head
97 | val o = writeObj(w.writes(i), h)
98 | ps.tail.foldLeft(o)(writeObj).asInstanceOf[JsObject]
99 | case _ =>
100 | throw new RuntimeException(s"path $path is not a path of JsObject") // XXX: should be a compile time error
101 | }
102 | }
103 | }
104 |
105 | object Writes extends Writes
106 |
--------------------------------------------------------------------------------
/validation-core/src/main/scala/DefaultWrites.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | trait DateWrites {
4 | def dateW(pattern: String): Write[java.util.Date, String] =
5 | Write[java.util.Date, String] { d =>
6 | new java.text.SimpleDateFormat(pattern).format(d)
7 | }
8 |
9 | implicit def dateW: Write[java.util.Date, String] =
10 | dateW("yyyy-MM-dd")
11 |
12 | def localDateW(pattern: String): Write[java.time.LocalDate, String] =
13 | Write[java.time.LocalDate, String] { d =>
14 | java.time.format.DateTimeFormatter.ofPattern(pattern).format(d)
15 | }
16 |
17 | implicit def localDateW: Write[java.time.LocalDate, String] =
18 | localDateW("yyyy-MM-dd")
19 |
20 | def zonedDateTimeW(pattern: String): Write[java.time.ZonedDateTime, String] =
21 | Write[java.time.ZonedDateTime, String] { d =>
22 | java.time.format.DateTimeFormatter.ofPattern(pattern).format(d)
23 | }
24 |
25 | implicit def zonedDateTimeW: Write[java.time.ZonedDateTime, String] =
26 | zonedDateTimeW("yyyy-MM-dd")
27 |
28 | implicit def timeW: Write[java.time.LocalDateTime, Long] =
29 | Write[java.time.LocalDateTime, Long](_.toInstant(java.time.ZoneOffset.UTC).toEpochMilli)
30 |
31 | def isoDateW: Write[java.util.Date, String] =
32 | Write[java.util.Date, String] { d =>
33 | import org.joda.time.format.ISODateTimeFormat
34 | val fmt = ISODateTimeFormat.dateTimeNoMillis()
35 | fmt.print(d.getTime)
36 | }
37 |
38 | def jodaDateW(pattern: String): Write[org.joda.time.DateTime, String] =
39 | Write[org.joda.time.DateTime, String] { d =>
40 | val fmt = org.joda.time.format.DateTimeFormat.forPattern(pattern)
41 | fmt.print(d)
42 | }
43 |
44 | implicit def jodaDateW: Write[org.joda.time.DateTime, String] =
45 | jodaDateW("yyyy-MM-dd")
46 |
47 | implicit def jodaTimeW: Write[org.joda.time.DateTime, Long] =
48 | Write[org.joda.time.DateTime, Long](_.getMillis)
49 |
50 | def jodaLocalDateW(pattern: String): Write[org.joda.time.LocalDate, String] =
51 | Write[org.joda.time.LocalDate, String] { d =>
52 | import org.joda.time.format.{DateTimeFormat, ISODateTimeFormat}
53 | val fmt =
54 | if (pattern == "") ISODateTimeFormat.date
55 | else DateTimeFormat.forPattern(pattern)
56 | fmt.print(d)
57 | }
58 |
59 | implicit def jodaLocalDateW: Write[org.joda.time.LocalDate, String] =
60 | jodaLocalDateW("")
61 |
62 | def sqlDateW(pattern: String): Write[java.sql.Date, String] =
63 | dateW(pattern).contramap(d => new java.util.Date(d.getTime))
64 |
65 | implicit def sqlDateW: Write[java.sql.Date, String] =
66 | sqlDateW("yyyy-MM-dd")
67 | }
68 |
69 | trait DefaultWrites extends DateWrites {
70 | protected def optionW[I, J, O](r: => WriteLike[I, J], empty: O)(
71 | implicit w: Path => WriteLike[J, O]) =
72 | (p: Path) =>
73 | Write[Option[I], O] { maybeI =>
74 | maybeI.map { i =>
75 | Write.toWrite(w(p)).contramap(r.writes).writes(i)
76 | }.getOrElse(empty)
77 | }
78 |
79 | implicit def seqW[I, O](implicit w: WriteLike[I, O]) =
80 | Write[Seq[I], Seq[O]] {
81 | _.map(w.writes)
82 | }
83 |
84 | implicit def headW[I, O](implicit w: WriteLike[I, O]): Write[I, Seq[O]] =
85 | Write.toWrite(w).map(Seq(_))
86 |
87 | def ignored[O](x: O) = Write[O, O](_ => x)
88 | }
89 |
90 | trait GenericWrites[O] {
91 | implicit def arrayW[I](implicit w: WriteLike[Seq[I], O]) =
92 | Write((_: Array[I]).toSeq) andThen w
93 |
94 | implicit def listW[I](implicit w: WriteLike[Seq[I], O]) =
95 | Write((_: List[I]).toSeq) andThen w
96 |
97 | implicit def traversableW[I](implicit w: WriteLike[Seq[I], O]) =
98 | Write((_: Traversable[I]).toSeq) andThen w
99 |
100 | implicit def setW[I](implicit w: WriteLike[Seq[I], O]) =
101 | Write((_: Set[I]).toSeq) andThen w
102 | }
103 |
104 | trait NumericTypes2StringWrites {
105 | implicit val intW: Write[Int, String] = Write(_.toString)
106 | implicit val shortW: Write[Short, String] = Write(_.toString)
107 | implicit val booleanW: Write[Boolean, String] = Write(_.toString)
108 | implicit val longW: Write[Long, String] = Write(_.toString)
109 | implicit val floatW: Write[Float, String] = Write(_.toString)
110 | implicit val doubleW: Write[Double, String] = Write(_.toString)
111 | implicit val bigDecimalW: Write[BigDecimal, String] = Write(_.toString)
112 | }
113 |
--------------------------------------------------------------------------------
/validation-core/src/main/scala/backcompat.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | /**
4 | * Backcompat with 1.x. All the methods are deprecated
5 | */
6 | // $COVERAGE-OFF$Disabling highlighting by default.
7 |
8 | object Success {
9 | @deprecated("use cats.data.Validated.Valid", "2.0")
10 | def apply[A](a: A) = cats.data.Validated.valid(a)
11 | @deprecated("use cats.data.Validated.Valid", "2.0")
12 | def unapply[A](v: Valid[A]): Option[A] = Option(v.a)
13 | }
14 |
15 | object Failure {
16 | @deprecated("user cats.data.Validated.Invalid", "2.0")
17 | def apply[E](ves: Seq[E]) = cats.data.Validated.invalid(ves)
18 | @deprecated("use cats.data.Validated.Invalid", "2.0")
19 | def unapply[E](v: Invalid[E]): Option[E] = Option(v.e)
20 | }
21 |
22 | trait VABackCompat[E, A] {
23 | import cats.data.Validated.{valid, invalid}
24 |
25 | val v: Validated[Seq[E], A]
26 |
27 | @deprecated("use isValid", "2.0")
28 | def isSuccess = v.isValid
29 |
30 | @deprecated("use isInvalid", "2.0")
31 | def isFailure = v.isInvalid
32 |
33 | @deprecated("viaEither is deprecated", "2.0")
34 | def viaEither[EE, AA](
35 | f: Either[Seq[E], A] => Either[Seq[EE], AA]): Validated[Seq[EE], AA] =
36 | f(v.toEither).fold(invalid, valid)
37 |
38 | @deprecated("filterNot is deprecated", "2.0")
39 | def filterNot[EE >: E](error: EE)(p: A => Boolean): Validated[Seq[EE], A] =
40 | viaEither {
41 | _.right.flatMap { a =>
42 | if (p(a)) Left(Seq(error)) else Right(a)
43 | }
44 | }
45 |
46 | @deprecated("filterNot is deprecated", "2.0")
47 | def filterNot(p: A => Boolean): Validated[Seq[E], A] =
48 | viaEither {
49 | _.right.flatMap { a =>
50 | if (p(a)) Left(Nil) else Right(a)
51 | }
52 | }
53 |
54 | @deprecated("filter is deprecated", "2.0")
55 | def filter(p: A => Boolean): Validated[Seq[E], A] =
56 | viaEither {
57 | _.right.flatMap { a =>
58 | if (p(a)) Right(a) else Left(Nil)
59 | }
60 | }
61 |
62 | @deprecated("filter is deprecated", "2.0")
63 | def filter[EE >: E](otherwise: EE)(p: A => Boolean): Validated[Seq[EE], A] =
64 | viaEither {
65 | _.right.flatMap { a =>
66 | if (p(a)) Right(a) else Left(Seq(otherwise))
67 | }
68 | }
69 |
70 | @deprecated("collect is deprecated", "2.0")
71 | def collect[EE >: E, B](otherwise: EE)(
72 | p: PartialFunction[A, B]): Validated[Seq[EE], B] = viaEither {
73 | _.right.flatMap {
74 | case t if p.isDefinedAt(t) => Right(p(t))
75 | case _ => Left(Seq(otherwise))
76 | }
77 | }
78 |
79 | @deprecated("withFilter is deprecated", "2.0")
80 | def withFilter(p: A => Boolean) = new WithFilter(p)
81 |
82 | final class WithFilter(p: A => Boolean) {
83 | def map[B](f: A => B): Validated[Seq[E], B] = v match {
84 | case Valid(a) =>
85 | if (p(a)) Valid(f(a))
86 | else Invalid(Nil)
87 | case Invalid(errs) => Invalid(errs)
88 | }
89 | def flatMap[EE >: E, B](
90 | f: A => Validated[Seq[EE], B]): Validated[Seq[EE], B] = v match {
91 | case Valid(a) =>
92 | if (p(a)) f(a)
93 | else Invalid(Nil)
94 | case Invalid(errs) => Invalid(errs)
95 | }
96 | def foreach(f: A => Unit): Unit = v match {
97 | case Valid(a) if p(a) => f(a)
98 | case _ => ()
99 | }
100 | def withFilter(q: A => Boolean) = new WithFilter(a => p(a) && q(a))
101 | }
102 |
103 | @deprecated("use toOption.get", "2.0")
104 | def get = v.toOption.get
105 |
106 | @deprecated("use toOption", "2.0")
107 | def asOpt = v.toOption
108 |
109 | @deprecated("use toEither", "2.0")
110 | def asEither = v.toEither
111 |
112 | @deprecated("use leftMap", "2.0")
113 | def fail = FailProjection(v)
114 |
115 | @deprecated("use map", "2.0")
116 | def success = v
117 | }
118 |
119 | final case class FailProjection[+E, +A](v: Validated[Seq[E], A]) {
120 | def map[F](f: Seq[E] => Seq[F]): Validated[Seq[F], A] = v match {
121 | case Valid(v) => Valid(v)
122 | case Invalid(e) => Invalid(f(e))
123 | }
124 | }
125 |
126 | object Validation {
127 | @deprecated("use .toList.sequence", "2.0")
128 | def sequence[E, A](
129 | vs: Seq[Validated[Seq[E], A]]): Validated[Seq[E], Seq[A]] = {
130 | import cats.instances.list._; import cats.syntax.traverse._;
131 | type VE[X] = Validated[Seq[E], X]
132 | vs.toList.sequence[VE, A]
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/validation-xml/src/main/scala/Rules.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package xml
3 |
4 | import scala.xml._
5 |
6 | trait Rules extends DefaultRules[Node] with ParsingRules {
7 | implicit def nodeR[O](implicit r: RuleLike[String, O]): Rule[Node, O] =
8 | Rule
9 | .fromMapping[Node, String] { node =>
10 | val children = (node \ "_")
11 | if (children.isEmpty) Valid(node.text)
12 | else
13 | Invalid(Seq(ValidationError(
14 | "error.invalid",
15 | "a non-leaf node can not be validated to String")))
16 | }
17 | .andThen(r)
18 |
19 | def attributeR[O](key: String)(
20 | implicit r: RuleLike[String, O]): Rule[Node, O] =
21 | Rule
22 | .fromMapping[Node, String] { node =>
23 | node.attribute(key).flatMap(_.headOption).map(_.text) match {
24 | case Some(value) => Valid(value)
25 | case None => Invalid(Seq(ValidationError("error.required")))
26 | }
27 | }
28 | .andThen(r)
29 |
30 | def optAttributeR[O](key: String)(
31 | implicit r: RuleLike[String, O]): Rule[Node, Option[O]] =
32 | Rule[Node, Option[O]] { node =>
33 | node.attribute(key).flatMap(_.headOption).map(_.text) match {
34 | case Some(str) => r.validate(str).map(Some(_))
35 | case None => Valid(None)
36 | }
37 | }
38 |
39 | implicit def pickInNode[II <: Node, O](p: Path)(
40 | implicit r: RuleLike[Node, O]): Rule[II, O] = {
41 | def search(path: Path, node: Node): Option[Node] = path.path match {
42 | case KeyPathNode(key) :: tail =>
43 | (node \ key).headOption.flatMap(childNode =>
44 | search(Path(tail), childNode))
45 |
46 | case IdxPathNode(idx) :: tail =>
47 | (node \ "_")
48 | .lift(idx)
49 | .flatMap(childNode => search(Path(tail), childNode))
50 |
51 | case Nil => Some(node)
52 | }
53 |
54 | Rule[II, Node] { node =>
55 | search(p, node) match {
56 | case None =>
57 | Invalid(Seq(Path -> Seq(ValidationError("error.required"))))
58 | case Some(resNode) => Valid(resNode)
59 | }
60 | }.andThen(r)
61 | }
62 |
63 | private def pickInS[T](implicit r: RuleLike[Seq[Node], T]): Rule[Node, T] =
64 | Rule
65 | .fromMapping[Node, Seq[Node]] { node =>
66 | val children = (node \ "_")
67 | Valid(children)
68 | }
69 | .andThen(r)
70 |
71 | implicit def pickSeq[O](implicit r: RuleLike[Node, O]): Rule[Node, Seq[O]] =
72 | pickInS(seqR[Node, O])
73 | implicit def pickSet[O](implicit r: RuleLike[Node, O]): Rule[Node, Set[O]] =
74 | pickInS(setR[Node, O])
75 | implicit def pickList[O](
76 | implicit r: RuleLike[Node, O]): Rule[Node, List[O]] =
77 | pickInS(listR[Node, O])
78 | implicit def pickTraversable[O](
79 | implicit r: RuleLike[Node, O]): Rule[Node, Traversable[O]] =
80 | pickInS(traversableR[Node, O])
81 |
82 | implicit def ooo[O](
83 | p: Path)(implicit pick: Path => RuleLike[Node, Node],
84 | coerce: RuleLike[Node, O]): Rule[Node, Option[O]] =
85 | optionR(Rule.zero[O])(pick, coerce)(p)
86 |
87 | def optionR[J, O](r: => RuleLike[J, O], noneValues: RuleLike[Node, Node]*)(
88 | implicit pick: Path => RuleLike[Node, Node],
89 | coerce: RuleLike[Node, J]): Path => Rule[Node, Option[O]] =
90 | super.opt[J, O](r, noneValues: _*)
91 |
92 | def pickChildsWithAttribute[O](
93 | key: String, attrKey: String, attrValue: String)(
94 | implicit r: RuleLike[Node, O]): Rule[Node, Seq[O]] =
95 | Rule.fromMapping[Node, Seq[Node]] { node =>
96 | Valid( (node \ key).filter(_.attribute(attrKey).exists(_.text == attrValue)).toSeq )
97 | }.andThen(seqR(r))
98 |
99 | def pickChildWithAttribute[O](
100 | key: String, attrKey: String, attrValue: String)(
101 | implicit r: RuleLike[Node, O]): Rule[Node, O] =
102 | Rule
103 | .fromMapping[Node, Node] { node =>
104 | val maybeChild = (node \ key).find(
105 | _.attribute(attrKey).filter(_.text == attrValue).isDefined)
106 | maybeChild match {
107 | case Some(child) => Valid(child)
108 | case None =>
109 | Invalid(Seq(ValidationError(
110 | "error.required",
111 | s"child with attribute $attrKey = $attrValue not found")))
112 | }
113 | }
114 | .andThen(r)
115 | }
116 |
117 | object Rules extends Rules
118 |
--------------------------------------------------------------------------------
/validation-jsjson/src/main/scala/Writes.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package jsjson
3 |
4 | import cats.Monoid
5 | import scala.scalajs.js
6 |
7 | trait DefaultMonoids {
8 | implicit def jsonMonoid = new Monoid[js.Dynamic] {
9 | // TODO: Should this be a deepMerge?
10 | def combine(a1: js.Dynamic, a2: js.Dynamic): js.Dynamic =
11 | js.Dictionary[js.Dynamic](
12 | (a1.asInstanceOf[js.Dictionary[js.Dynamic]] ++ a2
13 | .asInstanceOf[js.Dictionary[js.Dynamic]]).toSeq: _*
14 | )
15 | .asInstanceOf[js.Dynamic]
16 |
17 | def empty: js.Dynamic =
18 | js.Dynamic.literal()
19 | }
20 | }
21 |
22 | trait Writes
23 | extends DefaultWrites
24 | with DefaultMonoids
25 | with GenericWrites[js.Dynamic] {
26 | private def writeObj(j: js.Dynamic, n: PathNode): js.Dynamic = n match {
27 | case IdxPathNode(_) => js.Array(j).asInstanceOf[js.Dynamic]
28 | case KeyPathNode(key) => js.Dynamic.literal(key -> j)
29 | }
30 |
31 | implicit val validationErrorW = Write[ValidationError, js.Dynamic] { err =>
32 | js.Dynamic.literal(
33 | "msg" -> err.message,
34 | "args" -> err.args.foldLeft(js.Array(js.Array[Object]())) {
35 | (arr, arg) =>
36 | js.Array(arr :+ arg.toString)
37 | })
38 | }
39 |
40 | implicit def errorsW(
41 | implicit wErrs: WriteLike[Seq[ValidationError], js.Dynamic]) =
42 | Write[(Path, Seq[ValidationError]), js.Dynamic] {
43 | case (p, errs) =>
44 | js.Dynamic.literal(p.toString -> wErrs.writes(errs))
45 | }
46 |
47 | implicit def failureW(
48 | implicit w: WriteLike[(Path, Seq[ValidationError]), js.Dynamic]) =
49 | Write[Invalid[Seq[(Path, Seq[ValidationError])]], js.Dynamic] {
50 | case Invalid(errs) =>
51 | errs.map(w.writes).reduce(jsonMonoid.combine)
52 | }
53 |
54 | implicit val stringW = Write[String, js.Dynamic](_.asInstanceOf[js.Dynamic])
55 |
56 | implicit val intW = Write[Int, js.Dynamic](_.asInstanceOf[js.Dynamic])
57 | implicit val shortW = Write[Short, js.Dynamic](_.asInstanceOf[js.Dynamic])
58 | implicit val floatW = Write[Float, js.Dynamic](_.asInstanceOf[js.Dynamic])
59 | implicit val doubleW = Write[Double, js.Dynamic](_.asInstanceOf[js.Dynamic])
60 | implicit val bigDecimalW =
61 | Write[BigDecimal, js.Dynamic](_.toString.asInstanceOf[js.Dynamic])
62 | // Long are *opaque*, see http://www.scala-js.org/doc/semantics.html
63 | implicit val longW = Write[Long, js.Dynamic] { l =>
64 | (l: js.Any).asInstanceOf[js.Dynamic]
65 | }
66 |
67 | implicit def booleanW =
68 | Write[Boolean, js.Dynamic](_.asInstanceOf[js.Dynamic])
69 |
70 | implicit def seqToJsArray[I](
71 | implicit w: WriteLike[I, js.Dynamic]): Write[Seq[I], js.Dynamic] =
72 | Write(ss => js.Array(ss.map(w.writes _): _*).asInstanceOf[js.Dynamic])
73 |
74 | def optionW[I, J](r: => WriteLike[I, J])(
75 | implicit w: Path => WriteLike[J, js.Dynamic])
76 | : Path => Write[Option[I], js.Dynamic] =
77 | super.optionW[I, J, js.Dynamic](r, js.Dynamic.literal())
78 |
79 | implicit def optionW[I](implicit w: Path => WriteLike[I, js.Dynamic])
80 | : Path => Write[Option[I], js.Dynamic] =
81 | optionW(Write.zero[I])
82 |
83 | implicit def mapW[I](implicit w: WriteLike[I, js.Dynamic]) =
84 | Write[Map[String, I], js.Dynamic] { m =>
85 | // Can't use js.Dynamic.literal here because of SI-9308.
86 | js.Dictionary[js.Dynamic](m.mapValues(w.writes).toSeq: _*)
87 | .asInstanceOf[js.Dynamic]
88 | }
89 |
90 | implicit def vaW[I](implicit w: WriteLike[I, js.Dynamic]) =
91 | Write[VA[I], js.Dynamic] { va =>
92 | js.Dictionary(
93 | "isValid" -> va.isValid.asInstanceOf[js.Dynamic],
94 | "output" -> va.fold(_ => null, w.writes),
95 | "errors" -> va.fold(e => failureW.writes(Invalid(e)), _ => null)
96 | )
97 | .asInstanceOf[js.Dynamic]
98 | }
99 |
100 | implicit def writeJson[I](path: Path)(
101 | implicit w: WriteLike[I, js.Dynamic]): Write[I, js.Dynamic] = Write {
102 | i =>
103 | path match {
104 | case Path(KeyPathNode(x) :: _) \: _ =>
105 | val ps = path.path.reverse
106 | val h = ps.head
107 | val o = writeObj(w.writes(i), h)
108 | ps.tail.foldLeft(o)(writeObj).asInstanceOf[js.Dynamic]
109 | case Path(Nil) =>
110 | w.writes(i).asInstanceOf[js.Dynamic]
111 | case _ =>
112 | throw new RuntimeException(s"path $path is not a path of JsObject") // XXX: should be a compile time error
113 | }
114 | }
115 | }
116 |
117 | object Writes extends Writes
118 |
--------------------------------------------------------------------------------
/validation-delimited/src/main/scala/Rules.scala:
--------------------------------------------------------------------------------
1 | package jto.validation.delimited
2 |
3 | import jto.validation._
4 |
5 | /**
6 | * Rules for parsing/validating/transforming Array[String] as typically returned from CSV parsers.
7 | *
8 | * {{
9 | * case class Contact(name: String, email: String, birthday: Option[LocalDate])
10 | *
11 | * val contactReads = From[Delimited] { __ => (
12 | * (__ \ 0).read[String] and
13 | * (__ \ 1).read(email) and
14 | * (__ \ 2).read(optionR[LocalDate](equalTo("N/A")))
15 | * )(Contact)}
16 | *
17 | * val csv1 = "Ian Hummel,ian@example.com,1981-07-24".split(",")
18 | * val csv2 = "Jane Doe,jane@example.com,N/A".split(",")
19 | *
20 | * contactReads.validate(csv1) // returns Valid(Contact("Ian Hummel", "ian@example.com", Some(new LocalDate(1981, 7, 24))))
21 | * contactReads.validate(csv2) // returns Valid(Contact("Jane Doe", "jane@example.com", None))
22 | * }}
23 | */
24 | trait Rules extends DefaultRules[Delimited] with ParsingRules {
25 | import scala.language.implicitConversions
26 |
27 | /**
28 | * Extract the value at a given index, transforming it into a given type.
29 | *
30 | * @param p An index into the array
31 | * @param r A Rule for converting the value from String to O
32 | * @tparam O The desired type for the value
33 | * @return Invalid if the index is out of bounds or the Path was not an IdxPathNode
34 | */
35 | implicit def pick[O](p: Path)(
36 | implicit r: RuleLike[String, O]): Rule[Delimited, O] =
37 | Rule[Delimited, String] { delimited =>
38 | p.path match {
39 | case IdxPathNode(i) :: t if i < delimited.length => Valid(delimited(i))
40 | case _ => Invalid(Seq(Path -> Seq(ValidationError("error.required"))))
41 | }
42 | }.andThen(r)
43 |
44 | /**
45 | * By default, the empty string "" will be considered as None for Option reads
46 | */
47 | private val isEmpty = validateWith[String]("error.present") { _.isEmpty }
48 |
49 | /**
50 | * Read an optional value using the specified value/rules to determine what is considered None vs what is Some(_).
51 | *
52 | * @param noneValues Rules for determining if a value should be None
53 | * @param pick Function to extract a value from a given index
54 | * @param coerce Coerce the value from String to type O
55 | * @tparam O The desired type for the value
56 | * @return The optional value
57 | */
58 | def optionR[O](noneValues: RuleLike[String, String]*)(
59 | implicit pick: Path => RuleLike[Delimited, String],
60 | coerce: RuleLike[String, O]): Path => Rule[Delimited, Option[O]] =
61 | myOpt[O](coerce, noneValues: _*)
62 |
63 | /**
64 | * Function for creating a mapping from indexes to [[Rule]]s which read optional values from an Array[String].
65 | *
66 | * @param coerce Coerce the value from String to type O
67 | * @param noneValues Rules for determining if a value should be None
68 | * @param pick Function to extract a value from a given index
69 | * @tparam O The desired type for the value
70 | * @return The optional value
71 | */
72 | private def myOpt[O](
73 | coerce: => RuleLike[String, O], noneValues: RuleLike[String, String]*)(
74 | implicit pick: Path => RuleLike[Delimited, String]) =
75 | (path: Path) =>
76 | Rule[Delimited, Option[O]] { delimited =>
77 | val isNone =
78 | not(noneValues.foldLeft(Rule.zero[String])(_ andThen not(_)))
79 | .map(_ => None)
80 | val v =
81 | (pick(path).validate(delimited).map(Some.apply) orElse Valid(None))
82 | Validated.fromEither(
83 | v.toEither.right.flatMap {
84 | case None => Right(None)
85 | case Some(i) =>
86 | isNone
87 | .orElse(Rule.toRule(coerce).map[Option[O]](Some.apply))
88 | .validate(i)
89 | .toEither
90 | }
91 | )
92 | }
93 |
94 | /**
95 | * An implicit defining a default Option reader. Uses "" as the empty value.
96 | *
97 | * @param p An index into the array
98 | * @param pick Function to extract a value from a given index
99 | * @param coerce Coerce the value from String to type O
100 | * @tparam O The desired type for the value
101 | * @return The optional value
102 | */
103 | implicit def ooo[O](
104 | p: Path)(implicit pick: Path => RuleLike[Delimited, String],
105 | coerce: RuleLike[String, O]): Rule[Delimited, Option[O]] =
106 | optionR(isEmpty)(pick, coerce)(p)
107 | }
108 |
109 | object Rules extends Rules
110 |
--------------------------------------------------------------------------------
/validation-jsonast/shared/src/main/scala/Rules.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package jsonast
3 |
4 | trait Rules extends DefaultRules[JValue] {
5 | private def jsonAs[T](
6 | f: PartialFunction[JValue, Validated[Seq[ValidationError], T]])(
7 | msg: String, args: Any*) =
8 | Rule.fromMapping[JValue, T](f.orElse {
9 | case j => Invalid(Seq(ValidationError(msg, args: _*)))
10 | })
11 |
12 | implicit def stringR =
13 | jsonAs[String] {
14 | case JString(v) => Valid(v)
15 | }("error.invalid", "String")
16 |
17 | implicit def booleanR =
18 | jsonAs[Boolean] {
19 | case JBoolean(v) => Valid(v)
20 | }("error.invalid", "Boolean")
21 |
22 | // Note: Mappings of JsNumber to Number are validating that the JsNumber is indeed valid
23 | // in the target type. i.e: JsNumber(4.5) is not considered parseable as an Int.
24 | implicit def intR =
25 | jsonAs[Int] {
26 | case JNumber(v) if BigDecimal(v).isValidInt => Valid(v.toInt)
27 | }("error.number", "Int")
28 |
29 | implicit def shortR =
30 | jsonAs[Short] {
31 | case JNumber(v) if BigDecimal(v).isValidShort => Valid(v.toShort)
32 | }("error.number", "Short")
33 |
34 | implicit def longR =
35 | jsonAs[Long] {
36 | case JNumber(v) if BigDecimal(v).isValidLong => Valid(v.toLong)
37 | }("error.number", "Long")
38 |
39 | implicit def jsNumber =
40 | jsonAs[JNumber] {
41 | case v @ JNumber(_) => Valid(v)
42 | }("error.number", "Number")
43 |
44 | implicit def jsBooleanR =
45 | jsonAs[JBoolean] {
46 | case v @ JBoolean(_) => Valid(v)
47 | }("error.invalid", "Boolean")
48 |
49 | implicit def jsStringR =
50 | jsonAs[JString] {
51 | case v @ JString(_) => Valid(v)
52 | }("error.invalid", "String")
53 |
54 | implicit def jsObjectR =
55 | jsonAs[JObject] {
56 | case v @ JObject(_) => Valid(v)
57 | }("error.invalid", "Object")
58 |
59 | implicit def jsArrayR =
60 | jsonAs[JArray] {
61 | case v @ JArray(_) => Valid(v)
62 | }("error.invalid", "Array")
63 |
64 | implicit def floatR =
65 | jsonAs[Float] {
66 | case JNumber(v) if BigDecimal(v).isDecimalFloat => Valid(v.toFloat)
67 | }("error.number", "Float")
68 |
69 | implicit def doubleR =
70 | jsonAs[Double] {
71 | case JNumber(v) if BigDecimal(v).isDecimalDouble => Valid(v.toDouble)
72 | }("error.number", "Double")
73 |
74 | implicit def bigDecimal =
75 | jsonAs[BigDecimal] {
76 | case JNumber(v) => Valid(BigDecimal(v))
77 | }("error.number", "BigDecimal")
78 |
79 | import java.{math => jm}
80 | implicit def javaBigDecimal =
81 | jsonAs[jm.BigDecimal] {
82 | case JNumber(v) => Valid(BigDecimal(v).bigDecimal)
83 | }("error.number", "BigDecimal")
84 |
85 | implicit val jsNullR: Rule[JValue, JNull.type] = jsonAs[JNull.type] {
86 | case JNull => Valid(JNull)
87 | }("error.invalid", "null")
88 |
89 | implicit def ooo[O](
90 | p: Path)(implicit pick: Path => RuleLike[JValue, JValue],
91 | coerce: RuleLike[JValue, O]): Rule[JValue, Option[O]] =
92 | optionR(Rule.zero[O])(pick, coerce)(p)
93 |
94 | def optionR[J, O](
95 | r: => RuleLike[J, O], noneValues: RuleLike[JValue, JValue]*)(
96 | implicit pick: Path => RuleLike[JValue, JValue],
97 | coerce: RuleLike[JValue, J]): Path => Rule[JValue, Option[O]] =
98 | super.opt[J, O](r, (jsNullR.map(n => n: JValue) +: noneValues): _*)
99 |
100 | implicit def mapR[O](
101 | implicit r: RuleLike[JValue, O]): Rule[JValue, Map[String, O]] =
102 | super.mapR[JValue, O](r, jsObjectR.map { case JObject(fs) => fs.toSeq })
103 |
104 | implicit def jsValueR[O](implicit r: RuleLike[JObject, O]): Rule[JValue, O] =
105 | jsObjectR.andThen(r)
106 |
107 | implicit def pickInJson[II <: JValue, O](p: Path)(
108 | implicit r: RuleLike[JValue, O]): Rule[II, O] = {
109 |
110 | def search(path: Path, json: JValue): Option[JValue] = path.path match {
111 | case KeyPathNode(k) :: t =>
112 | json match {
113 | case JObject(js) =>
114 | js.find(_._1 == k).flatMap(kv => search(Path(t), kv._2))
115 | case _ => None
116 | }
117 |
118 | case IdxPathNode(i) :: t =>
119 | json match {
120 | case JArray(js) => js.lift(i).flatMap(j => search(Path(t), j))
121 | case _ => None
122 | }
123 |
124 | case Nil => Some(json)
125 | }
126 |
127 | Rule[II, JValue] { json =>
128 | search(p, json) match {
129 | case None =>
130 | Invalid(Seq(Path -> Seq(ValidationError("error.required"))))
131 | case Some(js) => Valid(js)
132 | }
133 | }.andThen(r)
134 | }
135 |
136 | // XXX: a bit of boilerplate
137 | private def pickInS[T](
138 | implicit r: RuleLike[Seq[JValue], T]): Rule[JValue, T] =
139 | jsArrayR.map { case JArray(fs) => Seq(fs: _*) }.andThen(r)
140 | implicit def pickSeq[O](implicit r: RuleLike[JValue, O]) =
141 | pickInS(seqR[JValue, O])
142 | implicit def pickSet[O](implicit r: RuleLike[JValue, O]) =
143 | pickInS(setR[JValue, O])
144 | implicit def pickList[O](implicit r: RuleLike[JValue, O]) =
145 | pickInS(listR[JValue, O])
146 | implicit def pickArray[O: scala.reflect.ClassTag](
147 | implicit r: RuleLike[JValue, O]) = pickInS(arrayR[JValue, O])
148 | implicit def pickTraversable[O](implicit r: RuleLike[JValue, O]) =
149 | pickInS(traversableR[JValue, O])
150 | }
151 |
152 | object Rules extends Rules
153 |
--------------------------------------------------------------------------------
/validation-jsjson/src/main/scala/Rules.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package jsjson
3 |
4 | import scala.scalajs.js
5 | import scala.util.Try
6 |
7 | trait Rules extends DefaultRules[js.Dynamic] {
8 | private def jsonAs[T](
9 | f: PartialFunction[js.Any, Validated[Seq[ValidationError], T]])(
10 | msg: String, args: Any*) =
11 | Rule.fromMapping[js.Dynamic, T](f.orElse {
12 | case j => Invalid(Seq(ValidationError(msg, args: _*)))
13 | })
14 |
15 | implicit def stringR =
16 | jsonAs[String] {
17 | case v if (v: Any).isInstanceOf[String] => Valid(v.asInstanceOf[String])
18 | }("error.invalid", "String")
19 |
20 | implicit def booleanR =
21 | jsonAs[Boolean] {
22 | case v if v.isInstanceOf[Boolean] => Valid(v.asInstanceOf[Boolean])
23 | }("error.invalid", "Boolean")
24 |
25 | implicit def intR =
26 | jsonAs[Int] {
27 | case v if v.isInstanceOf[Int] => Valid(v.asInstanceOf[Int])
28 | }("error.number", "Int")
29 |
30 | implicit def shortR =
31 | jsonAs[Short] {
32 | case v if v.isInstanceOf[Short] => Valid(v.asInstanceOf[Short])
33 | }("error.number", "Short")
34 |
35 | implicit def longR =
36 | jsonAs[Long] {
37 | // Long are *opaque*, see http://www.scala-js.org/doc/semantics.html
38 | case v if js.typeOf(v) == "number" && Try(v.toString.toLong).isSuccess =>
39 | Valid(v.toString.toLong)
40 | }("error.number", "Long")
41 |
42 | implicit def jsObjectR =
43 | jsonAs[js.Dictionary[js.Dynamic]] {
44 | case v
45 | if v != null && js.typeOf(v) == "object" && !js.Array.isArray(v) =>
46 | Valid(v.asInstanceOf[js.Dictionary[js.Dynamic]])
47 | }("error.invalid", "Object")
48 |
49 | implicit def jsArrayR[A] =
50 | jsonAs[js.Array[A]] {
51 | case v: js.Array[_] => Valid(v.asInstanceOf[js.Array[A]])
52 | }("error.invalid", "Array")
53 |
54 | implicit def floatR =
55 | jsonAs[Float] {
56 | case v if v.isInstanceOf[Float] => Valid(v.asInstanceOf[Float])
57 | }("error.number", "Float")
58 |
59 | implicit def doubleR =
60 | jsonAs[Double] {
61 | case v if v.isInstanceOf[Double] => Valid(v.asInstanceOf[Double])
62 | }("error.number", "Double")
63 |
64 | implicit def bigDecimal =
65 | jsonAs[BigDecimal] {
66 | case v if Try(BigDecimal(v.toString)).isSuccess =>
67 | Valid(BigDecimal(v.toString))
68 | }("error.number", "BigDecimal")
69 |
70 | import java.{math => jm}
71 | implicit def javaBigDecimal =
72 | jsonAs[jm.BigDecimal] {
73 | case v if Try(new jm.BigDecimal(v.toString)).isSuccess =>
74 | Valid(new jm.BigDecimal(v.toString))
75 | }("error.number", "BigDecimal")
76 |
77 | implicit val jsNullR = jsonAs[Null] {
78 | case v if v == null => Valid(null)
79 | }("error.invalid", "null")
80 |
81 | implicit def ooo[O](
82 | p: Path)(implicit pick: Path => RuleLike[js.Dynamic, js.Dynamic],
83 | coerce: RuleLike[js.Dynamic, O]): Rule[js.Dynamic, Option[O]] =
84 | optionR(Rule.zero[O])(pick, coerce)(p)
85 |
86 | def optionR[J, O](
87 | r: => RuleLike[J, O], noneValues: RuleLike[js.Dynamic, js.Dynamic]*)(
88 | implicit pick: Path => RuleLike[js.Dynamic, js.Dynamic],
89 | coerce: RuleLike[js.Dynamic, J]): Path => Rule[js.Dynamic, Option[O]] =
90 | super.opt[J, O](r, (jsNullR.map(n => n: js.Dynamic) +: noneValues): _*)
91 |
92 | implicit def mapR[O](
93 | implicit r: RuleLike[js.Dynamic, O]): Rule[js.Dynamic, Map[String, O]] =
94 | super.mapR[js.Dynamic, O](r, jsObjectR.map(_.toSeq))
95 |
96 | implicit def jsDictToDyn[O](
97 | implicit r: RuleLike[js.Dictionary[js.Dynamic], O])
98 | : Rule[js.Dynamic, O] =
99 | jsObjectR.andThen(r)
100 |
101 | implicit def pickInJson[II <: js.Dynamic, O](p: Path)(
102 | implicit r: RuleLike[js.Dynamic, O]): Rule[II, O] = {
103 | def search(path: Path, json: js.Dynamic): Option[js.Dynamic] =
104 | path.path match {
105 | case KeyPathNode(k) :: t =>
106 | jsObjectR.validate(json).toOption.flatMap {
107 | obj: js.Dictionary[js.Dynamic] =>
108 | obj.find(_._1 == k).flatMap(kv => search(Path(t), kv._2))
109 | }
110 |
111 | case IdxPathNode(i) :: t =>
112 | jsArrayR.validate(json).toOption.flatMap {
113 | array: js.Array[js.Dynamic] =>
114 | array.lift(i).flatMap(j => search(Path(t), j))
115 | }
116 |
117 | case Nil => Some(json)
118 | }
119 |
120 | Rule[II, js.Dynamic] { json =>
121 | search(p, json) match {
122 | case None =>
123 | Invalid(Seq(Path -> Seq(ValidationError("error.required"))))
124 | case Some(js) => Valid(js)
125 | }
126 | }.andThen(r)
127 | }
128 |
129 | // XXX: a bit of boilerplate
130 | private def pickInS[T](
131 | implicit r: RuleLike[Seq[js.Dynamic], T]): Rule[js.Dynamic, T] =
132 | jsArrayR[js.Dynamic].map(fs => Seq(fs: _*)).andThen(r)
133 | implicit def pickSeq[O](implicit r: RuleLike[js.Dynamic, O]) =
134 | pickInS(seqR[js.Dynamic, O])
135 | implicit def pickSet[O](implicit r: RuleLike[js.Dynamic, O]) =
136 | pickInS(setR[js.Dynamic, O])
137 | implicit def pickList[O](implicit r: RuleLike[js.Dynamic, O]) =
138 | pickInS(listR[js.Dynamic, O])
139 | implicit def pickArray[O: scala.reflect.ClassTag](
140 | implicit r: RuleLike[js.Dynamic, O]) = pickInS(arrayR[js.Dynamic, O])
141 | implicit def pickTraversable[O](implicit r: RuleLike[js.Dynamic, O]) =
142 | pickInS(traversableR[js.Dynamic, O])
143 | }
144 |
145 | object Rules extends Rules
146 |
--------------------------------------------------------------------------------
/validation-playjson/src/main/scala/Rules.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package playjson
3 |
4 | import play.api.libs.json.{JsValue, JsObject, JsString, JsNumber, JsBoolean, JsArray, JsNull}
5 |
6 | trait Rules extends DefaultRules[JsValue] {
7 | private def jsonAs[T](
8 | f: PartialFunction[JsValue, Validated[Seq[ValidationError], T]])(
9 | msg: String, args: Any*) =
10 | Rule.fromMapping[JsValue, T](f.orElse {
11 | case j => Invalid(Seq(ValidationError(msg, args: _*)))
12 | })
13 |
14 | implicit def stringR =
15 | jsonAs[String] {
16 | case JsString(v) => Valid(v)
17 | }("error.invalid", "String")
18 |
19 | implicit def booleanR =
20 | jsonAs[Boolean] {
21 | case JsBoolean(v) => Valid(v)
22 | }("error.invalid", "Boolean")
23 |
24 | // Note: Mappings of JsNumber to Number are validating that the JsNumber is indeed valid
25 | // in the target type. i.e: JsNumber(4.5) is not considered parseable as an Int.
26 | // That's a bit stricter than the "old" Read, which just cast to the target type, possibly loosing data.
27 | implicit def intR =
28 | jsonAs[Int] {
29 | case JsNumber(v) if v.isValidInt => Valid(v.toInt)
30 | }("error.number", "Int")
31 |
32 | implicit def shortR =
33 | jsonAs[Short] {
34 | case JsNumber(v) if v.isValidShort => Valid(v.toShort)
35 | }("error.number", "Short")
36 |
37 | implicit def longR =
38 | jsonAs[Long] {
39 | case JsNumber(v) if v.isValidLong => Valid(v.toLong)
40 | }("error.number", "Long")
41 |
42 | implicit def jsNumberR =
43 | jsonAs[JsNumber] {
44 | case v @ JsNumber(_) => Valid(v)
45 | }("error.number", "Number")
46 |
47 | implicit def jsBooleanR =
48 | jsonAs[JsBoolean] {
49 | case v @ JsBoolean(_) => Valid(v)
50 | }("error.invalid", "Boolean")
51 |
52 | implicit def jsStringR =
53 | jsonAs[JsString] {
54 | case v @ JsString(_) => Valid(v)
55 | }("error.invalid", "String")
56 |
57 | implicit def jsObjectR =
58 | jsonAs[JsObject] {
59 | case v @ JsObject(_) => Valid(v)
60 | }("error.invalid", "Object")
61 |
62 | implicit def jsArrayR =
63 | jsonAs[JsArray] {
64 | case v @ JsArray(_) => Valid(v)
65 | }("error.invalid", "Array")
66 |
67 | implicit def floatR =
68 | jsonAs[Float] {
69 | case JsNumber(v) if v.isDecimalFloat => Valid(v.toFloat)
70 | }("error.number", "Float")
71 |
72 | implicit def doubleR =
73 | jsonAs[Double] {
74 | case JsNumber(v) if v.isDecimalDouble => Valid(v.toDouble)
75 | }("error.number", "Double")
76 |
77 | implicit def bigDecimal =
78 | jsonAs[BigDecimal] {
79 | case JsNumber(v) => Valid(v)
80 | }("error.number", "BigDecimal")
81 |
82 | import java.{math => jm}
83 | implicit def javaBigDecimal =
84 | jsonAs[jm.BigDecimal] {
85 | case JsNumber(v) => Valid(v.bigDecimal)
86 | }("error.number", "BigDecimal")
87 |
88 | implicit val jsNullR: Rule[JsValue, JsNull.type] = jsonAs[JsNull.type] {
89 | case JsNull => Valid(JsNull)
90 | }("error.invalid", "null")
91 |
92 | implicit def ooo[O](
93 | p: Path)(implicit pick: Path => RuleLike[JsValue, JsValue],
94 | coerce: RuleLike[JsValue, O]): Rule[JsValue, Option[O]] =
95 | optionR(Rule.zero[O])(pick, coerce)(p)
96 |
97 | def optionR[J, O](
98 | r: => RuleLike[J, O], noneValues: RuleLike[JsValue, JsValue]*)(
99 | implicit pick: Path => RuleLike[JsValue, JsValue],
100 | coerce: RuleLike[JsValue, J]): Path => Rule[JsValue, Option[O]] =
101 | super.opt[J, O](r, (jsNullR.map(n => n: JsValue) +: noneValues): _*)
102 |
103 | implicit def mapR[O](
104 | implicit r: RuleLike[JsValue, O]): Rule[JsValue, Map[String, O]] =
105 | super.mapR[JsValue, O](r, jsObjectR.map { case JsObject(fs) => fs.toSeq })
106 |
107 | implicit def JsValue[O](
108 | implicit r: RuleLike[JsObject, O]): Rule[JsValue, O] =
109 | jsObjectR.andThen(r)
110 |
111 | implicit def pickInJson[II <: JsValue, O](p: Path)(
112 | implicit r: RuleLike[JsValue, O]): Rule[II, O] = {
113 |
114 | def search(path: Path, json: JsValue): Option[JsValue] = path.path match {
115 | case KeyPathNode(k) :: t =>
116 | json match {
117 | case JsObject(js) =>
118 | js.find(_._1 == k).flatMap(kv => search(Path(t), kv._2))
119 | case _ => None
120 | }
121 | case IdxPathNode(i) :: t =>
122 | json match {
123 | case JsArray(js) => js.lift(i).flatMap(j => search(Path(t), j))
124 | case _ => None
125 | }
126 | case Nil => Some(json)
127 | }
128 |
129 | Rule[II, JsValue] { json =>
130 | search(p, json) match {
131 | case None =>
132 | Invalid(Seq(Path -> Seq(ValidationError("error.required"))))
133 | case Some(js) => Valid(js)
134 | }
135 | }.andThen(r)
136 | }
137 |
138 | private def pickInS[T](
139 | implicit r: RuleLike[Seq[JsValue], T]): Rule[JsValue, T] =
140 | jsArrayR.map { case JsArray(fs) => fs.toSeq }.andThen(r)
141 | implicit def pickSeq[O](implicit r: RuleLike[JsValue, O]) =
142 | pickInS(seqR[JsValue, O])
143 | implicit def pickSet[O](implicit r: RuleLike[JsValue, O]) =
144 | pickInS(setR[JsValue, O])
145 | implicit def pickList[O](implicit r: RuleLike[JsValue, O]) =
146 | pickInS(listR[JsValue, O])
147 | implicit def pickArray[O: scala.reflect.ClassTag](
148 | implicit r: RuleLike[JsValue, O]) = pickInS(arrayR[JsValue, O])
149 | implicit def pickTraversable[O](implicit r: RuleLike[JsValue, O]) =
150 | pickInS(traversableR[JsValue, O])
151 | }
152 |
153 | object Rules extends Rules
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The unified data validation library
2 |
3 | [](https://travis-ci.org/jto/validation) [](https://coveralls.io/github/jto/validation) [](https://maven-badges.herokuapp.com/maven-central/io.github.jto/validation-core_2.11) [](https://www.scala-js.org) [](https://gitter.im/jto/validation?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 |
5 |
6 | ## Overview
7 |
8 | The unified validation API aims to provide a comprehensive toolkit to validate data from any format against user defined rules, and transform them to other types.
9 |
10 | Basically, assuming you have this:
11 |
12 | ```scala
13 | import play.api.libs.json._
14 | import jto.validation._
15 |
16 | case class Person(name: String, age: Int, lovesChocolate: Boolean)
17 |
18 | val json = Json.parse("""{
19 | "name": "Julien",
20 | "age": 28,
21 | "lovesChocolate": true
22 | }""")
23 |
24 | implicit val personRule = {
25 | import jto.validation.playjson.Rules._
26 | Rule.gen[JsValue, Person]
27 | }
28 | ```
29 |
30 | It can do this:
31 |
32 | ```scala
33 | scala> personRule.validate(json)
34 | res0: jto.validation.VA[Person] = Valid(Person(Julien,28,true))
35 | ```
36 |
37 | > **BUT IT'S NOT LIMITED TO JSON**
38 |
39 | It's also a unification of play's [Form Validation API](https://www.playframework.com/documentation/2.3.x/ScalaForms), and its [Json validation API](https://www.playframework.com/documentation/2.3.x/ScalaJsonCombinators).
40 |
41 | Being based on the same concepts as play's Json validation API, it should feel very similar to any developer already working with it. The unified validation API is, rather than a totally new design, a simple generalization of those concepts.
42 |
43 |
44 | ## Design
45 |
46 | The unified validation API is designed around a core defined in package `jto.validation`, and "extensions". Each extension provides primitives to validate and serialize data from / to a particular format ([Json](http://jto.github.io/validation/docs/book/ScalaValidationJson.html), [form encoded request body](http://jto.github.io/validation/docs/book/ScalaValidationMigrationForm.html), etc.). See [the extensions documentation](http://jto.github.io/validation/docs/book/ScalaValidationExtensions.html) for more information.
47 |
48 | To learn more about data validation, please consult [Validation and transformation with Rule](docs/src/main/tut/ScalaValidationRule.md), for data serialization read [Serialization with Write](docs/src/main/tut/ScalaValidationWrite.md). If you just want to figure all this out by yourself, please see the [Cookbook](docs/src/main/tut/ScalaValidationCookbook.md).
49 |
50 |
51 | ## Using the validation api in your project
52 |
53 | Add the following dependencies your `build.sbt` as needed:
54 |
55 | ```scala
56 | resolvers += Resolver.sonatypeRepo("releases")
57 |
58 | val validationVersion = "2.1.0"
59 |
60 | libraryDependencies ++= Seq(
61 | "io.github.jto" %% "validation-core" % validationVersion,
62 | "io.github.jto" %% "validation-playjson" % validationVersion,
63 | "io.github.jto" %% "validation-jsonast" % validationVersion,
64 | "io.github.jto" %% "validation-form" % validationVersion,
65 | "io.github.jto" %% "validation-delimited" % validationVersion,
66 | "io.github.jto" %% "validation-xml" % validationVersion
67 | // "io.github.jto" %%% "validation-jsjson" % validationVersion
68 | )
69 | ```
70 |
71 | ## Play dependencies
72 |
73 | | Validation | Play |
74 | | ---------- | ----- |
75 | | 2.1.x | 2.6.x |
76 | | 2.0.x | 2.5.x |
77 | | 1.1.x | 2.4.x |
78 | | 1.0.2 | 2.3.x |
79 |
80 |
81 | ## Documentation
82 |
83 | [Documentation is here](http://jto.github.io/validation/docs/book/)
84 |
85 | - [Validating and transforming data](http://jto.github.io/validation/docs/book/ScalaValidationRule.html)
86 | - [Combining Rules](http://jto.github.io/validation/docs/book/ScalaValidationRuleCombinators.html)
87 | - [Serializing data with Write](http://jto.github.io/validation/docs/book/ScalaValidationWrite.html)
88 | - [Combining Writes](http://jto.github.io/validation/docs/book/ScalaValidationWriteCombinators.html)
89 | - [Validation Inception](http://jto.github.io/validation/docs/book/ScalaValidationMacros.html)
90 | - [Play's Form API migration](http://jto.github.io/validation/docs/book/ScalaValidationMigrationForm.html)
91 | - [Play's Json API migration](http://jto.github.io/validation/docs/book/ScalaValidationMigrationJson.html)
92 | - [Extensions: Supporting new types](http://jto.github.io/validation/docs/book/ScalaValidationExtensions.html)
93 | - [Exporting Validations to Javascript using Scala.js](http://jto.github.io/validation/docs/book/ScalaJsValidation.html)
94 | - [Cookbook](http://jto.github.io/validation/docs/book/ScalaValidationCookbook.html)
95 | - [Release notes](http://jto.github.io/validation/docs/book/ReleaseNotes.html)
96 | - [v2.0 Migration guide](http://jto.github.io/validation/docs/book/V2MigrationGuide.html)
97 |
98 | ## Contributors
99 |
100 | - Julien Tournay - http://jto.github.io
101 | - Olivier Blanvillain - https://github.com/OlivierBlanvillain
102 | - Nick - https://github.com/stanch
103 | - Ian Hummel - https://github.com/themodernlife
104 | - Arthur Gautier - https://github.com/baloo
105 | - Jacques B - https://github.com/Timshel
106 | - Alexandre Tamborrino - https://github.com/atamborrino
107 |
--------------------------------------------------------------------------------
/docs/src/main/tut/ScalaValidationMigrationForm.md:
--------------------------------------------------------------------------------
1 | # Form API migration
2 |
3 | Although the new Validation API differs significantly from the `Form` API, migrating to new API is straightforward.
4 | This example is a case study of the migration of one of play sample application: "computer database".
5 |
6 | We'll consider `Application.scala`. This controller takes care of Computer creation, and edition. The models are defined in `Models.scala`
7 |
8 | ```scala
9 | case class Company(id: Pk[Long] = NotAssigned, name: String)
10 | case class Computer(id: Pk[Long] = NotAssigned, name: String, introduced: Option[Date], discontinued: Option[Date], companyId: Option[Long])
11 | ```
12 |
13 | Here's the `Application` controller, **before migration**:
14 |
15 | ```scala
16 | package controllers
17 |
18 | import play.api._
19 | import play.api.mvc._
20 | import play.api.data._
21 | import play.api.data.Forms._
22 | import anorm._
23 | import views._
24 | import models._
25 |
26 | object Application extends Controller {
27 |
28 | /** Describe the computer form (used in both edit and create screens). */
29 | val computerForm = Form(
30 | mapping(
31 | "id" -> ignored(NotAssigned:Pk[Long]),
32 | "name" -> nonEmptyText,
33 | "introduced" -> optional(date("yyyy-MM-dd")),
34 | "discontinued" -> optional(date("yyyy-MM-dd")),
35 | "company" -> optional(longNumber)
36 | )(Computer.apply)(Computer.unapply)
37 | )
38 |
39 | def index = // ...
40 |
41 | def list(page: Int, orderBy: Int, filter: String) = // ...
42 |
43 | def edit(id: Long) = Action {
44 | Computer.findById(id).map { computer =>
45 | Ok(html.editForm(id, computerForm.fill(computer), Company.options))
46 | }.getOrElse(NotFound)
47 | }
48 |
49 | def update(id: Long) = Action { implicit request =>
50 | computerForm.bindFromRequest.fold(
51 | formWithErrors => BadRequest(html.editForm(id, formWithErrors, Company.options)),
52 | computer => {
53 | Computer.update(id, computer)
54 | Home.flashing("success" -> "Computer %s has been updated".format(computer.name))
55 | }
56 | )
57 | }
58 |
59 | def create = Action {
60 | Ok(html.createForm(computerForm, Company.options))
61 | }
62 |
63 | def save = Action { implicit request =>
64 | computerForm.bindFromRequest.fold(
65 | formWithErrors => BadRequest(html.createForm(formWithErrors, Company.options)),
66 | computer => {
67 | Computer.insert(computer)
68 | Home.flashing("success" -> "Computer %s has been created".format(computer.name))
69 | }
70 | )
71 | }
72 |
73 | def delete(id: Long) = // ...
74 |
75 | }
76 |
77 | ```
78 |
79 | ### Validation rules migration
80 |
81 | The first thing we must change is the definition of the `Computer` validations.
82 | Instead of using `play.api.data.Form`, we must define a `Rule[UrlFormEncoded, Computer]`.
83 |
84 | `UrlFormEncoded` is simply an alias for `Map[String, Seq[String]]`, which is the type used by play for form encoded request bodies.
85 |
86 | Even though the syntax looks different, the logic is basically the same.
87 |
88 | ```tut:silent
89 | import java.util.Date
90 |
91 | case class Computer(id: Option[Long] = None, name: String, introduced: Option[Date], discontinued: Option[Date], companyId: Option[Long])
92 |
93 | import jto.validation._
94 | import jto.validation.forms.UrlFormEncoded
95 |
96 | implicit val computerValidated = From[UrlFormEncoded] { __ =>
97 | import jto.validation.forms.Rules._
98 | ((__ \ "id").read(ignored[UrlFormEncoded, Option[Long]](None)) ~
99 | (__ \ "name").read(notEmpty) ~
100 | (__ \ "introduced").read(optionR(dateR("yyyy-MM-dd"))) ~
101 | (__ \ "discontinued").read(optionR(dateR("yyyy-MM-dd"))) ~
102 | (__ \ "company").read[Option[Long]])(Computer.apply)
103 | }
104 | ```
105 |
106 | You start by defining a simple validation for each field.
107 |
108 | For example `"name" -> nonEmptyText` now becomes `(__ \ "name").read(notEmpty)`
109 | The next step is to compose these validations together, to get a new validation.
110 |
111 | The *old* api does that using a function called `mapping`, the validation api uses a method called `~` or `and` (`and` is an alias).
112 |
113 | ```scala
114 | mapping(
115 | "name" -> nonEmptyText,
116 | "introduced" -> optional(date("yyyy-MM-dd"))
117 | ```
118 |
119 | now becomes
120 |
121 | ```scala
122 | (__ \ "name").read(notEmpty) ~
123 | (__ \ "introduced").read(optionR(dateR("yyyy-MM-dd")))
124 | ```
125 |
126 | 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.
127 |
128 | > **Be careful with your imports**. Some rules have the same names than form mapping, which could make the implicit parameters resolution fail silently.
129 |
130 |
131 | ### Filling a `Form` with an object
132 |
133 | The new validation API comes with a `Form` class. This class is fully compatible with the existing form input helpers.
134 | You can use the `Form.fill` method to create a `Form` from a class.
135 |
136 | `Form.fill` needs an instance of `Write[T, UrlFormEncoded]`, where `T` is your class type.
137 |
138 | ```tut:silent
139 | import scala.Function.unlift
140 |
141 | implicit val computerW = To[UrlFormEncoded] { __ =>
142 | import jto.validation.forms.Writes._
143 | ((__ \ "id").write[Option[Long]] ~
144 | (__ \ "name").write[String] ~
145 | (__ \ "introduced").write(optionW(dateW("yyyy-MM-dd"))) ~
146 | (__ \ "discontinued").write(optionW(dateW("yyyy-MM-dd"))) ~
147 | (__ \ "company").write[Option[Long]])(unlift(Computer.unapply))
148 | }
149 | ```
150 |
151 | > Note that this `Write` takes care of formatting.
152 |
153 | ### Validating the submitted form
154 |
155 | Handling validation errors is vastly similar to the old api, the main difference is that `bindFromRequest` does not exist anymore.
156 |
157 | ```scala
158 | def save = Action(parse.urlFormEncoded) { implicit request =>
159 | val r = computerValidated.validate(request.body)
160 | r.fold(
161 | err => BadRequest(html.createForm((request.body, r), Company.options)),
162 | computer => {
163 | Computer.insert(computer)
164 | Home.flashing("success" -> "Computer %s has been updated".format(computer.name))
165 | }
166 | )
167 | }
168 | ```
169 |
--------------------------------------------------------------------------------
/docs/src/main/tut/ScalaValidationWriteCombinators.md:
--------------------------------------------------------------------------------
1 | # Combining Writes
2 |
3 | ## Introduction
4 |
5 | We've already explained what a `Write` is in [the previous chapter](ScalaValidationWrite.md). Those examples were only covering simple writes. Most of the time, writes are used to transform complex hierarchical objects.
6 |
7 | In the validation API, we create complex object writes by combining simple writes. This chapter details the creation of those complex writes.
8 |
9 | > 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](ScalaValidationJson.md), [Serializing Forms](ScalaValidationMigrationForm.md), and [Supporting new types](ScalaValidationExtensions.md) for more information.
10 |
11 | ## Path
12 |
13 | ### Serializing data using `Path`
14 |
15 | #### The `write` method
16 |
17 | We start by creating a Path representing the location at which we'd like to serialize our data:
18 |
19 | ```tut:silent
20 | import jto.validation._
21 | val location: Path = Path \ "user" \ "friend"
22 | ```
23 |
24 | `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`.
25 |
26 | But let's try something much easier for now:
27 |
28 | ```tut:nofail
29 | import jto.validation._
30 | import play.api.libs.json._
31 |
32 | val location: Path = Path \ "user" \ "friend"
33 | val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
34 | ```
35 |
36 | `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.
37 |
38 | If you try to run that code, the compiler gives you the following error:
39 |
40 | ```tut:nofail
41 | val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
42 | ```
43 |
44 | 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.
45 |
46 | Fortunately, such method already exists. All you have to do is import it:
47 |
48 | ```tut:silent
49 | import jto.validation.playjson.Writes._
50 | ```
51 |
52 | > 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.
53 |
54 | With those implicits in scope, we can finally create our `Write`:
55 |
56 | ```tut:silent
57 | val serializeFriend: Write[JsValue, JsObject] = location.write[JsValue, JsObject]
58 | ```
59 |
60 | Alright, so far we've defined a `Write` looking for some data of type `JsValue`, located at `/user/friend` in a `JsObject`.
61 |
62 | Now we need to apply this `Write` on our data:
63 |
64 | ```tut
65 | serializeFriend.writes(JsString("Julien"))
66 | ```
67 |
68 | ### Type coercion
69 |
70 | We now are capable of serializing data to a given `Path`. Let's do it again on a different sub-tree:
71 |
72 | ```tut:silent
73 | val agejs: Write[JsValue, JsObject] = (Path \ "user" \ "age").write[JsValue, JsObject]
74 | ```
75 |
76 | And if we apply this new `Write`:
77 |
78 | ```tut
79 | agejs.writes(JsNumber(28))
80 | ```
81 |
82 | That example is nice, but chances are `age` in not a `JsNumber`, but an `Int`.
83 | All we have to do is to change the input type in our `Write` definition:
84 |
85 | ```tut:silent
86 | val age: Write[Int, JsObject] = (Path \ "user" \ "age").write[Int, JsObject]
87 | ```
88 |
89 | And apply it:
90 |
91 | ```tut
92 | age.writes(28)
93 | ```
94 |
95 | So scala *automagically* figures out how to transform a `Int` into a `JsObject`. How does this happen?
96 |
97 | It's fairly simple. The definition of `write` looks like this:
98 |
99 | ```tut:silent
100 | def write[I, O](implicit w: Path => Write[I, O]): Write[I, O] = ???
101 | ```
102 |
103 | 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 `jto.validation.json.Writes`.
104 |
105 | ### Full example
106 |
107 | ```tut:silent
108 | import jto.validation._
109 | import jto.validation.playjson.Writes._
110 | import play.api.libs.json._
111 |
112 | val age: Write[Int, JsObject] = (Path \ "user" \ "age").write[Int, JsObject]
113 | ```
114 | ```tut
115 | age.writes(28)
116 | ```
117 |
118 | ## Combining Writes
119 |
120 | So far we've serialized only primitives types.
121 | Now we'd like to serialize an entire `User` object defined below, and transform it into a `JsObject`:
122 |
123 | ```tut:silent
124 | case class User(
125 | name: String,
126 | age: Int,
127 | email: Option[String],
128 | isAlive: Boolean
129 | )
130 | ```
131 |
132 | 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.
133 |
134 | ```tut:silent
135 | import jto.validation._
136 | import jto.validation.playjson.Writes._
137 | import play.api.libs.json._
138 | import scala.Function.unlift
139 |
140 | val userWrite: Write[User, JsObject] = To[JsObject] { __ =>
141 | import jto.validation.playjson.Writes._
142 | ((__ \ "name").write[String] ~
143 | (__ \ "age").write[Int] ~
144 | (__ \ "email").write[Option[String]] ~
145 | (__ \ "isAlive").write[Boolean])(unlift(User.unapply))
146 | }
147 | ```
148 |
149 |
150 | > **Important:** Note that we're importing `Writes._` **inside** the `To[I]{...}` block.
151 | It is recommended to always follow this pattern, as it nicely scopes the implicits, avoiding conflicts and accidental shadowing.
152 |
153 | `To[JsObject]` defines the `O` type of the writes we're combining. We could have written:
154 |
155 | ```scala
156 | (Path \ "name").write[String, JsObject] ~
157 | (Path \ "age").write[Int, JsObject] ~
158 | //...
159 | ```
160 |
161 | but repeating `JsObject` all over the place is just not very DRY.
162 |
163 | Let's test it now:
164 |
165 | ```tut
166 | userWrite.writes(User("Julien", 28, None, true))
167 | ```
168 |
--------------------------------------------------------------------------------
/validation-core/src/main/scala/Rule.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | import cats.Applicative
4 | import cats.syntax.apply._
5 |
6 | trait RuleLike[I, O] {
7 |
8 | /**
9 | * Apply the Rule to `data`
10 | * @param data The data to validate
11 | * @return The Result of validating the data
12 | */
13 | def validate(data: I): VA[O]
14 | }
15 |
16 | object RuleLike {
17 | implicit def zero[O]: RuleLike[O, O] = Rule[O, O](Valid.apply)
18 | }
19 |
20 | trait Rule[I, O] extends RuleLike[I, O] {
21 |
22 | @deprecated("use andThen instead.", "2.0")
23 | def compose[P](path: Path)(sub: => RuleLike[O, P]): Rule[I, P] =
24 | andThen(path)(sub)
25 |
26 | /**
27 | * Compose two Rules
28 | * {{{
29 | * val r1: Rule[JsValue, String] = // implementation
30 | * val r2: Rule[String, Date] = // implementation
31 | * val r = r1 .andThen(r2)
32 | *
33 | * }}}
34 | * @param path a prefix for the errors path if the result is a `Invalid`
35 | * @param sub the second Rule to apply
36 | * @return The combination of the two Rules
37 | */
38 | def andThen[P](path: Path)(sub: => RuleLike[O, P]): Rule[I, P] =
39 | this.flatMap { o =>
40 | Rule(_ => sub.validate(o))
41 | }.repath(path ++ _)
42 |
43 | def flatMap[B](f: O => Rule[I, B]): Rule[I, B] =
44 | Rule { d =>
45 | this.validate(d).map(f).fold(es => Invalid(es), r => r.validate(d))
46 | }
47 |
48 | /**
49 | * Create a new Rule that try `this` Rule, and apply `t` if it fails
50 | * {{{
51 | * val rb: Rule[JsValue, A] = From[JsValue]{ __ =>
52 | * ((__ \ "name").read[String] ~ (__ \ "foo").read[Int])(B.apply)
53 | * }
54 | *
55 | * val rc: Rule[JsValue, A] = From[JsValue]{ __ =>
56 | * ((__ \ "name").read[String] ~ (__ \ "bar").read[Int])(C.apply)
57 | * }
58 | * val rule = rb orElse rc orElse Rule(_ => typeInvalid)
59 | * }}}
60 | * @param t an alternative Rule
61 | * @return a Rule
62 | */
63 | def orElse[OO >: O](t: => RuleLike[I, OO]): Rule[I, OO] =
64 | Rule(d => this.validate(d) orElse t.validate(d))
65 |
66 | @deprecated("use andThen instead.", "2.0")
67 | def compose[P](sub: => RuleLike[O, P]): Rule[I, P] = andThen(sub)
68 | def andThen[P](sub: => RuleLike[O, P]): Rule[I, P] = andThen(Path)(sub)
69 |
70 | @deprecated("use andThen instead.", "2.0")
71 | def compose[P](m: Mapping[ValidationError, O, P]): Rule[I, P] = andThen(m)
72 | def andThen[P](m: Mapping[ValidationError, O, P]): Rule[I, P] =
73 | andThen(Rule.fromMapping(m))
74 |
75 | /**
76 | * Create a new Rule the validate `this` Rule and `r2` simultaneously
77 | * If `this` and `r2` both fail, all the error are returned
78 | * {{{
79 | * val valid = Json.obj(
80 | * "firstname" -> "Julien",
81 | * "lastname" -> "Tournay")
82 | * val composed = notEmpty |+| minLength(3)
83 | * (Path \ "firstname").read(composed).validate(valid) // Valid("Julien")
84 | * }}}
85 | */
86 | def |+|[OO <: O](r2: RuleLike[I, OO]): Rule[I, O] =
87 | Rule[I, O] { v =>
88 | (this.validate(v) *> r2.validate(v)).bimap(
89 | _.groupBy(_._1).map {
90 | case (path, errs) =>
91 | path -> errs.flatMap(_._2)
92 | }.toSeq,
93 | identity
94 | )
95 | }
96 |
97 | /**
98 | * This methods allows you to modify the Path of errors (if the result is a Invalid) when aplying the Rule
99 | */
100 | def repath(f: Path => Path): Rule[I, O] =
101 | Rule(
102 | d =>
103 | this
104 | .validate(d)
105 | .bimap(_.map { case (p, errs) => f(p) -> errs }, identity))
106 |
107 | def map[B](f: O => B): Rule[I, B] =
108 | Rule(d => this.validate(d).map(f))
109 |
110 | @deprecated("fmap is deprecated, use map instead", "2.0")
111 | def fmap[B](f: O => B): Rule[I, B] = map(f)
112 |
113 | def ap[A](mf: Rule[I, O => A]): Rule[I, A] =
114 | Rule { d =>
115 | val a = validate(d)
116 | val f = mf.validate(d)
117 | Validated.fromEither(
118 | (f *> a).toEither.right.flatMap(x => f.toEither.right.map(_ (x))))
119 | }
120 | }
121 |
122 | object Rule {
123 | def gen[I, O]: Rule[I, O] = macro MappingMacros.rule[I, O]
124 |
125 | /**
126 | * Turn a `A => Rule[B, C]` into a `Rule[(A, B), C]`
127 | * {{{
128 | * val passRule = From[JsValue] { __ =>
129 | * ((__ \ "password").read(notEmpty) ~ (__ \ "verify").read(notEmpty))
130 | * .tupled .andThen(Rule.uncurry(Rules.equalTo[String]).repath(_ => (Path \ "verify")))
131 | * }
132 | * }}}
133 | */
134 | def uncurry[A, B, C](f: A => Rule[B, C]): Rule[(A, B), C] =
135 | Rule { case (a, b) => f(a).validate(b) }
136 |
137 | def zero[O]: Rule[O, O] =
138 | toRule(RuleLike.zero[O])
139 |
140 | def pure[I, O](o: O): Rule[I, O] =
141 | Rule(_ => Valid(o))
142 |
143 | def apply[I, O](m: Mapping[(Path, Seq[ValidationError]), I, O]): Rule[I, O] =
144 | new Rule[I, O] {
145 | def validate(data: I): VA[O] = m(data)
146 | }
147 |
148 | def of[I, O](implicit r: Rule[I, O]): Rule[I, O] = r
149 |
150 | def toRule[I, O](r: RuleLike[I, O]): Rule[I, O] =
151 | new Rule[I, O] {
152 | def validate(data: I): VA[O] = r.validate(data)
153 | }
154 |
155 | def fromMapping[I, O](f: Mapping[ValidationError, I, O]): Rule[I, O] =
156 | Rule[I, O](f(_: I).bimap(errs => Seq(Path -> errs), identity))
157 |
158 | implicit def applicativeRule[I]: Applicative[Rule[I, ?]] =
159 | new Applicative[Rule[I, ?]] {
160 | def pure[A](a: A): Rule[I, A] = Rule.pure(a)
161 | def ap[A, B](mf: Rule[I, A => B])(ma: Rule[I, A]): Rule[I, B] = ma.ap(mf)
162 | }
163 |
164 | implicit def ruleSyntaxCombine[I]: SyntaxCombine[Rule[I, ?]] =
165 | new SyntaxCombine[Rule[I, ?]] {
166 | def apply[A, B](a: Rule[I, A], b: Rule[I, B]): Rule[I, A ~ B] =
167 | b.ap(a.map(a => c => new ~(a, c)))
168 | }
169 |
170 | implicit def ruleFunctorSyntaxObs[I, O](
171 | r: Rule[I, O])(implicit fcb: SyntaxCombine[Rule[I, ?]])
172 | : FunctorSyntaxObs[Rule[I, ?], O] =
173 | new FunctorSyntaxObs[Rule[I, ?], O](r)(fcb)
174 | }
175 |
176 | object Read {
177 | sealed trait Deferred[O] {
178 | def apply[I](i: I)(implicit r: RuleLike[I, O]) = r.validate(i)
179 | }
180 |
181 | def apply[O] = new Deferred[O]{}
182 | }
183 |
--------------------------------------------------------------------------------
/validation-core/src/main/scala/Formatter.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | trait From[I] {
4 | def apply[O](f: Reader[I] => RuleLike[I, O]): Rule[I, O] =
5 | Rule.toRule(f(Reader[I]()))
6 | }
7 | object From {
8 |
9 | /**
10 | * {{{
11 | * val r = From[UrlFormEncoded]{ __ =>
12 | * ((__ \ "firstname").read(notEmpty) ~
13 | * (__ \ "age").read(min(1)).tupled
14 | * }
15 | * r.validate(valid) == Valid("Julien" -> 28)
16 | * }}}
17 | */
18 | def apply[I] = new From[I] {}
19 |
20 | /**
21 | * Validate type `I` as an using the implicit `Write` w
22 | * {{{
23 | * val m = Map(
24 | * "name" -> Seq("bob"),
25 | * "friend.name" -> Seq("bobby"))
26 | * From[UrlFormEncoded, Person](m) == Valid(Person(List("bob", "bobby")))
27 | * }}}
28 | */
29 | def apply[I, O](i: I)(implicit r: RuleLike[I, O]) =
30 | r.validate(i)
31 | }
32 |
33 | trait To[I] {
34 | def apply[O](f: Writer[I] => WriteLike[O, I]): Write[O, I] =
35 | Write.toWrite(f(Writer[I]()))
36 | }
37 | object To {
38 |
39 | /**
40 | * {{{
41 | * val w = To[UrlFormEncoded] { __ =>
42 | * ((__ \ "email").write[Option[String]] ~
43 | * (__ \ "phone").write[String]).tupled
44 | * }
45 | *
46 | * val v = Some("jto@foobar.com") -> "01.23.45.67.89"
47 | *
48 | * w.writes(v) == Map(
49 | * "email" -> Seq("jto@foobar.com"),
50 | * "phone" -> Seq("01.23.45.67.89"))
51 | * }}}
52 | */
53 | def apply[I] = new To[I] {}
54 |
55 | /**
56 | * "Serialize" type `O` to type `I` using the implicit `Write` w
57 | * {{{
58 | * To[Person2, UrlFormEncoded](Person(List("bob", "bobby"))) ==
59 | * Map(
60 | * "name" -> Seq("bob"),
61 | * "friend.name" -> Seq("bobby"))
62 | * }}}
63 | */
64 | def apply[O, I](o: O)(implicit w: WriteLike[O, I]) =
65 | w.writes(o)
66 | }
67 |
68 | case class Reader[I](path: Path = Path(Nil)) {
69 |
70 | /**
71 | * When applied, the rule will lookup for data at the given path, and apply the `sub` Rule on it
72 | * {{{
73 | * val json = Json.parse("""{
74 | * "informations": {
75 | * "label": "test"
76 | * }
77 | * }""")
78 | * val infoValidated = From[JsValue]{ __ => (__ \ "label").read(nonEmptyText) }
79 | * val v = From[JsValue]{ __ => (__ \ "informations").read(infoValidated)) }
80 | * v.validate(json) == Valid("test")
81 | * }}}
82 | * @param sub the constraint to apply on the subdata
83 | * @param l a lookup function. This function finds data in a structure of type I, and coerce it to type O
84 | * @return A Rule validating the existence and validity of data at `path`
85 | */
86 | def read[J, O](sub: => RuleLike[J, O])(
87 | implicit r: Path => RuleLike[I, J]): Rule[I, O] =
88 | Rule.toRule(r(path)).andThen(path)(sub)
89 |
90 | /**
91 | * Try to convert the data at `Path` to type `O`
92 | * {{{
93 | * val json = Json.parse("""{
94 | * "informations": {
95 | * "label": "test"
96 | * }
97 | * }""")
98 | * implicit val infoValidated = From[JsValue]{ __ => (__ \ "label").read[String] }
99 | * val v = From[JsValue]{ __ => (__ \ "informations").read[Informations]) }
100 | * v.validate(json) == Valid("test")
101 | * }}}
102 | * @param r a lookup function. This function finds data in a structure of type I, and coerce it to type O
103 | * @return A Rule validating the existence and validity of data at `path`.
104 | */
105 | def read[O](implicit r: Path => RuleLike[I, O]): Rule[I, O] =
106 | Rule { i =>
107 | read(Rule.zero[O])(r).validate(i)
108 | } // makes it lazy evaluated. Allows recursive writes
109 |
110 | def \(key: String): Reader[I] = Reader(path \ key)
111 | def \(idx: Int): Reader[I] = Reader(path \ idx)
112 | def \(child: PathNode): Reader[I] = Reader(path \ child)
113 | }
114 |
115 | case class Writer[I](path: Path = Path(Nil)) {
116 |
117 | /**
118 | * Create a Write that convert data to type `I`, and put it at Path `path`
119 | * {{{
120 | * val w = To[JsObject] { __ =>
121 | * (__ \ "informations").write[Seq[String]])
122 | * }
123 | * w.writes(Seq("foo", "bar")) == Json.obj("informations" -> Seq("foo", "bar"))
124 | * }}}
125 | * @note This method works fine with recursive writes
126 | */
127 | def write[O](implicit w: Path => WriteLike[O, I]): Write[O, I] =
128 | Write { x =>
129 | w(path).writes(x)
130 | } // makes it lazy evaluated. Allows recursive writes
131 |
132 | /**
133 | * Create a Write that convert data to type `I`, and put it at Path `path`
134 | * {{{
135 | * val w = To[JsObject] { __ =>
136 | * (__ \ "date").write(date("yyyy-MM-dd""))
137 | * }
138 | * w.writes(new Date()) == Json.obj("date" -> "2013-10-3")
139 | * }}}
140 | * @note This method works fine with recursive writes
141 | */
142 | def write[O, J](format: => WriteLike[O, J])(
143 | implicit w: Path => WriteLike[J, I]): Write[O, I] =
144 | Write.toWrite(w(path)).contramap(x => format.writes(x))
145 |
146 | def \(key: String): Writer[I] = Writer(path \ key)
147 | def \(idx: Int): Writer[I] = Writer(path \ idx)
148 | def \(child: PathNode): Writer[I] = Writer(path \ child)
149 | }
150 |
151 | trait Formatting[IR, IW] {
152 | def apply[O](f: Formatter[IR, IW] => Format[IR, IW, O]) =
153 | f(Formatter[IR, IW]())
154 | }
155 | object Formatting {
156 | def apply[IR, IW] = new Formatting[IR, IW] {}
157 | }
158 |
159 | case class Formatter[IR, IW](path: Path = Path(Nil)) {
160 |
161 | def format[JJ, J, O](subR: => RuleLike[J, O], subW: => WriteLike[O, JJ])(
162 | implicit r: Path => RuleLike[IR, J],
163 | w: Path => WriteLike[JJ, IW]): Format[IR, IW, O] = {
164 | Format[IR, IW, O](Reader(path).read(subR), Writer(path).write(subW))
165 | }
166 |
167 | def format[J, O](subR: => RuleLike[J, O])(
168 | implicit r: Path => RuleLike[IR, J],
169 | w: Path => WriteLike[O, IW]): Format[IR, IW, O] =
170 | format(subR, Write.zero[O])
171 |
172 | // def format[JJ, O](subW: => WriteLike[O, JJ])(implicit r: Path => RuleLike[I, O], w: Path => WriteLike[JJ, I]): Format[I, O] =
173 | // format(Rule.zero[O], subW)
174 |
175 | def format[O](
176 | implicit r: Path => RuleLike[IR, O],
177 | w: Path => WriteLike[O, IW]): Format[IR, IW, O] = new Format[IR, IW, O] {
178 | lazy val f = format(Rule.zero[O], Write.zero[O])
179 | def validate(i: IR) = f.validate(i)
180 | def writes(o: O) = f.writes(o)
181 | }
182 |
183 | def \(key: String): Formatter[IR, IW] = Formatter(path \ key)
184 | def \(idx: Int): Formatter[IR, IW] = Formatter(path \ idx)
185 | def \(child: PathNode): Formatter[IR, IW] = Formatter(path \ child)
186 | }
187 |
--------------------------------------------------------------------------------
/validation-form/src/main/scala/Rules.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 | package forms
3 |
4 | import scala.util.parsing.combinator.RegexParsers
5 |
6 | /**
7 | * Play provides you a `Map[String, Seq[String]]` (aliased as `UrlFormEncoded`) in request body for urlFormEncoded requests.
8 | * It's generally a lot more convenient to work on `Map[Path, Seq[String]]` to define Rules.
9 | * This object contains methods used to convert `Map[String, Seq[String]]` <-> `Map[Path, Seq[String]]`
10 | * @note We use the alias `UrlFormEncoded`, which is just a `Map[String, Seq[String]]`
11 | */
12 | object PM {
13 |
14 | /**
15 | * A parser converting a key of a Map[String, [Seq[String]]] to a Path instance
16 | * `foo.bar[0].baz` becomes `Path \ "foo" \ "bar" \ 0 \ "baz"`
17 | */
18 | object PathParser extends RegexParsers {
19 | override type Elem = Char
20 | def int = """\d""".r ^^ { _.toInt }
21 | def idx = "[" ~> int <~ "]" ^^ { IdxPathNode(_) }
22 | def key = rep1(not("." | idx) ~> ".".r) ^^ { ks =>
23 | KeyPathNode(ks.mkString)
24 | }
25 | def node = key ~ opt(idx) ^^ { case k ~ i => k :: i.toList }
26 | def path = (opt(idx) ~ repsep(node, ".")) ^^ {
27 | case i ~ ns => Path(i.toList ::: ns.flatten)
28 | }
29 |
30 | def parse(s: String) =
31 | parseAll(path, new scala.util.parsing.input.CharArrayReader(s.toArray))
32 | }
33 |
34 | type PM = Map[Path, String]
35 |
36 | /**
37 | * Find a sub-Map of all the elements at a Path starting with `path`
38 | * @param path The prefix to look for
39 | * @param data The map in which you want to lookup
40 | * @return a sub Map. If no key of `data` starts with `path`, this map will be empty
41 | */
42 | def find(path: Path)(data: PM): PM = data.flatMap {
43 | case (p, v) if p.path.startsWith(path.path) =>
44 | Map(Path(p.path.drop(path.path.length)) -> v)
45 | case _ =>
46 | Map.empty[Path, String]
47 | }
48 |
49 | /**
50 | * Apply `f` to all the keys of `m`
51 | */
52 | def repathPM(m: PM, f: Path => Path): PM = m.map { case (p, v) => f(p) -> v }
53 |
54 | /**
55 | * Apply `f` to all the keys of `m`
56 | */
57 | def repath(m: UrlFormEncoded, f: Path => Path): UrlFormEncoded =
58 | toM(repathPM(toPM(m), f))
59 |
60 | /**
61 | * Convert a Map[String, Seq[String]] to a Map[Path, Seq[String]]
62 | */
63 | def toPM(m: UrlFormEncoded): PM =
64 | m.toSeq.flatMap {
65 | case (p, vs) =>
66 | if (p.endsWith("[]")) {
67 | vs.zipWithIndex.map {
68 | case (v, i) => (asPath(p.dropRight(2)) \ i) -> v
69 | }
70 | } else {
71 | vs.headOption.map { asPath(p) -> _ }.toSeq
72 | }
73 | }.toMap
74 |
75 | /**
76 | * Convert a Map[Path, Seq[String]] to a Map[String, Seq[String]]
77 | */
78 | def toM(m: PM): UrlFormEncoded =
79 | m.map { case (p, v) => asKey(p) -> Seq(v) }
80 |
81 | private def asNodeKey(n: PathNode): String = n match {
82 | case IdxPathNode(i) => s"[$i]"
83 | case KeyPathNode(k) => k
84 | }
85 |
86 | /**
87 | * Convert a Path to a String key
88 | * @param p The path to convert
89 | * @return A String representation of `p`
90 | */
91 | def asKey(p: Path): String =
92 | p.path.headOption.toList.map(asNodeKey).mkString ++ p.path.tail
93 | .foldLeft("") {
94 | case (path, n @ IdxPathNode(i)) => path + asNodeKey(n)
95 | case (path, n @ KeyPathNode(k)) => path + "." + asNodeKey(n)
96 | }
97 |
98 | /**
99 | * Convert a String key to a Path using `PathParser`
100 | * @param k The String representation of path to convert
101 | * @return a `Path`
102 | */
103 | def asPath(k: String): Path = PathParser.parse(k) match {
104 | case PathParser.Failure(m, _) =>
105 | throw new RuntimeException(s"Invalid field name $k: $m")
106 | case PathParser.Error(m, _) =>
107 | throw new RuntimeException(s"Invalid field name $k: $m")
108 | case PathParser.Success(r, _) => r
109 | }
110 | }
111 |
112 | /**
113 | * This object provides Rules for Map[String, Seq[String]]
114 | */
115 | trait Rules extends DefaultRules[PM.PM] with ParsingRules {
116 | import PM._
117 |
118 | implicit def mapR[O](
119 | implicit r: RuleLike[Seq[String], O]): Rule[PM, Map[String, O]] =
120 | super.mapR[Seq[String], O](r, Rule.zero[PM].map { toM(_).toSeq })
121 |
122 | private val isEmpty = validateWith[PM]("validation.empty") { pm =>
123 | pm.filter { case (_, vs) => !vs.isEmpty }.isEmpty
124 | }
125 | implicit def optionR[O](
126 | implicit pick: Path => RuleLike[PM, PM],
127 | coerce: RuleLike[PM, O]): Path => Rule[PM, Option[O]] =
128 | opt(coerce, isEmpty)(pick, RuleLike.zero[PM])
129 |
130 | def optionR[J, O](r: => RuleLike[J, O], noneValues: RuleLike[PM, PM]*)(
131 | implicit pick: Path => RuleLike[PM, PM],
132 | coerce: RuleLike[PM, J]): Path => Rule[UrlFormEncoded, Option[O]] =
133 | path => {
134 | val nones = isEmpty +: noneValues
135 | val o = opt[J, O](r, nones: _*)(pick, coerce)(path)
136 | Rule.zero[UrlFormEncoded].map(toPM).andThen(o)
137 | }
138 |
139 | implicit def parseString[O](implicit r: RuleLike[String, O]): Rule[PM, O] = {
140 | val find = Rule[Option[String], String] {
141 | _.map(Valid(_)).getOrElse(
142 | Invalid(Seq(Path -> Seq(ValidationError("error.required")))))
143 | }
144 | Rule.zero[PM].map(_.get(Path)).andThen(find).andThen(r)
145 | }
146 |
147 | implicit def inArray[O: scala.reflect.ClassTag](
148 | implicit r: RuleLike[Seq[PM], Array[O]]): Path => Rule[PM, Array[O]] =
149 | inT[O, Traversable](Rule.toRule(r).map(_.toTraversable))(_).map(_.toArray)
150 |
151 | implicit def inT[O, T[_] <: Traversable[_]](
152 | implicit r: RuleLike[Seq[PM], T[O]]): Path => Rule[PM, T[O]] =
153 | path =>
154 | pickInPM(path)(Rule.zero)
155 | .orElse(Rule[PM, PM](_ => Valid(Map.empty)))
156 | .map { pm =>
157 | val (root, others) = pm.partition(_._1 == Path)
158 | val arrays = others.toSeq.flatMap {
159 | case (Path(IdxPathNode(i) :: Nil) \: t, v) => Seq(i -> Map(t -> v))
160 | case _ => Nil
161 | }.groupBy(_._1).toSeq.sortBy(_._1).map {
162 | case (i, pms) =>
163 | pms.map(_._2).foldLeft(Map.empty[Path, String]) { _ ++ _ }
164 | }
165 |
166 | (root +: arrays).filter(!_.isEmpty)
167 | }
168 | .andThen(r)
169 |
170 | implicit def pickInPM[O](p: Path)(implicit r: RuleLike[PM, O]): Rule[PM, O] =
171 | Rule[PM, PM] { pm =>
172 | Valid(PM.find(p)(pm))
173 | }.andThen(r)
174 |
175 | // Convert Rules exploring PM, to Rules exploring UrlFormEncoded
176 | implicit def convertToInM[O](p: Path)(
177 | implicit r: Path => RuleLike[PM, O]): Rule[UrlFormEncoded, O] =
178 | Rule.zero[UrlFormEncoded].map(toPM).andThen(r(p))
179 |
180 | implicit def convertRule[O](
181 | implicit r: RuleLike[UrlFormEncoded, O]): Rule[PM, O] =
182 | Rule.zero[PM].map(toM).andThen(r)
183 | }
184 |
185 | object Rules extends Rules
186 |
--------------------------------------------------------------------------------
/validation-core/src/main/scala/MappingMacros.scala:
--------------------------------------------------------------------------------
1 | package jto.validation
2 |
3 | object MappingMacros {
4 | import scala.reflect.macros.blackbox.Context
5 |
6 | private abstract class Helper {
7 | val context: Context
8 | import context.universe._
9 |
10 | def findAltMethod(
11 | s: MethodSymbol, paramTypes: List[Type]): Option[MethodSymbol] =
12 | // TODO: we can make this a bit faster by checking the number of params
13 | s.alternatives.collectFirst {
14 | case (apply: MethodSymbol)
15 | if (apply.paramLists.headOption.toSeq
16 | .flatMap(_.map(_.asTerm.typeSignature)) == paramTypes) =>
17 | apply
18 | }
19 |
20 | def getMethod(t: Type, methodName: String): Option[MethodSymbol] = {
21 | t.decl(TermName(methodName)) match {
22 | case NoSymbol => None
23 | case s => Some(s.asMethod)
24 | }
25 | }
26 |
27 | def getReturnTypes(s: MethodSymbol): List[Type] =
28 | s.returnType match {
29 | case TypeRef(_, _, args) =>
30 | args.head match {
31 | case t @ TypeRef(_, _, Nil) => List(t)
32 | case t @ TypeRef(_, _, args) =>
33 | if (t <:< typeOf[Option[_]]) List(t)
34 | else if (t <:< typeOf[Seq[_]]) List(t)
35 | else if (t <:< typeOf[Set[_]]) List(t)
36 | else if (t <:< typeOf[Map[_, _]]) List(t)
37 | else if (t <:< typeOf[Product]) args
38 | else
39 | context.abort(context.enclosingPosition,
40 | s"$s has unsupported return types")
41 | case t =>
42 | context.abort(
43 | context.enclosingPosition, s" expected TypeRef, got $t")
44 | }
45 | case t =>
46 | context.abort(
47 | context.enclosingPosition, s" expected TypeRef, got $t")
48 | }
49 |
50 | def getConstructorParamss[T: WeakTypeTag] =
51 | weakTypeOf[T].decls.collect {
52 | // true means we are using constructor (new $T(...))
53 | case m: MethodSymbol if m.isConstructor => (true, m.paramLists)
54 | }.headOption.orElse {
55 | scala.util.Try {
56 | val companionType = weakTypeOf[T].typeSymbol.companion.typeSignature
57 | val apply = getMethod(companionType, "apply")
58 | // false means we are using apply ($T.companion.apply(...))
59 | apply.map(a => (false, a.paramLists))
60 | }.toOption.flatten
61 | }.getOrElse {
62 | context.abort(
63 | context.enclosingPosition,
64 | s"Could not find constructor arguments of type ${weakTypeOf[T]}")
65 | }
66 |
67 | def lookup[T: WeakTypeTag] = {
68 | val companioned = weakTypeOf[T].typeSymbol
69 | val companionSymbol = companioned.companion
70 | val companionType = companionSymbol.typeSignature
71 |
72 | companionType match {
73 | case NoSymbol =>
74 | context.abort(context.enclosingPosition,
75 | s"No companion object found for $companioned")
76 | case _ =>
77 | val unapply = getMethod(companionType, "unapply").getOrElse(
78 | context.abort(context.enclosingPosition,
79 | s"No unapply method found for $companionSymbol"))
80 |
81 | val rts = getReturnTypes(unapply)
82 | val app = getMethod(companionType, "apply").getOrElse(context.abort(
83 | context.enclosingPosition, s"No apply method found"))
84 | val apply = findAltMethod(app, rts).getOrElse(context.abort(
85 | context.enclosingPosition,
86 | s"No apply method matching the unapply method found"))
87 |
88 | (apply, unapply)
89 | }
90 | }
91 | }
92 |
93 | def write[I: c.WeakTypeTag, O: c.WeakTypeTag](
94 | c: Context): c.Expr[Write[I, O]] = {
95 | import c.universe._
96 |
97 | val helper = new { val context: c.type = c } with Helper
98 | import helper._
99 |
100 | val (apply, unapply) = lookup[I]
101 |
102 | val writes = for (g <- apply.paramLists.headOption.toList;
103 | p <- g) yield {
104 | val term = p.asTerm
105 | val name = q"""${term.name.toString}"""
106 | q"""(__ \ $name).write[${term.typeSignature}]"""
107 | }
108 |
109 | val typeI = weakTypeOf[I].dealias
110 | val typeO = weakTypeOf[O].dealias
111 |
112 | // TODO: check return type, should be Option[X]
113 | val TypeRef(_, _, ps) = unapply.returnType
114 | val t = tq"${typeI} => ${ps.head}"
115 | val body = (writes: @unchecked) match {
116 | case w1 :: w2 :: ts =>
117 | val typeApply = ts.foldLeft(q"$w1 ~ $w2") { (t1, t2) =>
118 | q"$t1 ~ $t2"
119 | }
120 | q"($typeApply)(Function.unlift($unapply(_)))"
121 |
122 | case w1 :: Nil =>
123 | q"$w1.contramap(Function.unlift($unapply(_)): $t)"
124 | }
125 |
126 | // XXX: recursive values need the user to use explcitly typed implicit val
127 | c.Expr[Write[I, O]](
128 | q"""{ _root_.jto.validation.To[${typeO}] { __ => $body } }""")
129 | }
130 |
131 | def rule[I: c.WeakTypeTag, O: c.WeakTypeTag](
132 | c: Context): c.Expr[Rule[I, O]] = {
133 | import c.universe._
134 |
135 | val helper = new { val context: c.type = c } with Helper
136 | import helper._
137 |
138 | val (usingConstructor, constructorParamss) = getConstructorParamss[O]
139 |
140 | val reads = for (g <- constructorParamss.headOption.toList;
141 | p <- g) yield {
142 | val term = p.asTerm
143 | val name = q"""${term.name.toString}"""
144 | q"""(__ \ $name).read[${term.typeSignature}]"""
145 | }
146 |
147 | val typeI = weakTypeOf[I].dealias
148 | val typeO = weakTypeOf[O].dealias
149 |
150 | val args = constructorParamss.head.map(_ => TermName(c.freshName("arg")))
151 | val types = constructorParamss.head.map(p => p.typeSignature)
152 | val idents = args.map(a => Ident(a))
153 | val signature = (args zip types) map { case (a, t) => q"val $a: $t" }
154 | val applyƒ =
155 | if (usingConstructor) {
156 | q"{ (..$signature) => new $typeO(..$idents) }"
157 | } else {
158 | q"{ (..$signature) => ${typeO.typeSymbol.companion}.apply(..$idents) }"
159 | }
160 |
161 | val body = (reads: @unchecked) match {
162 | case w1 :: w2 :: ts =>
163 | val typeApply = ts.foldLeft(q"$w1 ~ $w2") { (t1, t2) =>
164 | q"$t1 ~ $t2"
165 | }
166 | q"($typeApply).apply($applyƒ)"
167 |
168 | case w1 :: Nil =>
169 | q"$w1.map($applyƒ)"
170 | }
171 |
172 | // XXX: recursive values need the user to use explcitly typed implicit val
173 | c.Expr[Rule[I, O]](
174 | q"""{ _root_.jto.validation.From[${typeI}] { __ => $body } }""")
175 | }
176 |
177 | def format[IR: c.WeakTypeTag, IW: c.WeakTypeTag, O: c.WeakTypeTag](
178 | c: Context): c.Expr[Format[IR, IW, O]] = {
179 | import c.universe._
180 |
181 | val r = rule[IR, O](c)
182 | val w = write[O, IW](c)
183 | c.Expr[Format[IR, IW, O]](q"""_root_.jto.validation.Format($r, $w)""")
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/project/Boilerplate.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 |
3 | /**
4 | * Copied, with some modifications, from https://github.com/milessabin/shapeless/blob/master/project/Boilerplate.scala
5 | *
6 | * Generate a range of boilerplate classes, those offering alternatives with 0-22 params
7 | * and would be tedious to craft by hand
8 | *
9 | * @author Miles Sabin
10 | */
11 |
12 | object Boilerplate {
13 | import scala.StringContext._
14 |
15 | implicit class BlockHelper(val sc: StringContext) extends AnyVal {
16 | def block(args: Any*): String = {
17 | val interpolated = sc.standardInterpolator(treatEscapes, args)
18 | val rawLines = interpolated split '\n'
19 | val trimmedLines = rawLines map { _ dropWhile (_.isWhitespace) }
20 | trimmedLines mkString "\n"
21 | }
22 | }
23 |
24 | val header = """
25 | // Auto-generated boilerplate
26 | // $COVERAGE-OFF$Disabling coverage for generated code
27 | """
28 |
29 | val minArity = 2
30 | val maxArity = 22
31 |
32 | val templates: Seq[Template] = List(
33 | InvariantSyntax,
34 | FunctorSyntax,
35 | ContravariantSyntax
36 | )
37 |
38 | /** Returns a seq of the generated files. As a side-effect, it actually generates them... */
39 | def gen(dir: File) =
40 | for(template <- templates) yield {
41 | val tgtFile = template.filename(dir / "jto" / "validation")
42 | IO.write(tgtFile, template.body)
43 | tgtFile
44 | }
45 |
46 | class TemplateVals(val arity: Int) {
47 | val synTypes = (0 until arity) map (n => s"A$n")
48 | val synVals = (0 until arity) map (n => s"a$n")
49 | val synTypedVals = (synVals zip synTypes) map { case (v,t) => v + ": " + t}
50 | val `A..N` = synTypes.mkString(", ")
51 | val `a..n` = synVals.mkString(", ")
52 | val `_.._` = Seq.fill(arity)("_").mkString(", ")
53 | val `(A..N)` = if (arity == 1) "Tuple1[A]" else synTypes.mkString("(", ", ", ")")
54 | val `(_.._)` = if (arity == 1) "Tuple1[_]" else Seq.fill(arity)("_").mkString("(", ", ", ")")
55 | val `(a..n)` = if (arity == 1) "Tuple1(a)" else synVals.mkString("(", ", ", ")")
56 | val `a:A..n:N` = synTypedVals mkString ", "
57 | val `a~n` = synVals.mkString(" ~ ")
58 | val `A~N` = synTypes.mkString(" ~ ")
59 | val `A~N-1` = (0 until arity - 1).map(n => s"A$n").mkString(" ~ ")
60 | val `a._1..a._N` = (1 to arity) map (n => s"a._$n") mkString ", "
61 | val `new ~(.., n)` = synVals.reduce[String] { case (acc, el) => s"new ~($acc, $el)" }
62 | }
63 |
64 | trait Template {
65 | def filename(root: File): File
66 | def content(tv: TemplateVals): String
67 | def range = minArity to maxArity
68 | def body: String = {
69 | val headerLines = header split '\n'
70 | val rawContents = range map { n => content(new TemplateVals(n)) split '\n' filterNot (_.isEmpty) }
71 | val preBody = rawContents.head takeWhile (_ startsWith "|") map (_.tail)
72 | val instances = rawContents flatMap {_ filter (_ startsWith "-") map (_.tail) }
73 | val postBody = rawContents.head dropWhile (_ startsWith "|") dropWhile (_ startsWith "-") map (_.tail)
74 | (headerLines ++ preBody ++ instances ++ postBody) mkString "\n"
75 | }
76 | }
77 |
78 | /*
79 | Blocks in the templates below use a custom interpolator, combined with post-processing to produce the body
80 |
81 | - The contents of the `header` val is output first
82 |
83 | - Then the first block of lines beginning with '|'
84 |
85 | - Then the block of lines beginning with '-' is replicated once for each arity,
86 | with the `templateVals` already pre-populated with relevant relevant vals for that arity
87 |
88 | - Then the last block of lines prefixed with '|'
89 |
90 | The block otherwise behaves as a standard interpolated string with regards to variable substitution.
91 | */
92 |
93 | object InvariantSyntax extends Template {
94 | def filename(root: File) = root / "InvariantSyntax.scala"
95 |
96 | def content(tv: TemplateVals) = {
97 | import tv._
98 |
99 | val next = if (arity >= maxArity) "" else
100 | s"def ~[A$arity](m3: M[A$arity]) = new InvariantSyntax${arity+1}[${`A..N`}, A$arity](combine(m1, m2), m3)"
101 |
102 | block"""
103 | |package jto.validation
104 | |
105 | |import cats.Invariant
106 | |
107 | |class InvariantSyntax[M[_]](combine: SyntaxCombine[M]) {
108 | |
109 | - class InvariantSyntax$arity[${`A..N`}](m1: M[${`A~N-1`}], m2: M[A${arity-1}]) {
110 | - $next
111 | -
112 | - def apply[B](f1: (${`A..N`}) => B, f2: B => (${`A..N`}))(implicit fu: Invariant[M]): M[B] =
113 | - fu.imap[${`A~N`}, B](
114 | - combine(m1, m2))({ case ${`a~n`} => f1(${`a..n`}) })(
115 | - (b: B) => { val (${`a..n`}) = f2(b); ${`new ~(.., n)`} }
116 | - )
117 | -
118 | - def tupled(implicit fu: Invariant[M]): M[(${`A..N`})] =
119 | - apply[(${`A..N`})]({ (${`a:A..n:N`}) => (${`a..n`}) }, { (a: (${`A..N`})) => (${`a._1..a._N`}) })
120 | - }
121 | -
122 | |}
123 | """
124 | }
125 | }
126 |
127 | object FunctorSyntax extends Template {
128 | def filename(root: File) = root / "FunctorSyntax.scala"
129 |
130 | def content(tv: TemplateVals) = {
131 | import tv._
132 |
133 | val next = if (arity >= maxArity) "" else
134 | s"def ~[A$arity](m3: M[A$arity]) = new FunctorSyntax${arity+1}[${`A..N`}, A$arity](combine(m1, m2), m3)"
135 |
136 | block"""
137 | |package jto.validation
138 | |
139 | |import cats.Functor
140 | |
141 | |class FunctorSyntax[M[_]](combine: SyntaxCombine[M]) {
142 | |
143 | - class FunctorSyntax${arity}[${`A..N`}](m1: M[${`A~N-1`}], m2: M[A${arity-1}]) {
144 | - $next
145 | -
146 | - def apply[B](f: (${`A..N`}) => B)(implicit fu: Functor[M]): M[B] =
147 | - fu.map[${`A~N`}, B](combine(m1, m2))({ case ${`a~n`} => f(${`a..n`}) })
148 | -
149 | - def tupled(implicit fu: Functor[M]): M[(${`A..N`})] =
150 | - apply[(${`A..N`})]({ (${`a:A..n:N`}) => (${`a..n`}) })
151 | - }
152 | -
153 | |}
154 | """
155 | }
156 | }
157 |
158 | object ContravariantSyntax extends Template {
159 | def filename(root: File) = root / "ContravariantSyntax.scala"
160 |
161 | def content(tv: TemplateVals) = {
162 | import tv._
163 |
164 | val next = if (arity >= maxArity) "" else
165 | s"def ~[A$arity](m3: M[A$arity]) = new ContravariantSyntax${arity+1}[${`A..N`}, A$arity](combine(m1, m2), m3)"
166 |
167 | block"""
168 | |package jto.validation
169 | |
170 | |import cats.Contravariant
171 | |
172 | |class ContravariantSyntax[M[_]](combine: SyntaxCombine[M]) {
173 | |
174 | - class ContravariantSyntax${arity}[${`A..N`}](m1: M[${`A~N-1`}], m2: M[A${arity-1}]) {
175 | - $next
176 | -
177 | - def apply[B](f: B => (${`A..N`}))(implicit fu: Contravariant[M]): M[B] =
178 | - fu.contramap(combine(m1, m2))((b: B) => { val (${`a..n`}) = f(b); ${`new ~(.., n)`} })
179 | -
180 | - def tupled(implicit fu: Contravariant[M]): M[(${`A..N`})] =
181 | - apply[(${`A..N`})]({ (a: (${`A..N`})) => (${`a._1..a._N`}) })
182 | - }
183 | -
184 | |}
185 | """
186 | }
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/docs/src/main/tut/ScalaJsValidation.md:
--------------------------------------------------------------------------------
1 | # Exporting Validations to Javascript using Scala.js
2 |
3 | ```tut:invisible
4 | def cat(path: String): Unit =
5 | println(scala.io.Source.fromFile(s"play-scalajs-example/$path").mkString.trim)
6 | ```
7 | Validation 2.0.x 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.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
28 |
43 |
44 |
45 |
46 |
47 |
92 |
93 | 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.
94 |
95 | 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:
96 |
97 | ```tut
98 | cat("project/plugins.sbt")
99 | ```
100 |
101 | 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:
102 |
103 | ```
104 |
105 | +- build.sbt
106 | +- jvm
107 | | +- app
108 | | +- conf
109 | | +- public
110 | | +- test
111 | +- js
112 | | +- src/main/scala
113 | +- shared
114 | +- src/main/scala
115 | ```
116 |
117 | 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](https://www.scala-js.org/doc/project/cross-build.html), and on [`sbt-play-scalajs` documentation](https://github.com/vmunier/sbt-play-scalajs).
118 |
119 | ```tut
120 | cat("build.sbt")
121 | ```
122 |
123 | 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:
124 |
125 | ```tut
126 | cat("jvm/app/views/main.scala.html")
127 | ```
128 |
129 | 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:
130 |
131 | ```tut
132 | cat("shared/src/main/scala/User.scala")
133 | ```
134 |
135 | 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 )
136 |
137 | ```tut
138 | cat("../validation-jsonast/shared/src/main/scala/JValue.scala")
139 | ```
140 |
141 | 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`:
142 |
143 | - `Ast.from: Rule[play.api.libs.json.JsValue, JValue]`
144 | - `Ast.to: Write[JValue, play.api.libs.json.JsValue]`
145 | - `Ast.from: Rule[scala.scalajs.jsDynamic, JValue]`
146 | - `Ast.to: Write[JValue, scala.scalajs.jsDynamic]`
147 |
148 | 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.
149 |
150 | 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:
151 |
152 | ```tut
153 | cat("js/src/main/scala/Validate.scala")
154 | ```
155 |
156 | Finally, we can create a simple view with a textarea which validates it's content on every keystroke:
157 |
158 | ```tut
159 | cat("jvm/app/views/index.scala.html")
160 | ```
161 |
162 | This complete code of this example is available in the [play-scalajs-example](https://github.com/jto/validation/tree/v2.0/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.
163 |
--------------------------------------------------------------------------------
/docs/src/main/tut/ScalaValidationRule.md:
--------------------------------------------------------------------------------
1 | # Validating and transforming data
2 |
3 | ## Introduction
4 |
5 | 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.
6 |
7 | ## A simple example
8 |
9 | Let's say you want to coerce a `String` into an `Float`.
10 | All you need to do is to define a `Rule` from String to Float:
11 |
12 | ```tut:silent
13 | import jto.validation._
14 | def isFloat: Rule[String, Float] = ???
15 | ```
16 |
17 | When a `String` is parsed into an `Float`, two scenarios are possible, either:
18 |
19 | - The `String` can be parsed as a `Float`.
20 | - The `String` can NOT be parsed as a `Float`
21 |
22 | In a typical Scala application, you would use `Float.parseFloat` to parse a `String`. On an "invalid" value, this method throws a `NumberFormatException`.
23 |
24 | When validating data, we'd certainly prefer to avoid exceptions, as the failure case is expected to happen quite often.
25 |
26 | 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.
27 |
28 | 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]`.
29 |
30 | All you have to do is import the default Rules.
31 |
32 | ```tut:silent
33 | import jto.validation._
34 | object Rules extends GenericRules with ParsingRules
35 | Rules.floatR
36 | ```
37 |
38 | Let's now test it against different String values:
39 |
40 | ```tut
41 | Rules.floatR.validate("1")
42 | Rules.floatR.validate("-13.7")
43 | Rules.floatR.validate("abc")
44 | ```
45 |
46 | > `Rule` is typesafe. You can't apply a `Rule` on an unsupported type, the compiler won't let you:
47 | >
48 | ```tut:nofail
49 | Rules.floatR.validate(Seq(32))
50 | ```
51 |
52 | "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).
53 |
54 | `Validated` represents possible outcomes of Rule application, it can be either :
55 |
56 | - A `Valid`, holding the value being validated
57 | When we use `Rule.float` on "1", since "1" is a valid representation of a `Float`, it returns `Valid(1.0)`
58 | - A `Invalid`, containing all the errors.
59 | 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.
60 |
61 | > Note that `Validated` is a parameterized type. Just like `Rule`, it keeps track of the input and output types.
62 | The method `validate` of a `Rule[I, O]` always return a `VA[I, O]`
63 |
64 | ## Defining your own Rules
65 |
66 | Creating a new `Rule` is almost as simple as creating a new function.
67 | All there is to do is to pass a function `I => Validated[I, O]` to `Rule.fromMapping`.
68 |
69 | This example creates a new `Rule` trying to get the first element of a `List[Int]`.
70 | In case of an empty `List[Int]`, the rule should return a `Invalid`.
71 |
72 | ```tut:silent
73 | val headInt: Rule[List[Int], Int] = Rule.fromMapping {
74 | case Nil => Invalid(Seq(ValidationError("error.emptyList")))
75 | case head :: _ => Valid(head)
76 | }
77 | ```
78 |
79 | ```tut
80 | headInt.validate(List(1, 2, 3, 4, 5))
81 | headInt.validate(Nil)
82 | ```
83 |
84 | We can make this rule a bit more generic:
85 |
86 | ```tut:silent
87 | def head[T]: Rule[List[T], T] = Rule.fromMapping {
88 | case Nil => Invalid(Seq(ValidationError("error.emptyList")))
89 | case head :: _ => Valid(head)
90 | }
91 | ```
92 |
93 | ```tut
94 | head.validate(List('a', 'b', 'c', 'd'))
95 | head.validate(List[Char]())
96 | ```
97 |
98 | ## Composing Rules
99 |
100 | 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`.
101 |
102 | There two different types of composition
103 |
104 | ### "Sequential" composition
105 |
106 | 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]`.
107 |
108 | 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`.
109 |
110 | We already have defined:
111 |
112 | 1. `head: Rule[List[T], T]` returns the first element of a `List`
113 | 2. `float: Rule[String, Float]` parses a `String` into a `Float`
114 |
115 | 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`.
116 |
117 | 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:
118 |
119 | ```tut:silent
120 | val firstFloat: Rule[List[String], Float] = head.andThen(Rules.floatR)
121 | ```
122 | ```tut
123 | firstFloat.validate(List("1", "2"))
124 | firstFloat.validate(List("1.2", "foo"))
125 | ```
126 |
127 | If the list is empty, we get the error from `head`
128 |
129 | ```tut
130 | firstFloat.validate(List())
131 | ```
132 |
133 | If the first element is not parseable, we get the error from `Rules.float`.
134 |
135 | ```tut
136 | firstFloat.validate(List("foo", "2"))
137 | ```
138 |
139 | Of course everything is still typesafe:
140 |
141 | ```tut:nofail
142 | firstFloat.validate(List(1, 2, 3))
143 | ```
144 |
145 | #### Improving reporting.
146 |
147 | All is fine with our new `Rule` but the error reporting when we parse an element is not perfect yet.
148 | When a parsing error happens, the `Invalid` does not tell us that it happened on the first element of the `List`.
149 |
150 | To fix that, we can pass an additionnal parameter to `andThen`:
151 |
152 | ```tut:silent
153 | val firstFloat2: Rule[List[String],Float] = head.andThen(Path \ 0)(Rules.floatR)
154 | ```
155 | ```tut
156 | firstFloat2.validate(List("foo", "2"))
157 | ```
158 |
159 | ### "Parallel" composition
160 |
161 | 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]`.
162 |
163 | 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.
164 |
165 | Consider the following example: We want to write a `Rule` that given an `Int`, check that this `Int` is positive and even.
166 | The validation API already provides `Rules.min`, we have to define `even` ourselves:
167 |
168 | ```tut:silent
169 | val positive: Rule[Int,Int] = Rules.min(0)
170 | val even: Rule[Int,Int] = Rules.validateWith[Int]("error.even"){ _ % 2 == 0 }
171 | ```
172 |
173 | Now we can compose those rules using `|+|`
174 |
175 | ```tut:silent
176 | val positiveAndEven: Rule[Int,Int] = positive |+| even
177 | ```
178 |
179 | Let's test our new `Rule`:
180 |
181 | ```tut
182 | positiveAndEven.validate(12)
183 | positiveAndEven.validate(-12)
184 | positiveAndEven.validate(13)
185 | positiveAndEven.validate(-13)
186 | ```
187 |
188 | Note that both rules are applied. If both fail, we get two `ValidationError`.
189 |
--------------------------------------------------------------------------------
/docs/src/main/tut/ScalaValidationCookbook.md:
--------------------------------------------------------------------------------
1 | # Cookbook
2 |
3 | > 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](ScalaValidationJson.md), [Validating Forms](ScalaValidationMigrationForm.md), and [Supporting new types](ScalaValidationExtensions.md) for more information.
4 |
5 | ## `Rule`
6 |
7 | ### Typical case class validation
8 |
9 | ```tut:silent
10 | import jto.validation._
11 | import play.api.libs.json._
12 |
13 | case class Creature(
14 | name: String,
15 | isDead: Boolean,
16 | weight: Float)
17 |
18 | implicit val creatureRule: Rule[JsValue, Creature] = From[JsValue] { __ =>
19 | import jto.validation.playjson.Rules._
20 | ((__ \ "name").read[String] ~
21 | (__ \ "isDead").read[Boolean] ~
22 | (__ \ "weight").read[Float])(Creature.apply)
23 | }
24 | ```
25 | ```tut
26 | val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0f)
27 | From[JsValue, Creature](js)
28 |
29 | From[JsValue, Creature](Json.obj())
30 | ```
31 |
32 | ### Dependent values
33 |
34 | A common example of this use case is the validation of `password` and `password confirmation` fields in a signup form.
35 |
36 | 1. First, you need to validate that each field is valid independently
37 | 2. Then, given the two values, you need to validate that they are equals.
38 |
39 | ```tut:silent
40 | import jto.validation._
41 | import play.api.libs.json._
42 |
43 | val passRule: Rule[JsValue, String] = From[JsValue] { __ =>
44 | import jto.validation.playjson.Rules._
45 | // This code creates a `Rule[JsValue, (String, String)]` each of of the String must be non-empty
46 | ((__ \ "password").read(notEmpty) ~
47 | (__ \ "verify").read(notEmpty)).tupled
48 | // We then create a `Rule[(String, String), String]` validating that given a `(String, String)`,
49 | // both strings are equals. Those rules are then composed together.
50 | .andThen(Rule.uncurry(equalTo[String])
51 | // In case of `Invalid`, we want to control the field holding the errors.
52 | // We change the `Path` of errors using `repath`
53 | .repath(_ => (Path \ "verify"))
54 | )
55 | }
56 | ```
57 |
58 | Let's test it:
59 |
60 | ```tut
61 | passRule.validate(Json.obj("password" -> "foo", "verify" -> "foo"))
62 | passRule.validate(Json.obj("password" -> "", "verify" -> "foo"))
63 | passRule.validate(Json.obj("password" -> "foo", "verify" -> ""))
64 | passRule.validate(Json.obj("password" -> "", "verify" -> ""))
65 | passRule.validate(Json.obj("password" -> "foo", "verify" -> "bar"))
66 | ```
67 |
68 | ### Recursive types
69 |
70 | When validating recursive types:
71 |
72 | - Use the `lazy` keyword to allow forward reference.
73 | - As with any recursive definition, the type of the `Rule` **must** be explicitly given.
74 |
75 | ```tut:silent
76 | case class User(
77 | name: String,
78 | age: Int,
79 | email: Option[String],
80 | isAlive: Boolean,
81 | friend: Option[User])
82 | ```
83 |
84 | ```tut:silent
85 | import jto.validation._
86 | import play.api.libs.json._
87 |
88 | // Note the lazy keyword
89 | implicit lazy val userRule: Rule[JsValue, User] = From[JsValue] { __ =>
90 | import jto.validation.playjson.Rules._
91 |
92 | ((__ \ "name").read[String] ~
93 | (__ \ "age").read[Int] ~
94 | (__ \ "email").read[Option[String]] ~
95 | (__ \ "isAlive").read[Boolean] ~
96 | (__ \ "friend").read[Option[User]])(User.apply)
97 | }
98 | ```
99 |
100 | or using macros:
101 |
102 | ```tut
103 | import jto.validation._
104 | import play.api.libs.json._
105 | import jto.validation.playjson.Rules._
106 |
107 | // Note the lazy keyword, and the explicit typing
108 | implicit lazy val userRule: Rule[JsValue, User] = Rule.gen[JsValue, User]
109 | ```
110 |
111 | ### Read keys
112 |
113 | ```tut:silent
114 | import jto.validation._
115 | import play.api.libs.json._
116 |
117 | val js = Json.parse("""
118 | {
119 | "values": [
120 | { "foo": "bar" },
121 | { "bar": "baz" }
122 | ]
123 | }
124 | """)
125 |
126 | val r: Rule[JsValue, Seq[(String, String)]] = From[JsValue] { __ =>
127 | import jto.validation.playjson.Rules._
128 |
129 | val tupleR: Rule[JsValue, (String, String)] = Rule.fromMapping[JsValue, (String, String)] {
130 | case JsObject(Seq((key, JsString(value)))) => Valid(key.toString -> value)
131 | case _ => Invalid(Seq(ValidationError("BAAAM")))
132 | }
133 |
134 | (__ \ "values").read(seqR(tupleR))
135 | }
136 | ```
137 | ```tut
138 | r.validate(js)
139 | ```
140 |
141 | ### Validate subclasses (and parse the concrete class)
142 |
143 | Consider the following class definitions:
144 |
145 | ```tut:silent
146 | trait A
147 | case class B(foo: Int) extends A
148 | case class C(bar: Int) extends A
149 |
150 | val b = Json.obj("name" -> "B", "foo" -> 4)
151 | val c = Json.obj("name" -> "C", "bar" -> 6)
152 | val e = Json.obj("name" -> "E", "eee" -> 6)
153 | ```
154 |
155 | #### Trying all the possible rules implementations
156 |
157 | ```tut:silent
158 | import cats.syntax.apply._
159 |
160 | val rb: Rule[JsValue, A] = From[JsValue] { __ =>
161 | import jto.validation.playjson.Rules._
162 | (__ \ "name").read(equalTo("B")) *> (__ \ "foo").read[Int].map(B.apply)
163 | }
164 |
165 | val rc: Rule[JsValue, A] = From[JsValue] { __ =>
166 | import jto.validation.playjson.Rules._
167 | (__ \ "name").read(equalTo("C")) *> (__ \ "bar").read[Int].map(C.apply)
168 | }
169 |
170 | val typeInvalid = Invalid(Seq(Path -> Seq(ValidationError("validation.unknownType"))))
171 | val rule = rb orElse rc orElse Rule(_ => typeInvalid)
172 | ```
173 | ```tut
174 | rule.validate(b)
175 | rule.validate(c)
176 | rule.validate(e)
177 | ```
178 |
179 | #### Using class discovery based on field discrimination
180 |
181 | ```tut:silent
182 | val typeInvalid = Invalid(Seq(Path -> Seq(ValidationError("validation.unknownType"))))
183 |
184 | val rule: Rule[JsValue, A] = From[JsValue] { __ =>
185 | import jto.validation.playjson.Rules._
186 | (__ \ "name").read[String].flatMap[A] {
187 | case "B" => (__ \ "foo").read[Int].map(B.apply _)
188 | case "C" => (__ \ "bar").read[Int].map(C.apply _)
189 | case _ => Rule(_ => typeInvalid)
190 | }
191 | }
192 | ```
193 | ```tut
194 | rule.validate(b)
195 | rule.validate(c)
196 | rule.validate(e)
197 | ```
198 |
199 | ## `Write`
200 |
201 | ### typical case class `Write`
202 |
203 | ```tut:silent
204 | import jto.validation._
205 | import play.api.libs.json._
206 | import scala.Function.unlift
207 |
208 | case class Creature(
209 | name: String,
210 | isDead: Boolean,
211 | weight: Float)
212 |
213 | implicit val creatureWrite = To[JsObject] { __ =>
214 | import jto.validation.playjson.Writes._
215 | ((__ \ "name").write[String] ~
216 | (__ \ "isDead").write[Boolean] ~
217 | (__ \ "weight").write[Float])(unlift(Creature.unapply))
218 | }
219 | ```
220 | ```tut
221 | To[Creature, JsObject](Creature("gremlins", false, 1f))
222 | ```
223 |
224 | ### Adding static values to a `Write`
225 |
226 | ```tut:silent
227 | import jto.validation._
228 | import play.api.libs.json._
229 |
230 | case class LatLong(lat: Float, long: Float)
231 |
232 | implicit val latLongWrite = {
233 | import jto.validation.playjson.Writes._
234 | To[JsObject] { __ =>
235 | ((__ \ "lat").write[Float] ~
236 | (__ \ "long").write[Float])(unlift(LatLong.unapply))
237 | }
238 | }
239 |
240 | case class Point(coords: LatLong)
241 |
242 | implicit val pointWrite = {
243 | import jto.validation.playjson.Writes._
244 | To[JsObject] { __ =>
245 | ((__ \ "coords").write[LatLong] ~
246 | (__ \ "type").write[String]) ((_: Point).coords -> "point")
247 | }
248 | }
249 | ```
250 | ```tut
251 | val p = Point(LatLong(123.3F, 334.5F))
252 | pointWrite.writes(p)
253 | ```
254 |
--------------------------------------------------------------------------------
/docs/src/main/tut/ScalaValidationExtensions.md:
--------------------------------------------------------------------------------
1 | # Extensions: Supporting new types
2 |
3 | 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.
4 |
5 | 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.
6 |
7 | ## Rules
8 |
9 | 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.
10 |
11 | The base of all Rules is the capacity to extract a subset of some input data.
12 |
13 | For the type `JsValue`, we need to be able to extract a `JsValue` at a given `Path`:
14 |
15 | ```tut
16 | import jto.validation._
17 | import play.api.libs.json.{KeyPathNode => JSKeyPathNode, IdxPathNode => JIdxPathNode, _}
18 | object Ex1 {
19 |
20 | def pathToJsPath(p: Path) =
21 | play.api.libs.json.JsPath(p.path.map{
22 | case KeyPathNode(key) => JSKeyPathNode(key)
23 | case IdxPathNode(i) => JIdxPathNode(i)
24 | })
25 |
26 | implicit def pickInJson(p: Path): Rule[JsValue, JsValue] =
27 | Rule[JsValue, JsValue] { json =>
28 | pathToJsPath(p)(json) match {
29 | case Nil => Invalid(Seq(Path -> Seq(ValidationError("error.required"))))
30 | case js :: _ => Valid(js)
31 | }
32 | }
33 | }
34 | ```
35 |
36 | Now we are able to do this:
37 |
38 | ```tut
39 | {
40 | import Ex1._
41 |
42 | val js = Json.obj(
43 | "field1" -> "alpha",
44 | "field2" -> 123L,
45 | "field3" -> Json.obj(
46 | "field31" -> "beta",
47 | "field32"-> 345))
48 |
49 | val pick: Rule[JsValue, JsValue] = From[JsValue] { __ =>
50 | (__ \ "field2").read[JsValue]
51 | }
52 |
53 | pick.validate(js)
54 | }
55 | ```
56 |
57 | Which is nice, but is would be much more convenient if we could extract that value as an `Int`.
58 |
59 | One solution is to write the following method:
60 |
61 | ```scala
62 | implicit def pickIntInJson[O](p: Path): Rule[JsValue, JsValue] = ???
63 | ```
64 |
65 | But we would end up copying 90% of the code we already wrote.
66 | Instead of doing so, we're going to make `pickInJson` a bit smarter by adding an implicit parameter:
67 |
68 | ```scala
69 | implicit def pickInJson[O](p: Path)(implicit r: Rule[JsValue, O]): Rule[JsValue, O] =
70 | Rule[JsValue, JsValue] { json =>
71 | pathToJsPath(p)(json) match {
72 | case Nil => Invalid(Seq(Path -> Seq(ValidationError("error.required"))))
73 | case js :: _ => Valid(js)
74 | }
75 | }.andThen(r)
76 | ```
77 |
78 | 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:
79 |
80 | ```tut
81 | def jsonAs[T](f: PartialFunction[JsValue, Validated[Seq[ValidationError], T]])(args: Any*) =
82 | Rule.fromMapping[JsValue, T](
83 | f.orElse{ case j => Invalid(Seq(ValidationError("validation.invalid", args: _*)))
84 | })
85 |
86 | def stringRule = jsonAs[String] {
87 | case JsString(v) => Valid(v)
88 | }("String")
89 |
90 | def booleanRule = jsonAs[Boolean]{
91 | case JsBoolean(v) => Valid(v)
92 | }("Boolean")
93 | ```
94 |
95 | The types you generally want to support natively are:
96 |
97 | - String
98 | - Boolean
99 | - Int
100 | - Short
101 | - Long
102 | - Float
103 | - Double
104 | - java BigDecimal
105 | - scala BigDecimal
106 |
107 | ## Higher order Rules
108 |
109 | Supporting primitives is nice, but not enough. Users are going to deal with `Seq` and `Option`. We need to support those types too.
110 |
111 | ### Option
112 |
113 | 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`.
114 |
115 | The `jto.validation.DefaultRules[I]` traits provides a helper for building the desired method. It's signature is:
116 |
117 | ```scala
118 | 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]
119 | ```
120 |
121 | - `noneValues` is a List of all the values we should consider to be `None`. For Json that would be `JsNull`.
122 | - `pick` is an extractor. It's going to extract a subtree.
123 | - `coerce` is a type conversion `Rule`
124 | - `r` is a `Rule` to be applied to the data if they are found
125 |
126 | All you have to do is to use this method to implement a specialized version for your type.
127 | For example it's defined this way for Json:
128 |
129 | ```scala
130 | 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]]
131 | = super.opt[J, O](r, (jsNull.map(n => n: JsValue) +: noneValues):_*)
132 | ```
133 | 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.
134 |
135 | Despite the type signature funkiness, this function is actually **really** simple to use:
136 |
137 | ```tut:silent
138 | val maybeEmail: Rule[JsValue, Option[String]] = From[JsValue] { __ =>
139 | import jto.validation.playjson.Rules._
140 | (__ \ "email").read(optionR(email))
141 | }
142 | ```
143 | ```tut
144 | maybeEmail.validate(Json.obj("email" -> "foo@bar.com"))
145 | maybeEmail.validate(Json.obj("email" -> "baam!"))
146 | maybeEmail.validate(Json.obj("email" -> JsNull))
147 | maybeEmail.validate(Json.obj())
148 | ```
149 |
150 | Alright, so now we can explicitly define rules for optional data.
151 |
152 | But what if we write `(__ \ "age").read[Option[Int]]`? It does not compile!
153 | We need to define an implicit rule for that:
154 |
155 | ```scala
156 | implicit def option[O](p: Path)(implicit pick: Path => Rule[JsValue, JsValue], coerce: Rule[JsValue, O]): Rule[JsValue, Option[O]] =
157 | option(Rule.zero[O])(pick, coerce)(p)
158 | ```
159 |
160 | ```tut:silent
161 | val maybeAge: Rule[JsValue, Option[Int]] = From[JsValue] { __ =>
162 | import jto.validation.playjson.Rules._
163 | (__ \ "age").read[Option[Int]]
164 | }
165 | ```
166 |
167 | ### Lazyness
168 |
169 | It's very important that every Rule is completely lazily evaluated . The reason for that is that you may be validating recursive types:
170 |
171 | ```tut
172 | case class RecUser(name: String, friends: Seq[RecUser] = Nil)
173 |
174 | val u = RecUser(
175 | "bob",
176 | Seq(RecUser("tom")))
177 |
178 | lazy val w: Rule[JsValue, RecUser] = From[JsValue] { __ =>
179 | import jto.validation.playjson.Rules._
180 | ((__ \ "name").read[String] ~
181 | (__ \ "friends").read(seqR(w)))(RecUser.apply) // !!! recursive rule definition
182 | }
183 | ```
184 |
185 | ## Writes
186 |
187 | 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:
188 |
189 | ```tut
190 | {
191 | implicit def writeJson[I](path: Path)(implicit w: Write[I, JsValue]): Write[I, JsObject] = ???
192 | }
193 | ```
194 |
195 | And you then define all the primitive writes:
196 |
197 | ```tut
198 | {
199 | implicit def anyval[T <: AnyVal] = ???
200 | }
201 | ```
202 |
203 | ### Monoid
204 |
205 | 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]`:
206 |
207 | ```tut
208 | {
209 | import cats.Monoid
210 | implicit def jsonMonoid = new Monoid[JsObject] {
211 | def combine(a1: JsObject, a2: JsObject) = a1 deepMerge a2
212 | def empty = Json.obj()
213 | }
214 | }
215 | ```
216 |
217 | from there you're able to create complex writes like:
218 |
219 | ```tut:silent
220 | import jto.validation._
221 | import play.api.libs.json._
222 | import scala.Function.unlift
223 |
224 | case class User(
225 | name: String,
226 | age: Int,
227 | email: Option[String],
228 | isAlive: Boolean)
229 |
230 | val userWrite = To[JsObject] { __ =>
231 | import jto.validation.playjson.Writes._
232 | ((__ \ "name").write[String] ~
233 | (__ \ "age").write[Int] ~
234 | (__ \ "email").write[Option[String]] ~
235 | (__ \ "isAlive").write[Boolean])(unlift(User.unapply))
236 | }
237 | ```
238 |
239 | ## Testing
240 |
241 | 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.
242 |
--------------------------------------------------------------------------------
/docs/src/main/tut/ScalaValidationMigrationJson.md:
--------------------------------------------------------------------------------
1 | # Migration from the Json API
2 |
3 | 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.
4 |
5 | > 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.
6 |
7 | ## `Reads` migration
8 |
9 | The 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.
10 |
11 | Basically `Reads[String]` == `Rule[JsValue, String]`.
12 |
13 | Migrating a Json `Reads` to a `Rule` is just a matter of modifying imports and specifying the input type.
14 |
15 | Let's take a typical example from the Json API documentation:
16 |
17 | ```tut:silent
18 | case class Creature(
19 | name: String,
20 | isDead: Boolean,
21 | weight: Float)
22 | ```
23 |
24 | Using the json API, you would have defined something like:
25 |
26 | ```tut
27 | {
28 | import play.api.libs.json._
29 | import play.api.libs.functional.syntax._
30 |
31 | implicit val creatureReads = (
32 | (__ \ "name").read[String] and
33 | (__ \ "isDead").read[Boolean] and
34 | (__ \ "weight").read[Float]
35 | )(Creature.apply _)
36 |
37 | val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0F)
38 | Json.fromJson[Creature](js)
39 | }
40 | ```
41 |
42 | Using the new API, this code becomes:
43 |
44 | ```tut:silent
45 | import jto.validation._
46 | import play.api.libs.json._
47 |
48 | implicit val creatureRule: Rule[JsValue, Creature] = From[JsValue] { __ =>
49 | import jto.validation.playjson.Rules._
50 | ((__ \ "name").read[String] ~
51 | (__ \ "isDead").read[Boolean] ~
52 | (__ \ "weight").read[Float])(Creature.apply)
53 | }
54 | ```
55 | ```tut
56 | val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0F)
57 | From[JsValue, Creature](js)
58 | ```
59 |
60 | 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.
61 |
62 | > **Important:** Note that we're importing `Rules._` **inside** the `From[JsValue]{...}` block.
63 | It is recommended to always follow this pattern, as it nicely scopes the implicits, avoiding conflicts and accidental shadowing.
64 |
65 | ### readNullable
66 |
67 | 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`:
68 |
69 | ```tut:silent
70 | val nullableStringRule: Rule[JsValue, Option[String]] = From[JsValue] { __ =>
71 | import jto.validation.playjson.Rules._
72 | (__ \ "foo").read[Option[String]]
73 | }
74 |
75 | val js1 = Json.obj("foo" -> "bar")
76 | val js2 = Json.obj("foo" -> JsNull)
77 | val js3 = Json.obj()
78 | ```
79 | ```tut
80 | nullableStringRule.validate(js1)
81 | nullableStringRule.validate(js2)
82 | nullableStringRule.validate(js3)
83 | ```
84 |
85 | ### keepAnd
86 |
87 | The general use for `keepAnd` is to apply two validation on the same `JsValue`, for example:
88 |
89 | ```tut:silent
90 | {
91 | import play.api.libs.json._
92 | import Reads._
93 | import play.api.libs.functional.syntax._
94 | (JsPath \ "key1").read[String](email keepAnd minLength[String](5))
95 | }
96 | ```
97 |
98 | You can achieve the same thing in the Validation API using [Rules composition](ScalaValidationRule.md)
99 |
100 | ```tut:silent
101 | From[JsValue] { __ =>
102 | import jto.validation.playjson.Rules._
103 | (__ \ "key1").read(email |+| minLength(5))
104 | }
105 | ```
106 |
107 | ### lazy reads
108 |
109 | Reads are always lazy in the new validation API, therefore, you don't need to use any specific function, even for recursive types:
110 |
111 | ```tut:silent
112 | import play.api.libs.json._
113 | import play.api.libs.functional.syntax._
114 |
115 | case class User(id: Long, name: String, friend: Option[User] = None)
116 |
117 | implicit lazy val UserReads: Reads[User] = (
118 | (__ \ 'id).read[Long] and
119 | (__ \ 'name).read[String] and
120 | (__ \ 'friend).lazyReadNullable(UserReads)
121 | )(User.apply _)
122 |
123 | val js = Json.obj(
124 | "id" -> 123L,
125 | "name" -> "bob",
126 | "friend" -> Json.obj("id" -> 124L, "name" -> "john", "friend" -> JsNull))
127 | ```
128 | ```tut
129 | Json.fromJson[User](js)
130 | ```
131 |
132 | becomes:
133 |
134 | ```tut:silent
135 | case class User(id: Long, name: String, friend: Option[User] = None)
136 |
137 | implicit lazy val userRule: Rule[JsValue, User] = From[JsValue] { __ =>
138 | import jto.validation.playjson.Rules._
139 | ((__ \ "id").read[Long] ~
140 | (__ \ "name").read[String] ~
141 | (__ \ "friend").read(optionR(userRule)))(User.apply)
142 | }
143 |
144 | val js = Json.obj(
145 | "id" -> 123L,
146 | "name" -> "bob",
147 | "friend" -> Json.obj("id" -> 124L, "name" -> "john", "friend" -> JsNull))
148 | ```
149 | ```tut
150 | From[JsValue, User](js)
151 | ```
152 |
153 | ### Numeric types
154 |
155 | You should be aware that numeric type coercion is a bit stricter in the validation API.
156 |
157 | For example:
158 |
159 | ```tut
160 | val js = Json.obj("n" -> 42.5f)
161 | js.validate((__ \ "n").read[Int]) // JsSuccess(42, /n)
162 | ```
163 |
164 | whereas with the validation API, an `Int` must really be an `Int`:
165 |
166 | ```scala
167 | import json.Rules._
168 | val js = Json.obj("n" -> 42.5f)
169 | (Path \ "n").read[JsValue, Int].validate(js)
170 | ```
171 |
172 | ### `json.apply` and `path.as[T]`
173 |
174 | 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".
175 |
176 | The preferred solution is to use `path.read[T]` and to handle failure properly.
177 |
178 | ```tut:silent
179 | {
180 | val js = Json.obj("foo" -> "bar")
181 | (js \ "foo").as[String]
182 | }
183 | ```
184 |
185 | becomes
186 |
187 | ```tut:silent
188 | {
189 | import jto.validation.playjson.Rules._
190 | (Path \ "foo").read[JsValue, String]
191 | }
192 | ```
193 |
194 | ### pickBranch
195 |
196 | `JsPath` has a `prickBranch` method, that creates a `Reads` extracting a subtree in a Json object:
197 |
198 | For example, given the following json object, we can extract a sub tree:
199 |
200 | ```tut:silent
201 | {
202 | import play.api.libs.json._
203 |
204 | val js = Json.obj(
205 | "field1" -> "alpha",
206 | "field2" -> 123L,
207 | "field3" -> Json.obj(
208 | "field31" -> "beta",
209 | "field32"-> 345
210 | ))
211 |
212 | val pick = (__ \ "field3").json.pickBranch
213 | pick.reads(js) // Valid({"field3":{"field31":"beta","field32":345}})
214 | }
215 | ```
216 |
217 | In the validation API, you simply use `read` to create a rule picking a branch:
218 |
219 | ```tut:silent
220 | import jto.validation._
221 | import play.api.libs.json._
222 |
223 | val js = Json.obj(
224 | "field1" -> "alpha",
225 | "field2" -> 123L,
226 | "field3" -> Json.obj(
227 | "field31" -> "beta",
228 | "field32"-> 345
229 | ))
230 |
231 | val pick: Rule[JsValue, JsValue] = From[JsValue] { __ =>
232 | import jto.validation.playjson.Rules._
233 | (__ \ "field3").read[JsValue]
234 | }
235 | ```
236 | ```tut
237 | pick.validate(js)
238 | ```
239 |
240 | ## `Writes` migration
241 |
242 | `Writes` are really easy to port. Just like `Reads`, it's basically a matter of adding imports.
243 |
244 | For example, you would have defined a `Writes` for the `Creature` case class this way:
245 |
246 | ```scala
247 | import play.api.libs.json._
248 | import scala.Function.unlift
249 |
250 | case class Creature(
251 | name: String,
252 | isDead: Boolean,
253 | weight: Float)
254 |
255 | implicit val creatureWrite = (
256 | (__ \ "name").write[String] and
257 | (__ \ "isDead").write[Boolean] and
258 | (__ \ "weight").write[Float]
259 | )(unlift(Creature.unapply))
260 |
261 | Json.toJson(Creature("gremlins", false, 1f))
262 | ```
263 |
264 | With the validation API:
265 |
266 | ```tut:silent
267 | import jto.validation._
268 | import play.api.libs.json._
269 | import scala.Function.unlift
270 |
271 | case class Creature(
272 | name: String,
273 | isDead: Boolean,
274 | weight: Float)
275 |
276 | implicit val creatureWrite = To[JsObject]{ __ =>
277 | import jto.validation.playjson.Writes._
278 | ((__ \ "name").write[String] ~
279 | (__ \ "isDead").write[Boolean] ~
280 | (__ \ "weight").write[Float])(unlift(Creature.unapply))
281 | }
282 | ```
283 | ```tut
284 | val c = To[Creature, JsObject](Creature("gremlins", false, 1f))
285 | ```
286 |
287 | ## `Format` migration
288 |
289 | The 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.
290 |
291 | ## Json Inception (macro)
292 |
293 | Macros are also available for the validation API. See [Validation Inception](ScalaValidationMacros.md).
294 |
--------------------------------------------------------------------------------
/validation-xml/src/test/scala/WritesSpec.scala:
--------------------------------------------------------------------------------
1 | import jto.validation._
2 | import jto.validation.xml._
3 | import jto.validation.xml.Writes._
4 | import org.scalatest._
5 | import scala.Function.unlift
6 | import scala.Function.unlift
7 |
8 | class WritesSpec extends WordSpec with Matchers {
9 |
10 | case class Contact(
11 | firstname: String,
12 | lastname: String,
13 | company: Option[String],
14 | informations: Seq[ContactInformation]
15 | )
16 |
17 | case class ContactInformation(
18 | label: String,
19 | email: Option[String],
20 | phones: Seq[String]
21 | )
22 |
23 | val contact = Contact(
24 | "Julien",
25 | "Tournay",
26 | None,
27 | Seq(
28 | ContactInformation("Personal",
29 | Some("fakecontact@gmail.com"),
30 | Seq("01.23.45.67.89", "98.76.54.32.10"))))
31 |
32 | "Writes" should {
33 |
34 | "write string" in {
35 | val w = (Path \ "label").write[String, XmlWriter]
36 | w.writes("Hello World")( ) shouldBe Hello World
37 | }
38 |
39 | "write string as an attribute" in {
40 | val w = (Path).write(attributeW[String]("attr"))
41 | w.writes("Hello World")( ) shouldBe
42 | }
43 |
44 | "ignore values" in {
45 | (Path \ "n").write(ignored("foo")).writes("test")( ) shouldBe foo
46 | (Path \ "n").write(ignored(42)).writes(0)( ) shouldBe 42
47 | }
48 |
49 | "write an option" in {
50 | val w = To[XmlWriter] { __ =>
51 | ((__ \ "a").write[Option[Int]] ~ optAttributeW[Int]("b")) tupled
52 | }
53 | w.writes((Some(1), Some(2)))( ) shouldEqual 1
54 | w.writes((Some(1), None))( ) shouldEqual 1
55 | w.writes((None, Some(2)))( ) shouldEqual
56 | w.writes((None, None))( ) shouldEqual
57 | }
58 |
59 | "write a sequence" in {
60 | val s = Seq(1, 2, 3)
61 | val w = To[XmlWriter] { __ =>
62 | seqToNodeSeq(
63 | (__ \ "a").write[Int]
64 | )
65 | }
66 | w.writes(s)( ) shouldBe 1 2 3
67 | }
68 |
69 | "write primitive types" when {
70 |
71 | "Int" in {
72 | Path.write[Int, XmlWriter].writes(4)( ) shouldBe (4 )
73 | (Path \ "n" \ "o").write[Int, XmlWriter].writes(4)( ) shouldBe
74 | (4 )
75 | (Path \ "n" \ "o" \ "p").write[Int, XmlWriter].writes(4)( ) shouldBe
76 | (4
)
77 | }
78 |
79 | "Short" in {
80 | (Path \ "n").write[Short, XmlWriter].writes(4)( ) shouldBe
81 | (4 )
82 | (Path \ "n" \ "o").write[Short, XmlWriter].writes(4)( ) shouldBe
83 | (4 )
84 | (Path \ "n" \ "o" \ "p").write[Short, XmlWriter].writes(4)( ) shouldBe
85 | (4
)
86 | }
87 |
88 | "Long" in {
89 | (Path \ "n").write[Long, XmlWriter].writes(4)( ) shouldBe
90 | (4 )
91 | (Path \ "n" \ "o").write[Long, XmlWriter].writes(4)( ) shouldBe
92 | (4 )
93 | (Path \ "n" \ "o" \ "p").write[Long, XmlWriter].writes(4)( ) shouldBe
94 | (4
)
95 | }
96 |
97 | "Float" in {
98 | (Path \ "n").write[Float, XmlWriter].writes(4.5f)( ) shouldBe
99 | (4.5 )
100 | (Path \ "n" \ "o").write[Float, XmlWriter].writes(4.5f)( ) shouldBe
101 | (4.5 )
102 | (Path \ "n" \ "o" \ "p").write[Float, XmlWriter].writes(4.5f)( ) shouldBe
103 | (4.5
)
104 | }
105 |
106 | "Double" in {
107 | (Path \ "n").write[Double, XmlWriter].writes(4.5d)( ) shouldBe
108 | (4.5 )
109 | (Path \ "n" \ "o").write[Double, XmlWriter].writes(4.5d)( ) shouldBe
110 | (4.5 )
111 | (Path \ "n" \ "o" \ "p").write[Double, XmlWriter].writes(4.5d)( ) shouldBe
112 | (4.5
)
113 | }
114 |
115 | "scala Big Decimal" in {
116 | (Path \ "n")
117 | .write[BigDecimal, XmlWriter]
118 | .writes(BigDecimal("4.0"))( ) shouldBe (4.0 )
119 | (Path \ "n" \ "o")
120 | .write[BigDecimal, XmlWriter]
121 | .writes(BigDecimal("4.0"))( ) shouldBe
122 | (4.0 )
123 | (Path \ "n" \ "o" \ "p")
124 | .write[BigDecimal, XmlWriter]
125 | .writes(BigDecimal("4.0"))( ) shouldBe
126 | (4.0
)
127 | }
128 |
129 | "Boolean" in {
130 | (Path \ "n").write[Boolean, XmlWriter].writes(true)( ) shouldBe
131 | (true )
132 | (Path \ "n" \ "o").write[Boolean, XmlWriter].writes(false)( ) shouldBe
133 | (false )
134 | (Path \ "n" \ "o" \ "p")
135 | .write[Boolean, XmlWriter]
136 | .writes(true)( ) shouldBe (true
)
137 | }
138 | }
139 |
140 | "compose with child nodes and/or attributes" in {
141 | val w = To[XmlWriter] { __ =>
142 | ((__ \ "firstname").write[String] ~ (__ \ "age").write[Int]) tupled
143 | }
144 | w.writes(("Julien", 28))( ) shouldBe Julien 28
145 |
146 | val w1 = To[XmlWriter] { __ =>
147 | (attributeW[String]("firstname") ~ attributeW[Int]("age")) tupled
148 | }
149 | w1.writes(("Julien", 28))( ) shouldBe
150 |
151 | val w2 = To[XmlWriter] { __ =>
152 | (attributeW[String]("firstname") ~ (__ \ "age").write[Int]) tupled
153 | }
154 | w2.writes(("Julien", 28))( ) shouldBe 28
155 | }
156 |
157 | "do a deep write" in {
158 | val w = To[XmlWriter] { __ =>
159 | (__ \ "a" \ "b").write(
160 | ((__ \ "c").write[String] ~ (__ \ "d").write(
161 | (__ \ "e").write[String]
162 | )) tupled
163 | )
164 | }
165 | w.writes(("foo", "bar"))( ) shouldBe foo bar
166 | }
167 |
168 | "do a complex write" in {
169 | val w = To[XmlWriter] { __ =>
170 | ((__ \ "email").write[Option[String]] ~ (__ \ "phones").write(
171 | seqToNodeSeq(
172 | (__ \ "phone").write[String]
173 | ))) tupled
174 | }
175 |
176 | val v = Some("jto@foobar.com") -> Seq("01.23.45.67.89", "98.76.54.32.10")
177 |
178 | w.writes(v)( ) shouldBe jto@foobar.com 01.23.45.67.89 98.76.54.32.10
179 | w.writes(Some("jto@foobar.com") -> Nil)( ) shouldBe jto@foobar.com
180 | w.writes(None -> Nil)( ) shouldBe
181 | }
182 |
183 | "write recursive" when {
184 | case class RecUser(name: String, friends: Seq[RecUser] = Nil)
185 | val u = RecUser("bob", Seq(RecUser("tom")))
186 |
187 | val m =
188 | bob tom
189 |
190 | case class User1(name: String, friend: Option[User1] = None)
191 | val u1 = User1("bob", Some(User1("tom")))
192 |
193 | val m1 = bob tom
194 |
195 | "using explicit notation" in {
196 | lazy val w: Write[RecUser, XmlWriter] = To[XmlWriter] { __ =>
197 | ((__ \ "name").write[String] ~ (__ \ "friends").write(seqW(w)))(
198 | unlift(RecUser.unapply))
199 | }
200 | w.writes(u)( ) shouldBe m
201 |
202 | lazy val w2: Write[RecUser, XmlWriter] =
203 | ((Path \ "name").write[String, XmlWriter] ~ (Path \ "friends").write(
204 | seqW(w2)))(unlift(RecUser.unapply))
205 | w2.writes(u)( ) shouldBe m
206 |
207 | lazy val w3: Write[User1, XmlWriter] = To[XmlWriter] { __ =>
208 | ((__ \ "name").write[String] ~ (__ \ "friend").write(optionW(w3)))(
209 | unlift(User1.unapply))
210 | }
211 | w3.writes(u1)( ) shouldBe m1
212 | }
213 |
214 | "using implicit notation" in {
215 | implicit lazy val w: Write[RecUser, XmlWriter] = To[XmlWriter] { __ =>
216 | ((__ \ "name").write[String] ~ (__ \ "friends").write[Seq[RecUser]])(
217 | unlift(RecUser.unapply))
218 | }
219 | w.writes(u)( ) shouldBe m
220 |
221 | implicit lazy val w3: Write[User1, XmlWriter] = To[XmlWriter] { __ =>
222 | ((__ \ "name").write[String] ~ (__ \ "friend").write[Option[User1]])(
223 | unlift(User1.unapply))
224 | }
225 | w3.writes(u1)( ) shouldBe m1
226 | }
227 | }
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/docs/src/main/tut/ScalaValidationRuleCombinators.md:
--------------------------------------------------------------------------------
1 | # Combining Rules
2 |
3 | ## Introduction
4 |
5 | We've already explained what a `Rule` is in [the previous chapter](ScalaValidationRule.md).
6 | Those examples were only covering simple rules. However most of the time, rules are used to validate and transform complex hierarchical objects, like [Json](ScalaValidationJson.md), or [Forms](ScalaValidationMigrationForm.md).
7 |
8 | The validation API allows complex object rules creation by combining simple rules together. This chapter explains how to create complex rules.
9 |
10 | > Despite examples below are validating Json objects, the API is not dedicated only to Json and can be used on any type.
11 | > Please refer to [Validating Json](ScalaValidationJson.md), [Validating Forms](ScalaValidationMigrationForm.md), and [Supporting new types](ScalaValidationExtensions.md) for more information.
12 |
13 | ## Path
14 |
15 | The validation API defines a class named `Path`. A `Path` represents the location of a data among a complex object.
16 | Unlike `JsPath` it is not related to any specific type. It's just a location in some data.
17 | Most of the time, a `Path` is our entry point into the Validation API.
18 |
19 | A `Path` is declared using this syntax:
20 |
21 | ```tut
22 | import jto.validation.Path
23 | val path = Path \ "foo" \ "bar"
24 | ```
25 |
26 | `Path` here is the empty `Path` object. One may call it the root path.
27 |
28 | A path can also reference indexed data, such as a `Seq`
29 |
30 | ```tut
31 | val pi = Path \ "foo" \ 0
32 | ```
33 |
34 | ### Extracting data using `Path`
35 |
36 | Consider the following json:
37 |
38 | ```tut
39 | import play.api.libs.json._
40 |
41 | val js: JsValue = Json.parse("""{
42 | "user": {
43 | "name" : "toto",
44 | "age" : 25,
45 | "email" : "toto@jmail.com",
46 | "isAlive" : true,
47 | "friend" : {
48 | "name" : "tata",
49 | "age" : 20,
50 | "email" : "tata@coldmail.com"
51 | }
52 | }
53 | }""")
54 | ```
55 |
56 | The first step before validating anything is to be able to access a fragment of the complex object.
57 |
58 | 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).
59 |
60 | #### The `read` method
61 |
62 | We start by creating a `Path` representing the location of the data we're interested in:
63 |
64 | ```tut
65 | import jto.validation._
66 | val location: Path = Path \ "user" \ "friend"
67 | ```
68 |
69 | `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`.
70 |
71 | But let's try something much easier for now:
72 |
73 | ```tut:nofail
74 | import jto.validation._
75 | import play.api.libs.json._
76 |
77 | val location: Path = Path \ "user" \ "friend"
78 | val findFriend: Rule[JsValue, JsValue] = location.read[JsValue, JsValue]
79 | ```
80 |
81 | `location.read[JsValue, JsValue]` means we're trying to lookup at `location` in a `JsValue`, and we expect to find a `JsValue` there.
82 | In fact, we're defining a `Rule` that is picking a subtree in a `JsValue`.
83 |
84 | If you try to run that code, the compiler gives you the following error:
85 |
86 | ```tut:nofail
87 | val findFriend: Rule[JsValue, JsValue] = location.read[JsValue, JsValue]
88 | ```
89 |
90 |
91 | 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.
92 |
93 | Fortunately, such method already exists. All you have to do is to import it:
94 |
95 | ```tut
96 | import jto.validation.playjson.Rules._
97 | ```
98 |
99 | > 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.
100 |
101 | With those implicits in scope, we can finally create our `Rule`:
102 |
103 | ```tut:silent
104 | val findFriend: Rule[JsValue, JsValue] = location.read[JsValue, JsValue]
105 | ```
106 |
107 | 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`.
108 |
109 | Now we need to apply this `Rule` to our data:
110 |
111 | ```tut
112 | findFriend.validate(js)
113 | ```
114 |
115 | If we can't find anything, applying a `Rule` leads to a `Invalid`:
116 |
117 | ```tut
118 | (Path \ "foobar").read[JsValue, JsValue].validate(js)
119 | ```
120 |
121 | ### Type coercion
122 |
123 | We now are capable of extracting data at a given `Path`. Let's do it again on a different sub-tree:
124 |
125 | ```tut:silent
126 | val age = (Path \ "user" \ "age").read[JsValue, JsValue]
127 | ```
128 |
129 | Let's apply this new `Rule`:
130 |
131 | ```tut
132 | age.validate(js)
133 | ```
134 |
135 | Again, if the json is invalid:
136 |
137 | ```tut
138 | age.validate(Json.obj())
139 | ```
140 |
141 | The `Invalid` informs us that it could not find `/user/age` in that `JsValue`.
142 |
143 | That example is nice, but we'd certainly prefer to extract `age` as an `Int` rather than a `JsValue`.
144 | All we have to do is to change the output type in our `Rule` definition:
145 |
146 | ```tut:silent
147 | val age = (Path \ "user" \ "age").read[JsValue, Int]
148 | ```
149 |
150 | And apply it:
151 |
152 | ```tut
153 | age.validate(js)
154 | ```
155 |
156 | If we try to parse something that is not an `Int`, we get a `Invalid` with the appropriate Path and error:
157 |
158 | ```tut
159 | (Path \ "user" \ "name").read[JsValue, Int].validate(js)
160 | ```
161 |
162 | So scala *automagically* figures out how to transform a `JsValue` into an `Int`. How does this happen?
163 |
164 | It's fairly simple. The definition of `read` looks like this:
165 |
166 | ```tut:silent
167 | {
168 | def read[I, O](implicit r: Path => Rule[I, O]): Rule[I, O] = ???
169 | }
170 | ```
171 |
172 | So when use `(Path \ "user" \ "age").read[JsValue, Int]`, the compiler looks for an `implicit Path => Rule[JsValue, Int]`, which happens to exist in `jto.validation.json.Rules`.
173 |
174 |
175 | ### Validated
176 |
177 | 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`.
178 |
179 | ```tut:silent
180 | val js = Json.parse("""{
181 | "user": {
182 | "age" : -33
183 | }
184 | }""")
185 |
186 | val age = (Path \ "user" \ "age").read[JsValue, Int]
187 | ```
188 |
189 | Our current implementation of `age` is rather unsatisfying...
190 |
191 | ```tut
192 | age.validate(js)
193 | ```
194 |
195 | We can fix that very simply using `from`, and a built-in `Rule`:
196 |
197 | ```tut:silent
198 | val positiveAge = (Path \ "user" \ "age").from[JsValue](min(0))
199 | ```
200 |
201 | Let's try that again:
202 |
203 | ```tut
204 | positiveAge.validate(js)
205 | ```
206 |
207 | That's better, but still not perfect: 8765 is considered valid:
208 |
209 | ```tut
210 | val js2 = Json.parse("""{ "user": { "age" : 8765 } }""")
211 | positiveAge.validate(js2)
212 | ```
213 |
214 | Let's fix our `age` `Rule`:
215 |
216 | ```tut:silent
217 | val properAge = (Path \ "user" \ "age").from[JsValue](min(0) |+| max(130))
218 | ```
219 |
220 | and test it:
221 |
222 | ```tut:silent
223 | val jsBig = Json.parse("""{ "user": { "age" : 8765 } }""")
224 | properAge.validate(jsBig)
225 | ```
226 |
227 | ### Full example
228 |
229 | ```tut:silent
230 | import jto.validation._
231 | import jto.validation.playjson.Rules._
232 | import play.api.libs.json._
233 |
234 | val js = Json.parse("""{
235 | "user": {
236 | "name" : "toto",
237 | "age" : 25,
238 | "email" : "toto@jmail.com",
239 | "isAlive" : true,
240 | "friend" : {
241 | "name" : "tata",
242 | "age" : 20,
243 | "email" : "tata@coldmail.com"
244 | }
245 | }
246 | }""")
247 |
248 | val age = (Path \ "user" \ "age").from[JsValue](min(0) |+| max(130))
249 | ```
250 | ```tut
251 | age.validate(js)
252 | ```
253 |
254 | ## Combining Rules
255 |
256 | So far we've validated only fragments of our json object.
257 | Now we'd like to validate the entire object, and turn it into an instance of the `User` class defined below:
258 |
259 | ```tut
260 | case class User(
261 | name: String,
262 | age: Int,
263 | email: Option[String],
264 | isAlive: Boolean)
265 | ```
266 |
267 | 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.
268 |
269 | ```tut:silent
270 | import jto.validation._
271 | import play.api.libs.json._
272 |
273 | val userRule: Rule[JsValue, User] = From[JsValue] { __ =>
274 | import jto.validation.playjson.Rules._
275 | ((__ \ "name").read[String] ~
276 | (__ \ "age").read[Int] ~
277 | (__ \ "email").read[Option[String]] ~
278 | (__ \ "isAlive").read[Boolean])(User.apply)
279 | }
280 | ```
281 |
282 | > **Important:** Note that we're importing `Rules._` **inside** the `From[I]{...}` block.
283 | It is recommended to always follow this pattern, as it nicely scopes the implicits, avoiding conflicts and accidental shadowing.
284 |
285 | `From[JsValue]` defines the `I` type of the rules we're combining. We could have written:
286 |
287 | ```scala
288 | (Path \ "name").read[JsValue, String] ~
289 | (Path \ "age").read[JsValue, Int] ~
290 | //...
291 | ```
292 |
293 | but repeating `JsValue` all over the place is just not very DRY.
294 |
--------------------------------------------------------------------------------