├── 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 | [![Travis](https://api.travis-ci.org/jto/validation.png?branch=master)](https://travis-ci.org/jto/validation) [![Coverage Status](https://coveralls.io/repos/github/jto/validation/badge.svg)](https://coveralls.io/github/jto/validation) [![Maven](https://img.shields.io/maven-central/v/io.github.jto/validation-core_2.11.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.jto/validation-core_2.11) [![Scala.js](https://www.scala-js.org/assets/badges/scalajs-0.6.8.svg)](https://www.scala-js.org) [![Gitter](https://badges.gitter.im/jto/validation.svg)](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 | 
29 | 41 | 42 |
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 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 123 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 Julien28 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 foobar 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.com01.23.45.67.8998.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 | bobtom 189 | 190 | case class User1(name: String, friend: Option[User1] = None) 191 | val u1 = User1("bob", Some(User1("tom"))) 192 | 193 | val m1 = bobtom 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 | --------------------------------------------------------------------------------