v0.2.0+2 of constrain released. Constrain: Object Constraints - TopicsExpress



          

v0.2.0+2 of constrain released. 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: much more powerful annotations functions and an extensive matcher library Warning: Runtime Mirrors Used. Features Class level constraints for cross field validation Property constraints (also via getter) Constraint Inheritance Cascading validation Constraint Groups Constraints can be specified with: Dart functions matchers from the matchers library Detailed constraint violation model with json support Constraints on function parameters and returns Constraints on method parameters and returns with inheritance A core set of common constraints such as Min, Max and Pattern Usage Key Concept - Mandatoriness One of the most important concepts to remember is that all constraints on a property (other than @NotNull) are only applied to a property when its value is not null. So for example, you may have a constraint that defines what makes an email address valid. You can apply this to optional properties as well as mandatory ones. So for example if you have a mandatory home email and an optional work email you may have something like class Contacts { @NotNull() @Ensure(isValidEmail) String homeEmail; @Ensure(isValidEmail) String workEmail; } In effect constraints are applied as follows (illustrative only): if the @NotNull constraint is present apply that if the value is not null apply all other constraints So in the case of Contacts the homeEmail property must have a value and it must satisfy isValidEmail. However, workEmail is valid if it is null, but if it does have a value it must also satisfy isValidEmail. Define Constraints On Your Objects Note: It is recommended that you use one of the core constraints when one exists for your need. This will allow you to take advantage of packages that may be built in the future such as code generators from JSON schema that support them. See the section on Core Constraints below The following (rather contrived) example illustrates several features of constraints, which are described below. class Primate { @Ensure(isPositive) int age; } @Ensure(eitherOlderThan20OrHasAtLeastTwoAddresses, description: Must be either older than twenty or have at least two adresses) class Person extends Primate { @Ensure(isBetween10and90) int get age => super.age; @NotNull() @Ensure(allStreetsStartWith15, group: const Detailed()) List addresses; @Ensure(cantBeYourOwnParent, description: A person cannot be their own parent) Set parents; String toString() => Person[age: $age, addressses: $addresses]; } class Address { @Ensure(streetIsAtLeast10Characters) String street; String toString() => Address[street: $street]; } Matcher Constraints The first constraint we see is on the Primates age property @Ensure(isPositive) int age; isPositive is a const matcher that comes with the matcher library. In short a primates age will satisfy this constraint if it is greater than 0. Using matchers is a common way to specify constraints. When matcher based constraints are violated they provide details about what went wrong. Constraint Inheritance If you look at the Person class you will see that it extends Primate. This means that it will automatically inherit the age property and the isPositive constraint on it. That is, Persons will also be subject to this constraint. You can also see that Person has a getter on age as follows @Ensure(isBetween10and90) int get age => super.age; It simply redirects to the primates age and exists soley so that we can further constrain age (admittedly with a rather silly constraint). isBetween10and90 is another matcher but this time not a const so we must use a function to wrap it as follows Matcher isBetween10and90() => allOf(greaterThanOrEqualTo(10), lessThanOrEqualTo(90)); Note there is no NotNull constraint on age. This means it is allowed to be null and the other constraints (isPositive and isBetween10and90) will only be applied if it is non null. Next we can see that the addresses property has two constraints @NotNull() @Ensure(allStreetsStartWith15, group: const Detailed()) List addresses; NotNull NotNull indicates a property is mandatory. allStreetsStartWith15 illustrates two more features. Constraint Groups Firstly it specifies a group called Detailed. This means that this constraint will only be validated when that group is validated (as covered in the Validation section below). Boolean Function Constraints Secondly, it is an example of a boolean expression based constraint bool allStreetsStartWith15(List addresses) => addresses.every((a) => a.street == null || a.street.startsWith(15)); In addition to matchers, you can also use plain ol Dart code for your constraints. Note: as Dart does not have null safe path expressions you need to check each segment or risk an NPE Note that even though this constraint depends only on a single field of the Address class it is not defined on the Address classs street property. The reason is, that it is not intended to be true for all uses of Address, just those that are owned by Persons. Keep this in mind when you decide where constraints should live. The parents property illustrates yet another two features @Ensure(cantBeYourOwnParent, description: A person cannot be their own parent) Set parents; Constraint Descriptions Firstly, it contains a description named argument. This controls how the constraint will be referred to (e.g. when it is violated). Boolean Expressions with Owner Secondly, it is another form of boolean function constraint bool cantBeYourOwnParent(Set parents, Person person) => !parents.contains(person); Note the second argument person. This is the Person object that owns the parents field being validated. As you can see, this was needed to express this constraint. Most constraints dont need it but its very useful at times. Class Based Constraints If we jump back to the Person class you will notice a constraint on the class itself @Ensure(eitherOlderThan20OrHasAtLeastTwoAddresses, description: Must be either older than twenty or have at least two adresses) This is where you put cross field constraints. In other words, constraints that require more than one field of the class to express. Matcher eitherOlderThan20OrHasAtLeastTwoAddresses() => anyOf(hasAge(greaterThan(20)), hasAddresses(hasLength(greaterThanOrEqualTo(two)))); Note that class based constraints are also inherited. Cascading Lastly, we come to the Address class and the constraint on street @Ensure(streetIsAtLeast10Characters) String street; There is nothing terribly interesting about the constraint itself. Whats interesting is in the context of validating a Person. In order for the addresses property of Person to be considered valid it requires that each Address object is also valid. This means that the street property of each address must be at least 10 characters in length. Validate your Constrained Objects Now you can create instances of your objects and validate them. final Person person = new Person() ..age = -22 ..addresses = [new Address()..street = 16 blah st]; Validator v = new Validator(); Set violations = v.validate(person); print(violations); This prints Constraint violated at path Symbol(addresses).Symbol(street) Expected: an object with length of a value greater than or equal to Actual: 15 x st Which: has length of Constraint violated at path Symbol(parents) A person cannot be their own parent Constraint violated at path Symbol(age) Expected: (a value greater than or equal to and a value less than or equal to ) Actual: Which: is not a value greater than or equal to Constraint violated at path Symbol(age) Expected: a positive value Actual: Which: is not a positive value Constraint violated at path Must be either older than twenty or have at least two adresses Expected: (Person with age that a value greater than or Person with addresses that an object with length of a value greater than or equal to two) Actual: Person: Note, depending on the audience you may not simply print the violations like this. 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. When integrating with UI frameworks like polymer, you would typically use the structured information to provide better error messages. Specifying Constraint descriptions provide you complete control the wording of a constraint and is typically what you would want to show to the user. Validating Groups The model contained a single group called Detailed that was applied to the addresses property. It was excluded from validation in the previous example which was validating against the DefaultGroup To include this constraint too specify the groups as follows final violations = v.validate(person, groups: [const Detailed()]); Core Constraints Core constraints are useful to simplify adding common constraints and also for integrating with external constraint defintions (for example JSON Schema, XML Schema, HTML / Polymer Input Fields). Constrain provides a core set of constraints. Currently this includes Min and Max @Min(10.2) @Max(40.7, isInclusive: false) double foo; Min and Max can be applied to any Comparable that has a meaningful sense of ordering and can be made a const This includes all nums (int, double). Unfortunately DateTime doesnt have const construcutor. New constraints will likely be created for the DateTime equivalents of Min and Max in the future (like Before and After) Both Min and Max provide a bool property called isInclusive. Pattern @Pattern(r[\w]+-[\d]+) String id; Pattern allows you to constrain a String field with anything that implements the Pattern class. By default it assumes you give it a RegExp and does the conversion (because RegExp does not have a const constructor) You can prevent the conversion to RegExp with th isRegExp parameter @Pattern(a plain ol string, isRegExp: false) String id; Note: dart:core defines a class called Pattern. Using constrains Pattern will result in a warning that it is hiding the dart:core version. To get rid of this warning you need to add import dart:core hide Pattern; or else import constrain with a prefix like import package:constrain/constrain.dart as c;. To avoid this name clash Pattern will likely be renamed in the future JSON Encoding The rich model for constraint violations can be converted to JSON, for example to send it between a server and client. The detailed information allows clients to be intelligent about how they report the errors to the user final Person person = new Person() ..age = -22 ..addresses = [new Address()..street = 16 blah st]; Validator v = new Validator(); Set violations = v.validate(person); print(JSON.encode(violations)); prints (abbreviated) [ { constraint: { type: Ensure, description: null, group: DefaultGroup }, message: Constraint violated at path age\nExpected: (a value greater than or equal to and a value less than or equal to )\n Actual: \n Which: is not a value greater than or equal to \n, rootObject: { type: Person, value: { age: -22, parents: null, addresses: [ { street: 16 blah st } ] } }, leafObject: { type: Person, value: { age: -22, parents: null, addresses: [ { street: 16 blah st } ] } }, invalidValue: { type: int, value: -22 }, propertyPath: age, details: { expected: (a value greater than or equal to and a value less than or equal to ), actual: , mismatchDescription: is not a value greater than or equal to }, reason: null }, ...... Function Constraints You can add constraints to function parameters (positional and named) and return values @NotNull() String bar(@NotNull() int blah, String foo) => $blah; Similarly you can add constraints to class methods. Constraints will be inherited from: super classes interfaces mixins class Foo { String bar(@NotNull() int blah, String foo) => $blah; } class Blah extends Object with Foo { @NotNull() String bar(@Ensure(isBetween10and90) int blah, @NotNull() String foo) => $blah; } Validating Parameters validator.validateFunctionParameters(bar, [1, foo]) or for methods validator.validateFunctionParameters(new Foo().bar, [1, foo]) Validating Returns validator.validateFunctionReturn(new Foo().bar, some return value) Details Constraints The Constraint Class All constraints must implement (typically indirectly) the Constraint class. Its key method is void validate(T value, ConstraintValidationContext context); It is passed the value to be validated and a context. If the constraint is violated then it creates a ConstraintViolation by calling the contexts addViolation method void addViolation({String reason, ViolationDetails details}); optionally providing details and a reason. Typically you will not use Constraint directly or subclass it directly. The only two subtypes currently (and likely to remain that way) are: NotNull and Ensure If you do directly subtype Constraint then you will need to deal with a possible null value being passed to validate. This is not the case with Ensure. NotNull The NotNull constraint indicates the field is mandatory. It is somewhat special as it directly subclasses Constraint. Ensure Ensure is the main constraint subclass that the vast majority of constraints are likely to use. Ensure delegates the actual validation to its validator object. Note that the validator will only be called if the value passed to Ensures validate method is not null. The validator can be any of the following: 1. A ConstraintValidator function The ConstraintValidator function is the same signature as the validate method on Constraint. It is defined as typedef void ConstraintValidator(dynamic value, ConstraintValidationContext context); This is best used when you want control over the creation of the ConstraintViolation object. Note the owner of the value is available via the context 2. A SimpleConstraintValidator function This is a simplified form of validator function which is just a boolean expression indicating whether the constraint is valid. typedef bool SimpleConstraintValidator(dynamic value); This is typically used in preference to ConstraintValidator. 3. A SimplePropertyConstraintValidator function Contains the owner of the value as an additional argument. typedef bool SimplePropertyConstraintValidator(dynamic value, dynamic owner); 4. A Matcher Any const matcher can be used such as isEmpty. 5. A function that returns a Matcher Specically a function that adheres to typedef Matcher MatcherCreator(); This is the more common way to use matchers as most matchers take an argument and are accessed via function. These cannot currently be const in Dart so the workaround is to use a function that returns the matcher. Constraint Groups ConstraintGroups are used to restrict which constraints are validated during a validation. The ConstraintGroup is defined as abstract class ConstraintGroup { bool implies(ConstraintGroup other); } Matching for ConstraintGroups is done via the implies method. This method should return true to indicate that the constraint should be validated. To define your own simple group extend SimpleConstraintGroup and make sure you inclued a const constructor. class Detailed extends SimpleConstraintGroup { const Detailed(); } To compose a group out of other groups extend CompositeConstraintGroup class GroupOneAndTwo extends CompositeGroup { const GroupOneAndTwo() : super(const [const GroupOne(), const GroupTwo()]); } Looking 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. Further Reading Blog on Constraints TODO See open issues #dartlang
Posted on: Sat, 17 Jan 2015 03:14:51 +0000

Recently Viewed Topics




© 2015