-
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)" ]
}
}
