Skip to content
s_mach.validate is an open-source Scala library that provides methods for easily building reuseable, composable and serialization format agnostic data validators.
Scala
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
project
src
validate-core/src/main/scala/s_mach/validate
validate-play-json/src
.gitignore
.travis.yml
LICENSE
README.asciidoc
build.sbt
publish_checklist.asciidoc

README.asciidoc

s_mach.validate: data validators

Include in SBT

  1. Add to build.sbt

    libraryDependencies += "net.s_mach" %% "validate" % "1.0.0"
  2. 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"
    )
    Note
    s_mach.validate is based on blackbox macro support, present only in Scala 2.11+

Overview

s_mach.validate is an open-source Scala library that provides methods for easily building reuseable, composable and serialization format agnostic data validators.

Why do I need this?

  • 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.

Features

  • 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.

Versioning

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.

Example

$ 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)" ]
  }
}
You can’t perform that action at this time.