0.0.1
This commit is contained in:
parent
9dd2925a86
commit
bc2048669e
6 changed files with 188 additions and 42 deletions
56
README.md
56
README.md
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 ');
|
||||||
|
|
|
@ -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)) {
|
||||||
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 {
|
} else {
|
||||||
if (!matcher.matches(value, {})) {
|
if (!matcher.matches(value, {})) {
|
||||||
errors.add(matcher.describe(description).toString().trim());
|
if (!customErrorMessages.containsKey(key))
|
||||||
|
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,40 +181,46 @@ 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);
|
||||||
|
|
||||||
if (overwrite) {
|
for (var key in schema.keys) {
|
||||||
for (var key in schema.keys) {
|
var fieldName = key
|
||||||
var fieldName = key.replaceAll(_asterisk, '').replaceAll(_optional, '');
|
.replaceAll(_asterisk, '')
|
||||||
var isOptional = _optional.hasMatch(key);
|
.replaceAll(_forbidden, '')
|
||||||
var isRequired = _asterisk.hasMatch(key);
|
.replaceAll(_optional, '');
|
||||||
|
var isForbidden = _forbidden.hasMatch(key);
|
||||||
|
var isOptional = _optional.hasMatch(key);
|
||||||
|
var isRequired = _asterisk.hasMatch(key);
|
||||||
|
|
||||||
if (isOptional)
|
if (isForbidden) {
|
||||||
child.requiredFields.remove(fieldName);
|
child
|
||||||
else if (isRequired) child.requiredFields.add(fieldName);
|
..requiredFields.remove(fieldName)
|
||||||
|
..forbiddenFields.add(fieldName);
|
||||||
if (child.rules.containsKey(key)) child.rules.remove(key);
|
} else if (isOptional) {
|
||||||
|
child
|
||||||
_schema[fieldName] = schema[key];
|
..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)
|
if (overwrite) {
|
||||||
child.requiredFields.remove(fieldName);
|
if (child.rules.containsKey(fieldName)) child.rules.remove(fieldName);
|
||||||
else if (isRequired) child.requiredFields.add(fieldName);
|
|
||||||
|
|
||||||
_schema[fieldName] = schema[key];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_schema[fieldName] = schema[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
return child.._importSchema(_schema);
|
return child.._importSchema(_schema);
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue