It begins.

This commit is contained in:
thosakwe 2016-12-25 20:09:24 -05:00
parent b5a5d54157
commit f5666e8664
8 changed files with 311 additions and 16 deletions

2
.gitignore vendored
View file

@ -25,3 +25,5 @@ doc/api/
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
log.txt

View file

@ -2,6 +2,8 @@
[![version 0.0.0](https://img.shields.io/badge/pub-v0.0.0-red.svg)](https://pub.dartlang.org/packages/angel_validate)
[![build status](https://travis-ci.org/angel-dart/validate.svg)](https://travis-ci.org/angel-dart/validate)
(Not yet production ready, still missing several tests and a few matchers)
Validation library based on the `matcher` library, with Angel support.
Why re-invent the wheel, when you can use the same validators you already
use for tests?
@ -9,6 +11,19 @@ use for tests?
This library runs both on the server, and on the client. Thus, you can use
the same validation rules for forms on the server, and on the frontend.
For convenience's sake, this library also exports `matcher`.
* [Examples](#examples)
* [Creating a Validator](#creating-a-validator)
* [Validating Data](#validating-data)
* [Required Fields](#required-fields)
* [Default Values](#default-values)
* [Custom Validator Functions](#custom-validator-functions)
* [Extending Validators](#extending-validators)
* [Bundled Matchers](#bundled-matchers)
* [Nested Validators](#nested-validators)
* [Use with Angel](#use-with-angel)
# Examples
## Creating a Validator
@ -160,9 +175,7 @@ including:
* `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`.
* `isNegative`: Asserts a `num` is less than `0`.
* `isNum`: Asserts a value is a `num`.
* `isPositive`: Asserts a `num` is greater than `0`.
* `isString`: Asserts that a value is a `String`.
The remaining functionality is
@ -170,11 +183,8 @@ The remaining functionality is
# Nested Validators
Very often, the data we validate contains other data within. You can pass
a `Validator` instance to the constructor, and it will be wrapped within
a `Matcher` instance.
The class also exposes a `toMatcher()` method that creates a Matcher that
validates data using the instance.
a `Validator` instance to the constructor, because it extends the
`Matcher` class.
```dart
main() {
@ -198,7 +208,7 @@ main() {
'bio*': bio,
'books*': [
isList,
everyElement(book.toMatcher())
everyElement(book)
]
}, defaultValues: {
'books': []

View file

@ -1,4 +1,6 @@
/// Cross-platform validation library based on `matcher`.
library angel_validate;
export 'package:matcher/matcher.dart';
export 'src/matchers.dart';
export 'src/validator.dart';

13
lib/src/matchers.dart Normal file
View file

@ -0,0 +1,13 @@
import 'package:matcher/matcher.dart';
/// Asserts that a value either equals `true` or `false`.
final Matcher isBool = predicate((value) => value is String, 'a bool ');
/// Asserts that a value is an `int`.
final Matcher isInt = predicate((value) => value is String, 'an integer ');
/// Asserts that a value is a `num`.
final Matcher isNumber = predicate((value) => value is String, 'a number ');
/// Asserts that a value is a `String`.
final Matcher isString = predicate((value) => value is String, 'a String ');

View file

@ -1,24 +1,224 @@
import 'package:matcher/matcher.dart';
final RegExp _asterisk = new RegExp(r'\*$');
final RegExp _optional = new RegExp(r'\?$');
/// Returns a value based the result of a computation.
typedef DefaultValueFunction();
/// Determines if a value is valid.
typedef bool Filter(value);
/// Enforces the validity of input data, according to [Matcher]s.
class Validator {
class Validator extends Matcher {
/// Values that will be filled for fields if they are not present.
final Map<String, dynamic> defaultValues = {};
/// Conditions that must be met for input data to be considered valid.
final Map<String, List<Matcher>> rules = {};
/// Fields that must be present for data to be considered valid.
final List<String> requiredFields = [];
void _importSchema(Map<String, dynamic> schema) {
for (var key in schema.keys) {
var fieldName = key.replaceAll(_asterisk, '');
var isRequired = _asterisk.hasMatch(key);
if (isRequired) {
requiredFields.add(fieldName);
}
Iterable iterable = schema[key] is Iterable ? schema[key] : [schema[key]];
for (var rule in iterable) {
if (rule is Matcher) {
addRule(fieldName, rule);
} else if (rule is Filter) {
addRule(fieldName, predicate(rule));
} else {
throw new ArgumentError(
'Cannot use a(n) ${rule.runtimeType} as a validation rule.');
}
}
}
}
Validator.empty();
Validator(Map<String, dynamic> schema,
{Map<String, dynamic> defaultValues: const {}}) {
this.defaultValues.addAll(defaultValues ?? {});
_importSchema(schema);
}
/// Validates, and filters input data.
ValidationResult check(Map inputData) {}
ValidationResult check(Map inputData) {
List<String> errors = [];
var input = new Map.from(inputData);
Map<String, dynamic> data = {};
for (String key in defaultValues.keys) {
if (!input.containsKey(key)) {
var value = defaultValues[key];
input[key] = value is DefaultValueFunction ? value() : value;
}
}
for (String field in requiredFields) {
if (!input.containsKey(field)) {
errors.add("'$field' is required.");
}
}
for (var key in input.keys) {
if (key is String && rules.containsKey(key)) {
var valid = true;
var value = input[key];
var description = new StringDescription("Field '$key': expected ");
for (Matcher matcher in rules[key]) {
try {
if (matcher is Validator) {
var result = matcher.check(value);
if (result.errors.isNotEmpty) {
errors.addAll(result.errors);
valid = false;
}
} else {
if (!matcher.matches(value, {})) {
errors.add(matcher.describe(description).toString().trim());
valid = false;
}
}
} catch (e) {
errors.add(e.toString());
valid = false;
}
}
if (valid) {
data[key] = value;
}
}
}
if (errors.isNotEmpty) {
return new ValidationResult().._errors.addAll(errors);
}
return new ValidationResult().._data = data;
}
/// Validates input data, and throws an error if it is invalid.
///
///
/// Otherwise, the filtered data is returned.
Map enforce(Map inputData, {String errorMessage: 'Invalid data.'}) {}
Map<String, dynamic> enforce(Map inputData,
{String errorMessage: 'Invalid data.'}) {
var result = check(inputData);
if (result._errors.isNotEmpty) {
throw new ValidationException(errorMessage, errors: result._errors);
}
return result.data;
}
/// Creates a copy with additional validation rules.
Validator extend(Map<String, dynamic> schema,
{Map<String, dynamic> defaultValues: const {}, bool overwrite: false}) {
Map<String, dynamic> _schema = {};
var child = new Validator.empty()
..defaultValues.addAll(this.defaultValues)
..defaultValues.addAll(defaultValues ?? {})
..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);
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];
}
return child;
} 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);
}
/// Adds a [rule].
void addRule(String key, Matcher rule) {
if (!rules.containsKey(key)) {
rules[key] = [rule];
return;
}
rules[key].add(rule);
}
/// Adds all given [rules].
void addRules(String key, Iterable<Matcher> rules) {
rules.forEach((rule) => addRule(key, rule));
}
/// Removes a [rule].
void removeRule(String key, Matcher rule) {
if (rules.containsKey(key)) {
rules[key].remove(rule);
}
}
/// Removes all given [rules].
void removeRules(String key, Iterable<Matcher> rules) {
rules.forEach((rule) => removeRule(key, rule));
}
@override
Description describe(Description description) =>
description.add(' passes the provided validation schema: $rules');
@override
bool matches(item, Map matchState) {
enforce(item);
return true;
}
@override
String toString() => 'Validation schema: $rules';
}
/// The result of attempting to validate input data.
class ValidationResult {
Map _data;
Map<String, dynamic> _data;
final List<String> _errors = [];
/// The successfully validated data, filtered from the original input.
Map get data => _data;
Map<String, dynamic> get data => _data;
/// A list of errors that resulted in the given data being marked invalid.
///
///
/// This is empty if validation was successful.
List<String> get errors => new List<String>.unmodifiable(_errors);
}
@ -36,7 +236,7 @@ class ValidationException {
}
@override
String get toString {
String toString() {
if (errors.isEmpty) {
return message;
}

View file

@ -9,4 +9,6 @@ dependencies:
angel_framework: ^1.0.0-dev
matcher: ^0.12.0
dev_dependencies:
angel_diagnostics: ^1.0.0-dev
angel_test: ^1.0.0-dev
test: ^0.12.18

19
test/basic_test.dart Normal file
View file

@ -0,0 +1,19 @@
import 'package:angel_validate/angel_validate.dart';
import 'package:test/test.dart';
final Validator todoSchema = new Validator({
'id': [isInt, isPositive],
'text*': isString,
'completed*': isBool
}, defaultValues: {
'completed': false
});
main() {
test('todo', () {
expect(() {
todoSchema
.enforce({'id': 'fool', 'text': 'Hello, world!', 'completed': 4});
}, throwsA(new isInstanceOf<ValidationException>()));
});
}

47
test/server_test.dart Normal file
View file

@ -0,0 +1,47 @@
import 'dart:io';
import 'package:angel_diagnostics/angel_diagnostics.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_test/angel_test.dart';
import 'package:angel_validate/server.dart';
import 'package:test/test.dart';
final Validator echoSchema = new Validator({'message*': isString});
main() {
Angel app;
TestClient client;
setUp(() async {
app = new Angel();
app.chain(validate(echoSchema)).post('/echo',
(RequestContext req, res) async {
res.write('Hello, ${req.body['message']}!');
});
client = await connectTo(new DiagnosticsServer(app, new File('log.txt')));
});
tearDown(() async {
await client.close();
app = null;
client = null;
});
group('echo', () {
test('validate', () async {
var response = await client.post('/echo',
body: {'message': 'world'}, headers: {HttpHeaders.ACCEPT: '*/*'});
print('Response: ${response.body}');
expect(response, hasStatus(HttpStatus.OK));
expect(response.body, equals('Hello, world!'));
});
test('enforce', () async {
var response = await client.post('/echo',
body: {'foo': 'bar'}, headers: {HttpHeaders.ACCEPT: '*/*'});
print('Response: ${response.body}');
expect(response, hasStatus(HttpStatus.BAD_REQUEST));
});
});
}