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)
|
||||
* [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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue