This commit is contained in:
thosakwe 2016-12-26 08:04:42 -05:00
parent 9dd2925a86
commit bc2048669e
6 changed files with 188 additions and 42 deletions

View file

@ -17,8 +17,11 @@ For convenience's sake, this library also exports `matcher`.
* [Creating a Validator](#creating-a-validator)
* [Validating Data](#validating-data)
* [Required Fields](#required-fields)
* [Forbidden Fields](#forbidden-fields)
* [Default Values](#default-values)
* [Custom Validator Functions](#custom-validator-functions)
* [Auto-parsing Numbers](#autoparse)
* [Custom Error Messages](#custom-error-messages)
* [Extending Validators](#extending-validators)
* [Bundled Matchers](#bundled-matchers)
* [Nested Validators](#nested-validators)
@ -88,6 +91,10 @@ main() {
}
```
## Forbidden Fields
To prevent a field from showing up in valid data, suffix it
with a `'!'`.
## Default values
@ -127,10 +134,39 @@ main() {
}
```
# Custom Error Messages
If these are not present, `angel_validate` will *attempt* to generate
a coherent error message on its own.
```dart
new Validator({
'age': [greaterThanOrEqualTo(18)]
}, customErrorMessages: {
'age': 'You must be an adult to see this page.'
});
```
# autoParse
Oftentimes, fields that we want to validate as numbers are passed as strings.
Calling `autoParse` will correct this before validation.
```dart
main() {
var parsed = autoParse({
'age': '34',
'weight': '135.6'
}, ['age', 'weight']);
validator.enforce(parsed);
}
```
You can also call `checkParsed` or `enforceParsed` as a shorthand.
# Extending Validators
You can add situation-specific rules within a child validator.
You can also use `extend` to mark fields as required that originally
were not. Default value extension is also supported.
You can also use `extend` to mark fields as required or forbidden that originally
were not. Default value and custom error message extension is also supported.
```dart
final Validator userValidator = new Validator({
@ -170,12 +206,12 @@ register(Map userData) {
This library includes some `Matcher`s for common validations,
including:
* `isAlphaDash`: Asserts a `String` matches the Regular Expression ```/^[A-Za-z0-9_-]$/```.
* `isAlphaNum`: Asserts a `String` matches the Regular Expression ```/^[A-Za-z0-9]$/```
* `isAlphaDash`: Asserts that a `String` is alphanumeric, but also lets it contain dashes or underscores.
* `isAlphaNum`: Asserts that a `String` is alphanumeric.
* `isBool`: Asserts that a value either equals `true` or `false`.
* `isEmail`: Asserts a `String` complies to the RFC 5322 e-mail standard.
* `isInt`: Asserts a value is an `int`.
* `isNum`: Asserts a value is a `num`.
* `isEmail`: Asserts that a `String` complies to the RFC 5322 e-mail standard.
* `isInt`: Asserts that a value is an `int`.
* `isNum`: Asserts that a value is a `num`.
* `isString`: Asserts that a value is a `String`.
The remaining functionality is
@ -218,10 +254,12 @@ main() {
# Use with Angel
`server.dart` exposes three helper middleware:
`server.dart` exposes five helper middleware:
* `validate(validator)`: Validates and filters `req.body`, and throws an `AngelHttpException.BadRequest` if data is invalid.
* `validateEvent(validator)`: Sets `e.data` to the result of validation on a service event.
* `validateQuery(validator)`: Same as `validate`, but operates `req.query`.
* `validateQuery(validator)`: Same as `validate`, but operates on `req.query`.
* `autoParseBody(fields)`: Auto-parses numbers in `req.body`.
* `autoParseQuery(fields)`: Same as `autoParseBody`, but operates on `req.query`.
```dart
import 'package:angel_framework/angel_framework.dart';

View file

@ -5,6 +5,22 @@ import 'package:angel_framework/angel_framework.dart';
import 'angel_validate.dart';
export 'angel_validate.dart';
/// Auto-parses numbers in `req.body`.
RequestMiddleware autoParseBody(List<String> fields) {
return (RequestContext req, res) async {
req.body.addAll(autoParse(req.body, fields));
return true;
};
}
/// Auto-parses numbers in `req.query`.
RequestMiddleware autoParseQuery(List<String> fields) {
return (RequestContext req, res) async {
req.query.addAll(autoParse(req.query, fields));
return true;
};
}
/// Validates the data in `req.body`, and sets the body to
/// filtered data before continuing the response.
RequestMiddleware validate(Validator validator,

View file

@ -1,13 +1,31 @@
import 'package:matcher/matcher.dart';
final RegExp _alphaDash = new RegExp(r'^[A-Za-z0-9_-]+$');
final RegExp _alphaNum = new RegExp(r'^[A-Za-z0-9]+$');
final RegExp _email = new RegExp(
r"^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$");
/// Asserts that a `String` is alphanumeric, but also lets it contain dashes or underscores.
final Matcher isAlphaDash = predicate(
(value) => value is String && _alphaDash.hasMatch(value),
'alphanumeric (dashes and underscores are allowed) ');
/// Asserts that a `String` is alphanumeric, but also lets it contain dashes or underscores.
final Matcher isAlphaNum = predicate(
(value) => value is String && _alphaNum.hasMatch(value), 'alphanumeric ');
/// Asserts that a value either equals `true` or `false`.
final Matcher isBool = predicate((value) => value is String, 'a bool ');
final Matcher isBool = predicate((value) => value is bool, 'a bool ');
/// Asserts that a `String` complies to the RFC 5322 e-mail standard.
final Matcher isEmail = predicate(
(value) => value is String && _email.hasMatch(value), 'a valid e-mail ');
/// Asserts that a value is an `int`.
final Matcher isInt = predicate((value) => value is String, 'an integer ');
final Matcher isInt = predicate((value) => value is int, 'an integer ');
/// Asserts that a value is a `num`.
final Matcher isNumber = predicate((value) => value is String, 'a number ');
final Matcher isNum = predicate((value) => value is num, 'a number ');
/// Asserts that a value is a `String`.
final Matcher isString = predicate((value) => value is String, 'a String ');

View file

@ -1,6 +1,7 @@
import 'package:matcher/matcher.dart';
final RegExp _asterisk = new RegExp(r'\*$');
final RegExp _forbidden = new RegExp(r'\!$');
final RegExp _optional = new RegExp(r'\?$');
/// Returns a value based the result of a computation.
@ -9,11 +10,37 @@ typedef DefaultValueFunction();
/// Determines if a value is valid.
typedef bool Filter(value);
/// Converts the desired fields to their numeric representations, if present.
Map<String, dynamic> autoParse(Map inputData, List<String> fields) {
Map<String, dynamic> data = {};
for (var key in inputData.keys) {
if (!fields.contains(key)) {
data[key] = inputData[key];
} else {
try {
var n = num.parse(inputData[key].toString());
data[key] = n == n.toInt() ? n.toInt() : n;
} catch (e) {
// Invalid number, don't pass it
}
}
}
return data;
}
/// Enforces the validity of input data, according to [Matcher]s.
class Validator extends Matcher {
/// Pre-defined error messages for certain fields.
final Map<String, String> customErrorMessages = {};
/// Values that will be filled for fields if they are not present.
final Map<String, dynamic> defaultValues = {};
/// Fields that cannot be present in valid data.
final List<String> forbiddenFields = [];
/// Conditions that must be met for input data to be considered valid.
final Map<String, List<Matcher>> rules = {};
@ -22,10 +49,16 @@ class Validator extends Matcher {
void _importSchema(Map<String, dynamic> schema) {
for (var key in schema.keys) {
var fieldName = key.replaceAll(_asterisk, '');
var isRequired = _asterisk.hasMatch(key);
var fieldName = key
.replaceAll(_asterisk, '')
.replaceAll(_forbidden, '')
.replaceAll(_optional, '');
var isForbidden = _forbidden.hasMatch(key),
isRequired = _asterisk.hasMatch(key);
if (isRequired) {
if (isForbidden) {
forbiddenFields.add(fieldName);
} else if (isRequired) {
requiredFields.add(fieldName);
}
@ -47,8 +80,10 @@ class Validator extends Matcher {
Validator.empty();
Validator(Map<String, dynamic> schema,
{Map<String, dynamic> defaultValues: const {}}) {
{Map<String, dynamic> defaultValues: const {},
Map<String, dynamic> customErrorMessages: const {}}) {
this.defaultValues.addAll(defaultValues ?? {});
this.customErrorMessages.addAll(customErrorMessages ?? {});
_importSchema(schema);
}
@ -65,9 +100,21 @@ class Validator extends Matcher {
}
}
for (String field in forbiddenFields) {
if (input.containsKey(field)) {
if (!customErrorMessages.containsKey(field))
errors.add("'$field' is forbidden.");
else
errors.add(customErrorMessages[field]);
}
}
for (String field in requiredFields) {
if (!input.containsKey(field)) {
errors.add("'$field' is required.");
if (!customErrorMessages.containsKey(field))
errors.add("'$field' is required.");
else
errors.add(customErrorMessages[field]);
}
}
@ -88,7 +135,8 @@ class Validator extends Matcher {
}
} else {
if (!matcher.matches(value, {})) {
errors.add(matcher.describe(description).toString().trim());
if (!customErrorMessages.containsKey(key))
errors.add(matcher.describe(description).toString().trim());
valid = false;
}
}
@ -100,6 +148,8 @@ class Validator extends Matcher {
if (valid) {
data[key] = value;
} else if (customErrorMessages.containsKey(key)) {
errors.add(customErrorMessages[key]);
}
}
}
@ -111,6 +161,10 @@ class Validator extends Matcher {
return new ValidationResult().._data = data;
}
/// Validates, and filters input data after running [autoParse].
ValidationResult checkParsed(Map inputData, List<String> fields) =>
check(autoParse(inputData, fields));
/// Validates input data, and throws an error if it is invalid.
///
/// Otherwise, the filtered data is returned.
@ -127,40 +181,46 @@ class Validator extends Matcher {
/// Creates a copy with additional validation rules.
Validator extend(Map<String, dynamic> schema,
{Map<String, dynamic> defaultValues: const {}, bool overwrite: false}) {
{Map<String, dynamic> defaultValues: const {},
Map<String, String> customErrorMessages: const {},
bool overwrite: false}) {
Map<String, dynamic> _schema = {};
var child = new Validator.empty()
..defaultValues.addAll(this.defaultValues)
..defaultValues.addAll(defaultValues ?? {})
..customErrorMessages.addAll(this.customErrorMessages)
..customErrorMessages.addAll(customErrorMessages ?? {})
..requiredFields.addAll(requiredFields)
..rules.addAll(rules);
if (overwrite) {
for (var key in schema.keys) {
var fieldName = key.replaceAll(_asterisk, '').replaceAll(_optional, '');
var isOptional = _optional.hasMatch(key);
var isRequired = _asterisk.hasMatch(key);
for (var key in schema.keys) {
var fieldName = key
.replaceAll(_asterisk, '')
.replaceAll(_forbidden, '')
.replaceAll(_optional, '');
var isForbidden = _forbidden.hasMatch(key);
var isOptional = _optional.hasMatch(key);
var isRequired = _asterisk.hasMatch(key);
if (isOptional)
child.requiredFields.remove(fieldName);
else if (isRequired) child.requiredFields.add(fieldName);
if (child.rules.containsKey(key)) child.rules.remove(key);
_schema[fieldName] = schema[key];
if (isForbidden) {
child
..requiredFields.remove(fieldName)
..forbiddenFields.add(fieldName);
} else if (isOptional) {
child
..forbiddenFields.remove(fieldName)
..requiredFields.remove(fieldName);
} else if (isRequired) {
child
..forbiddenFields.remove(fieldName)
..requiredFields.add(fieldName);
}
} else {
for (var key in schema.keys) {
var fieldName = key.replaceAll(_asterisk, '').replaceAll(_optional, '');
var isOptional = _optional.hasMatch(key);
var isRequired = _asterisk.hasMatch(key);
if (isOptional)
child.requiredFields.remove(fieldName);
else if (isRequired) child.requiredFields.add(fieldName);
_schema[fieldName] = schema[key];
if (overwrite) {
if (child.rules.containsKey(fieldName)) child.rules.remove(fieldName);
}
_schema[fieldName] = schema[key];
}
return child.._importSchema(_schema);

View file

@ -1,6 +1,6 @@
name: angel_validate
description: Cross-platform validation library based on `matcher`.
version: 0.0.0
version: 0.0.1
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/validate
environment:

View file

@ -1,6 +1,12 @@
import 'package:angel_validate/angel_validate.dart';
import 'package:test/test.dart';
final Validator emailSchema = new Validator({
'to': [isNum, isPositive]
}, customErrorMessages: {
'to': 'Hello, world!'
});
final Validator todoSchema = new Validator({
'id': [isInt, isPositive],
'text*': isString,
@ -10,6 +16,14 @@ final Validator todoSchema = new Validator({
});
main() {
test('custom error message', () {
var result = emailSchema.check({'to': 2});
expect(result.errors, isList);
expect(result.errors, hasLength(1));
expect(result.errors.first, equals('Hello, world!'));
});
test('todo', () {
expect(() {
todoSchema