-
Add to build.sbt
libraryDependencies += "net.s_mach" %% "validate" % "1.0.0"
-
For Play JSON support, add to build.sbt
libraryDependencies ++= Seq( "net.s_mach" %% "validate" % "1.0.0", "net.s_mach" %% "validate-play-json" % "1.0.0" )
Notes_mach.validate is based on blackbox macro support, present only in Scala 2.11+
s_mach.validate is an open-source Scala library that provides methods for easily building reuseable, composable and serialization format agnostic data validators.
-
You want a validation DSL that is light-weight, terse, composable, reuseable and DRY, written exactly once.
-
You want to write validation code that doesn’t require first converting to a specific serialization format.
-
You want to write validation code that can be re-used for any serialization format.
-
You want to be able to display a light-weight human-readable schema derived from the validation code.
-
Create validators that test validation rules using a light-weight and terse DSL.
-
Write DRY validation code, exactly once, that can be re-used, composed and can be applied to all serialization formats.
-
Validate an instance against a validator to produce a human-readable list of validation failures (List[Rule]).
-
Output a human-readable "schema" of all rules tested and the expected type of each primitive value from any validator using Validator.explain.
-
Macro-generate validators for any product type (i.e. case class or tuple) using Validator.forProductType.
-
Constrain value space of value types (e.g. String, Int, etc) using value classes and Validator.forValueClass.
-
Convert List[Explain] or List[Rule] to human-readable Play JSON using prettyPrintJson method.
-
Compose validators with existing Play Format/Reads by using Format.withValidator or Reads.withValidator convenience methods.
s_mach.validate uses semantic versioning (http://semver.org/). s_mach.validate does not use the package private modifier. Instead, all code files outside of the s_mach.validate.impl package form the public interface and are governed by the rules of semantic versioning. Code files inside the s_mach.validate.impl package may be used by downstream applications and libraries. However, no guarantees are made as to the stability or interface of code in the s_mach.validate.impl package between versions.
$ sbt [info] Set current project to validate (in build file:/Users/lancegatlin/Code/s_mach.validate/) > project validate-play-json [info] Set current project to validate-play-json (in build file:/Users/lancegatlin/Code/s_mach.validate/) > test:console Welcome to Scala version 2.11.6 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_40). Type in expressions to have them evaluated. Type :help for more information. scala> :paste // Entering paste mode (ctrl-D to finish) import scala.collection.immutable.StringOps import s_mach.validate._ import play.api.libs.json._ import s_mach.validate.play_json._ // Use Scala value-class to restrict the value space of String // Name can be treated as String in code // See http://docs.scala-lang.org/overviews/core/value-classes.html implicit class Name( val underlying: String ) extends AnyVal with IsValueClass[String] object Name { import scala.language.implicitConversions // Because Scala doesn't support recursive implicit resolution, need to // add an implicit here to support using Name with StringOps methods such // as foreach, map, etc implicit def stringOps_Name(name: Name) = new StringOps(name.underlying) implicit val validator_Name = // Create a Validator[Name] based on a Validator[String] Validator.forValueClass[Name, String] { import Text._ // Build a Validator[String] by composing some pre-defined validators nonEmpty and maxLength(64) and allLettersOrSpaces } implicit val format_Name = Json // Auto-generate a value-class format from the already existing implicit // Format[String] .forValueClass.format[Name,String](new Name(_)) // Append the serialization-neutral Validator[Name] to the Play JSON Format[Name] .withValidator } implicit class Age( val underlying: Int ) extends AnyVal with IsValueClass[Int] object Age { implicit val validator_Age = { import Validator._ forValueClass[Age,Int]( ensure(s"must be between (0,150)") { age => 0 <= age && age <= 150 } ) } implicit val format_Age = Json.forValueClass.format[Age,Int](new Age(_)).withValidator } case class Person(id: Int, name: Name, age: Age) object Person { implicit val validator_Person = { import Validator._ // Macro generate a Validator for any product type (i.e. case class / tuple) // that implicitly resolves all validators for declared fields. For Person, // Validator[Int] for the id field, Validator[Name] for the name field and // Validator[Age] for the age field are automatically composed into a // Validator[Person]. forProductType[Person] and // Compose the macro generated Validator[Person] with an additional condition ensure( "age plus id must be less than 1000" // p.age is used here as if it was an Int here without any extra code )(p => p.id + p.age < 1000) } implicit val format_Person = Json.format[Person].withValidator } case class Family( father: Person, mother: Person, children: Seq[Person], grandMother: Option[Person], grandFather: Option[Person] ) object Family { implicit val validator_Family = // Macro generate a Validator for Family. Implicit methods in // s_mach.validate.CollectionValidatorImplicits automatically handle creating // Validators for Option and any Scala collection that inherits // scala.collection.Traversable (as long as the contained type has an implicit // Validator). // If set to None, Validator[Option[Person]], checks no Validator[Person] rules. // For Validator[M[A]] (where M[AA] <: Traversable[AA]) the rules of // Validator[Person] are checked for each Person in the collection. Validator.forProductType[Family] // Add some extra constaints using the optional builder syntax .ensure("father must be older than children") { family => family.children.forall(_.age < family.father.age) } .ensure("mother must be older than children") { family => family.children.forall(_.age < family.mother.age) } implicit val format_Family = Json.format[Family].withValidator } // Exiting paste mode, now interpreting. import s_mach.validate._ import play.api.libs.json._ import s_mach.validate.play_json._ defined class Name defined object Name defined class Age defined object Age defined class Person defined object Person defined class Family defined object Family scala> Person(1,"!!!",200) res0: Person = Person(1,!!!,200) scala> res0.validate res1: List[s_mach.validate.Rule] = List(name: must contain only letters or spaces, age: must be between (0,150)) scala> Json.toJson(res0) res2: play.api.libs.json.JsValue = {"id":1,"name":"!!!","age":200} scala> Json.fromJson[Person](res2) res3: play.api.libs.json.JsResult[Person] = JsError(ArrayBuffer((/age,List(ValidationError(List(must be between (0,150)),WrappedArray()))), (/name,List(ValidationError(List(must contain only letters or spaces),WrappedArray()))))) scala> validator[Person].explain.prettyPrintJson res4: String = { "this" : "age plus id must be less than 1000", "id" : [ "must be integer" ], "name" : [ "must be string", "must not be empty", "must not be longer than 64 characters", "must contain only letters or spaces" ], "age" : [ "must be integer", "must be between (0,150)" ] } scala> validator[Name].explain.prettyPrintJson res5: String = [ "must be string", "must not be empty", "must not be longer than 64 characters", "must contain only letters or spaces" ] scala> println(validator[Family].explain.prettyPrintJson) { "this" : [ "father must be older than children", "mother must be older than children" ], "father" : { "this" : "age plus id must be less than 1000", "id" : [ "must be integer" ], "name" : [ "must be string", "must not be empty", "must not be longer than 64 characters", "must contain only letters or spaces" ], "age" : [ "must be integer", "must be between (0,150)" ] }, "mother" : { "this" : "age plus id must be less than 1000", "id" : [ "must be integer" ], "name" : [ "must be string", "must not be empty", "must not be longer than 64 characters", "must contain only letters or spaces" ], "age" : [ "must be integer", "must be between (0,150)" ] }, "children" : { "this" : "must be array of zero or more members", "member" : { "this" : "age plus id must be less than 1000", "id" : [ "must be integer" ], "name" : [ "must be string", "must not be empty", "must not be longer than 64 characters", "must contain only letters or spaces" ], "age" : [ "must be integer", "must be between (0,150)" ] } }, "grandMother" : { "this" : [ "optional", "age plus id must be less than 1000" ], "id" : [ "must be integer" ], "name" : [ "must be string", "must not be empty", "must not be longer than 64 characters", "must contain only letters or spaces" ], "age" : [ "must be integer", "must be between (0,150)" ] }, "grandFather" : { "this" : [ "optional", "age plus id must be less than 1000" ], "id" : [ "must be integer" ], "name" : [ "must be string", "must not be empty", "must not be longer than 64 characters", "must contain only letters or spaces" ], "age" : [ "must be integer", "must be between (0,150)" ] } }