Constrain: Object Constraints for Dart

Introduction

Provides a constraint based Validation library inspired by Java Bean Validation but leveraging the superior language capabilities of Dart. In particular:

Warning: This is very much a work in progress at this stage so use with caution. It will be fleshed out soon

Usage

Define Constraints On Your Objects

Whilst you can subclass Constraint and create your own custom constraints (as is done in Java), thanks to the goodness of the matcher library we can get gazillions of constraints for free.

The Constrain library provides a matcher based constraint called Ensure. This takes a matcher as an argument.

Armed with this you can go crazy. If there is no matcher for what you want then add one.

The main catch is that annotations must be const. This means that the matchers used with the Ensure constraint must be const. Property ones like isNotNull, isNotEmpty are already const. Ones that take an argument though are not.

To get around this, Ensure takes either a Matcher or a Function that returns a Macher. This function needs to qualify as const. So static methods work.

The following shows a bunch of matcher based constraints.

class Address {
  String street;

  String toString() => 'Address[street: $street]';
}

isNotEmpty() => isNot(isEmpty);

class Person {
  @Ensure(_isBetween10and90)
  int age;

  @Ensure(isNotEmpty)
  @Ensure(_allStreetsStartWith15)
  List<Address> addresses;

  static _allStreetsStartWith15() => everyElement(startsWith("15"));
  static _isBetween10and90() => allOf(greaterThan(10), lessThanOrEqualTo(90));

  String toString() => 'Person[age: $age, addressses: $addresses]';
}

Validate

Now you can create instances of your objects and validate them.

final Person person = new Person()
  ..age = 6
  ..addresses = [new Address()..street = "16 blah st"];

Validator v = new Validator();
Set<ConstraintViolation> violations = v.validate(person);
print(violations);
	

This prints

{Invalid Person (Person[age: 6, addressses: [Address[street: 16 blah st]]]). Constraint violated at path Symbol("age")
Expected: (a value greater than <10> and a value less than or equal to <90>)
  Actual: <6>
   Which: is not a value greater than <10>
, Invalid Person (Person[age: 6, addressses: [Address[street: 16 blah st]]]). Constraint violated at path Symbol("addresses")
Expected: every element(a string starting with '15')
  Actual: [Address:Address[street: 16 blah st]]
   Which: has value Address:<Address[street: 16 blah st]> which Address:<Address[street: 16 blah st]> not a string at index 0
}

Which looks rather crap at the moment but that will be improved soon.

Note also that (just like in Java) the ConstraintViolation class is a structured object so in addition to a message you can get lots of details about exactly what was violated where.

Advanced: Look up Constraints

If you want to do something fancier, for example, integrate with some library (like Polymer, Json Schema etc.) then you may need to directly work with the constraints.

final resolver = new TypeDescriptorResolver();
final typeDescriptor = resolver.resolveFor(Person);
// now you can tranverse the descriptor to get all the constraints on this class and as transitively reachable.

TODO

Lots to do here. Some examples:

  • Implement cascading validation. Currently it only validates the immediate object.
  • Look into possible implementation via Source Mirrors and generating code for constraint resolvers and validators. Currently it is all at runtime.
  • Integration with other libraries like Polymer, Json Schema (generation).

Libraries

constrain
constraint
constraint.metadata
constraint.validate