Bond
Type-level validation for Scala
Bond provides a mechanism to express validation constraints by using the type system. A quick example:
import net.fwbrasil.bond._
case class Employee (
name: String with NonEmpty,
email: String with Email,
age: Int with GreaterThan[T.`14`.T]
)
case class Company (
employees: List[Employee] with MinSize[T.`2`.T]
)
Note: Scala does not have support for easily defining singleton types. The
T.`2`.T
definition is a shortcut forshapeless.Witness.`2`.T
, that produces the singleton type by using a macro. It is possible to define singleton types forString
as well:T.`"a"`.T
.
The type-level computations allow the application of constraint transformations at compile time. The 'lift' method is used to apply such transformations:
import net.fwbrasil.bond._
val employeeAge: Int with GreaterThan[T.`14`.T] = ...
val lifted: Int with GreaterThan[T.`12`.T] =
GreaterThan(12).lift(employee.age)
Observe that GreaterThan(14)
is liftable to GreaterThan(12)
because any number that is greater than 14
will be greater than 12
. The transformation is not applicable for GreaterThan(18)
:
GreaterThan(18).lift(employee.age)
[error] The lifting of 'Int with net.fwbrasil.bond.GreaterThan[Int(14)]'' to 'net.fwbrasil.bond.GreaterThan[Int(18)]' is not valid (Bond)
[error] GreaterThan(18).lift(employee.age)
[error] ^
As a previous step of type-level validations, it is necessary to apply value-level validations. In an ideal application, the value-level validations should happen on the boundaries where it is necessary to interface with non-validated values. For instance, the boundaries could be the user interface or a database interaction.
Bond provides a validation DSL that is capable of accumulating multiple errors:
import net.fwbrasil.bond._
object controller {
def createEmployeeRestEndpoint(name: String, email: String, age: Int) =
applyValueLevelValidations(name, email, age) match {
case Valid((name, email, age)) => render.json(Employee(name, email, age))
case Invalid(violations) => render.badRequest(violations)
}
private def applyValueLevelValidations(name: String, email: String, age: Int): Result[(String with NonEmpty, String with Email, Int with GreaterThan[T.`14`.T])] =
for {
name <- NonEmpty.validate(name)
email <- Email.validate(email)
age <- GreaterThan(14).validate(age)
} yield {
(name, email, age)
}
private object render {
def json(v: Any) = println(s"OK: $v")
def badRequest(v: Any) = println(s"BadRequest: $v")
}
}
Execution examples:
scala> controller.createEmployeeRestEndpoint("", "b", 1)
BadRequest: List(Violation(,NonEmpty), Violation(b,Email), Violation(1,GreaterThan(14)))
scala> controller.createEmployeeRestEndpoint("a", "b", 1)
BadRequest: List(Violation(b,Email), Violation(1,GreaterThan(14)))
scala> controller.createEmployeeRestEndpoint("a", "[email protected]", 1)
BadRequest: List(Violation(1,GreaterThan(14)))
scala> controller.createEmployeeRestEndpoint("a", "[email protected]", 17)
OK: Employee(a,b@a.com,17)
Getting started
To use bond, just add the dependency to the project's build configuration.
Important: Bond is available only for Scala 2.11.x
. Change x.x.x
with the latest version listed in the CHANGELOG.md file.
SBT
libraryDependencies ++= Seq(
"net.fwbrasil" %% "bond" % "x.x.x"
)
Maven
<dependency>
<groupId>net.fwbrasil</groupId>
<artifactId>bond</artifactId>
<version>x.x.x</version>
</dependency>
Built-in Validations
These validators can be used with any type that is convertible to Iterable[_]
. This includes List
, Seq
, String
and many other types.
- NonEmpty
- Empty
- Size(N)
- MinSize(N)
- MaxSize(N)
Validators to be used with any type that is convertible to java.lang.Number
.
- GreaterThan(N)
- GreaterThanOrEqual(N)
- LesserThan(N)
- LesserThanOrEqual(N)
- Odd
- Even
- Prime
- Perfect
- CreditCard
- StartsWith(S)
- EndsWith(S)
- MatchesRegex(S)
- URI
- URL
- UUID
- False
- True
- IsNull
- IsNotNull
Custom Validations
Example of custom validation:
trait Adult
case object Adult
extends Validator[Employee, Adult] {
def isValid(e: Employee) =
e.age >= 21
}
Example usage:
def registerForDangerousJob(employee: Employee with Adult) = ...
Important: Scala does not provide a mechanism to define the macro expansion order and the lift macro depends on the validation class. This means that you need to manually guarantee that the validation class is compiled before the lift
macro expansion. There are some workarounds to influence the compilation order:
- Use a separate source folder that compiles before the main source folder.
- Define the custom validations inside a separate sub-module
Versioning
Bond adheres to Semantic Versioning 2.0.0. If there is a violation of this scheme, report it as a bug. Specifically, if a patch or minor version is released and breaks backward compatibility, that version should be immediately yanked and/or a new version should be immediately released that restores compatibility. Any change that breaks the public API will only be introduced at a major-version release.
License
See the LICENSE-LGPL file for details.