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) * [Creating a Validator](#creating-a-validator)
* [Validating Data](#validating-data) * [Validating Data](#validating-data)
* [Required Fields](#required-fields) * [Required Fields](#required-fields)
* [Forbidden Fields](#forbidden-fields)
* [Default Values](#default-values) * [Default Values](#default-values)
* [Custom Validator Functions](#custom-validator-functions) * [Custom Validator Functions](#custom-validator-functions)
* [Auto-parsing Numbers](#autoparse)
* [Custom Error Messages](#custom-error-messages)
* [Extending Validators](#extending-validators) * [Extending Validators](#extending-validators)
* [Bundled Matchers](#bundled-matchers) * [Bundled Matchers](#bundled-matchers)
* [Nested Validators](#nested-validators) * [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 ## 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 # Extending Validators
You can add situation-specific rules within a child validator. You can add situation-specific rules within a child validator.
You can also use `extend` to mark fields as required that originally You can also use `extend` to mark fields as required or forbidden that originally
were not. Default value extension is also supported. were not. Default value and custom error message extension is also supported.
```dart ```dart
final Validator userValidator = new Validator({ final Validator userValidator = new Validator({
@ -170,12 +206,12 @@ register(Map userData) {
This library includes some `Matcher`s for common validations, This library includes some `Matcher`s for common validations,
including: including:
* `isAlphaDash`: 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 a `String` matches the Regular Expression ```/^[A-Za-z0-9]$/``` * `isAlphaNum`: Asserts that a `String` is alphanumeric.
* `isBool`: Asserts that a value either equals `true` or `false`. * `isBool`: Asserts that a value either equals `true` or `false`.
* `isEmail`: Asserts a `String` complies to the RFC 5322 e-mail standard. * `isEmail`: Asserts that a `String` complies to the RFC 5322 e-mail standard.
* `isInt`: Asserts a value is an `int`. * `isInt`: Asserts that a value is an `int`.
* `isNum`: Asserts a value is a `num`. * `isNum`: Asserts that a value is a `num`.
* `isString`: Asserts that a value is a `String`. * `isString`: Asserts that a value is a `String`.
The remaining functionality is The remaining functionality is
@ -218,10 +254,12 @@ main() {
# Use with Angel # 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. * `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. * `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 ```dart
import 'package:angel_framework/angel_framework.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'; import 'angel_validate.dart';
export '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 /// Validates the data in `req.body`, and sets the body to
/// filtered data before continuing the response. /// filtered data before continuing the response.
RequestMiddleware validate(Validator validator, RequestMiddleware validate(Validator validator,

View file

@ -1,13 +1,31 @@
import 'package:matcher/matcher.dart'; 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`. /// 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`. /// 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`. /// 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`. /// Asserts that a value is a `String`.
final Matcher isString = predicate((value) => value is String, 'a String '); final Matcher isString = predicate((value) => value is String, 'a String ');

View file

@ -1,6 +1,7 @@
import 'package:matcher/matcher.dart'; import 'package:matcher/matcher.dart';
final RegExp _asterisk = new RegExp(r'\*$'); final RegExp _asterisk = new RegExp(r'\*$');
final RegExp _forbidden = new RegExp(r'\!$');
final RegExp _optional = new RegExp(r'\?$'); final RegExp _optional = new RegExp(r'\?$');
/// Returns a value based the result of a computation. /// Returns a value based the result of a computation.
@ -9,11 +10,37 @@ typedef DefaultValueFunction();
/// Determines if a value is valid. /// Determines if a value is valid.
typedef bool Filter(value); 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. /// Enforces the validity of input data, according to [Matcher]s.
class Validator extends Matcher { 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. /// Values that will be filled for fields if they are not present.
final Map<String, dynamic> defaultValues = {}; 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. /// Conditions that must be met for input data to be considered valid.
final Map<String, List<Matcher>> rules = {}; final Map<String, List<Matcher>> rules = {};
@ -22,10 +49,16 @@ class Validator extends Matcher {
void _importSchema(Map<String, dynamic> schema) { void _importSchema(Map<String, dynamic> schema) {
for (var key in schema.keys) { for (var key in schema.keys) {
var fieldName = key.replaceAll(_asterisk, ''); var fieldName = key
var isRequired = _asterisk.hasMatch(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); requiredFields.add(fieldName);
} }
@ -47,8 +80,10 @@ class Validator extends Matcher {
Validator.empty(); Validator.empty();
Validator(Map<String, dynamic> schema, 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.defaultValues.addAll(defaultValues ?? {});
this.customErrorMessages.addAll(customErrorMessages ?? {});
_importSchema(schema); _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) { for (String field in requiredFields) {
if (!input.containsKey(field)) { if (!input.containsKey(field)) {
if (!customErrorMessages.containsKey(field))
errors.add("'$field' is required."); errors.add("'$field' is required.");
else
errors.add(customErrorMessages[field]);
} }
} }
@ -88,6 +135,7 @@ class Validator extends Matcher {
} }
} else { } else {
if (!matcher.matches(value, {})) { if (!matcher.matches(value, {})) {
if (!customErrorMessages.containsKey(key))
errors.add(matcher.describe(description).toString().trim()); errors.add(matcher.describe(description).toString().trim());
valid = false; valid = false;
} }
@ -100,6 +148,8 @@ class Validator extends Matcher {
if (valid) { if (valid) {
data[key] = value; 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; 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. /// Validates input data, and throws an error if it is invalid.
/// ///
/// Otherwise, the filtered data is returned. /// Otherwise, the filtered data is returned.
@ -127,41 +181,47 @@ class Validator extends Matcher {
/// Creates a copy with additional validation rules. /// Creates a copy with additional validation rules.
Validator extend(Map<String, dynamic> schema, 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 = {}; Map<String, dynamic> _schema = {};
var child = new Validator.empty() var child = new Validator.empty()
..defaultValues.addAll(this.defaultValues) ..defaultValues.addAll(this.defaultValues)
..defaultValues.addAll(defaultValues ?? {}) ..defaultValues.addAll(defaultValues ?? {})
..customErrorMessages.addAll(this.customErrorMessages)
..customErrorMessages.addAll(customErrorMessages ?? {})
..requiredFields.addAll(requiredFields) ..requiredFields.addAll(requiredFields)
..rules.addAll(rules); ..rules.addAll(rules);
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 (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);
}
if (overwrite) { if (overwrite) {
for (var key in schema.keys) { if (child.rules.containsKey(fieldName)) child.rules.remove(fieldName);
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);
if (child.rules.containsKey(key)) child.rules.remove(key);
_schema[fieldName] = schema[key]; _schema[fieldName] = schema[key];
} }
} 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];
}
}
return child.._importSchema(_schema); return child.._importSchema(_schema);
} }

View file

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

View file

@ -1,6 +1,12 @@
import 'package:angel_validate/angel_validate.dart'; import 'package:angel_validate/angel_validate.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
final Validator emailSchema = new Validator({
'to': [isNum, isPositive]
}, customErrorMessages: {
'to': 'Hello, world!'
});
final Validator todoSchema = new Validator({ final Validator todoSchema = new Validator({
'id': [isInt, isPositive], 'id': [isInt, isPositive],
'text*': isString, 'text*': isString,
@ -10,6 +16,14 @@ final Validator todoSchema = new Validator({
}); });
main() { 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', () { test('todo', () {
expect(() { expect(() {
todoSchema todoSchema