beta
This commit is contained in:
parent
0ca81479ff
commit
eb38637a6b
12 changed files with 312 additions and 62 deletions
|
@ -1,5 +1,7 @@
|
||||||
# 1.0.5
|
# 1.0.5-beta
|
||||||
* Use `wrapMatcher` on explicit values instead of throwing.
|
* Use `wrapMatcher` on explicit values instead of throwing.
|
||||||
|
* Add async matchers.
|
||||||
|
* Add context-aware matchers.
|
||||||
|
|
||||||
# 1.0.4
|
# 1.0.4
|
||||||
* `isNonEmptyString` trims strings.
|
* `isNonEmptyString` trims strings.
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
library angel_validate;
|
library angel_validate;
|
||||||
|
|
||||||
export 'package:matcher/matcher.dart';
|
export 'package:matcher/matcher.dart';
|
||||||
|
export 'src/context_aware.dart';
|
||||||
export 'src/matchers.dart';
|
export 'src/matchers.dart';
|
||||||
export 'src/validator.dart';
|
export 'src/validator.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
/// Support for using `angel_validate` with the Angel Framework.
|
/// Support for using `angel_validate` with the Angel Framework.
|
||||||
library angel_validate.server;
|
library angel_validate.server;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'src/async.dart';
|
import 'src/async.dart';
|
||||||
import 'angel_validate.dart';
|
import 'angel_validate.dart';
|
||||||
|
@ -50,7 +52,8 @@ RequestMiddleware filterQuery(Iterable<String> only) {
|
||||||
RequestMiddleware validate(Validator validator,
|
RequestMiddleware validate(Validator validator,
|
||||||
{String errorMessage: 'Invalid data.'}) {
|
{String errorMessage: 'Invalid data.'}) {
|
||||||
return (RequestContext req, res) async {
|
return (RequestContext req, res) async {
|
||||||
var result = validator.check(await req.lazyBody());
|
var result =
|
||||||
|
await asyncApplyValidator(validator, await req.lazyBody(), req.app);
|
||||||
|
|
||||||
if (result.errors.isNotEmpty) {
|
if (result.errors.isNotEmpty) {
|
||||||
throw new AngelHttpException.badRequest(
|
throw new AngelHttpException.badRequest(
|
||||||
|
@ -70,7 +73,7 @@ RequestMiddleware validate(Validator validator,
|
||||||
RequestMiddleware validateQuery(Validator validator,
|
RequestMiddleware validateQuery(Validator validator,
|
||||||
{String errorMessage: 'Invalid data.'}) {
|
{String errorMessage: 'Invalid data.'}) {
|
||||||
return (RequestContext req, res) async {
|
return (RequestContext req, res) async {
|
||||||
var result = validator.check(req.query);
|
var result = await asyncApplyValidator(validator, req.query, req.app);
|
||||||
|
|
||||||
if (result.errors.isNotEmpty) {
|
if (result.errors.isNotEmpty) {
|
||||||
throw new AngelHttpException.badRequest(
|
throw new AngelHttpException.badRequest(
|
||||||
|
@ -89,8 +92,9 @@ RequestMiddleware validateQuery(Validator validator,
|
||||||
/// filtered data before continuing the service event.
|
/// filtered data before continuing the service event.
|
||||||
HookedServiceEventListener validateEvent(Validator validator,
|
HookedServiceEventListener validateEvent(Validator validator,
|
||||||
{String errorMessage: 'Invalid data.'}) {
|
{String errorMessage: 'Invalid data.'}) {
|
||||||
return (HookedServiceEvent e) {
|
return (HookedServiceEvent e) async {
|
||||||
var result = validator.check(e.data as Map);
|
var result = await asyncApplyValidator(
|
||||||
|
validator, e.data as Map, (e.request?.app ?? e.service.app) as Angel);
|
||||||
|
|
||||||
if (result.errors.isNotEmpty) {
|
if (result.errors.isNotEmpty) {
|
||||||
throw new AngelHttpException.badRequest(
|
throw new AngelHttpException.badRequest(
|
||||||
|
@ -102,3 +106,36 @@ HookedServiceEventListener validateEvent(Validator validator,
|
||||||
..addAll(result.data);
|
..addAll(result.data);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Asynchronously apply a [validator], running any [AngelMatcher]s.
|
||||||
|
Future<ValidationResult> asyncApplyValidator(
|
||||||
|
Validator validator, Map data, Angel app) async {
|
||||||
|
var result = validator.check(data);
|
||||||
|
if (result.errors.isNotEmpty) return result;
|
||||||
|
|
||||||
|
var errantKeys = <String>[], errors = <String>[];
|
||||||
|
|
||||||
|
for (var key in result.data.keys) {
|
||||||
|
var value = result.data[key];
|
||||||
|
var description = new StringDescription("'$key': expected ");
|
||||||
|
|
||||||
|
for (var rule in validator.rules[key]) {
|
||||||
|
if (rule is AngelMatcher) {
|
||||||
|
var r = await rule.matchesWithAngel(value, key, result.data, {}, app);
|
||||||
|
|
||||||
|
if (!r) {
|
||||||
|
errors.add(rule.describe(description).toString().trim());
|
||||||
|
errantKeys.add(key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var m = new Map<String, dynamic>.from(result.data);
|
||||||
|
for (var key in errantKeys) {
|
||||||
|
m.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.withData(m).withErrors(errors);
|
||||||
|
}
|
||||||
|
|
|
@ -2,16 +2,14 @@ import 'dart:async';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_http_exception/angel_http_exception.dart';
|
import 'package:angel_http_exception/angel_http_exception.dart';
|
||||||
import 'package:matcher/matcher.dart';
|
import 'package:matcher/matcher.dart';
|
||||||
|
import 'context_aware.dart';
|
||||||
|
|
||||||
/// Returns an [AngelMatcher] that uses an arbitrary function that returns
|
/// Returns an [AngelMatcher] that uses an arbitrary function that returns
|
||||||
/// true or false for the actual value.
|
/// true or false for the actual value.
|
||||||
///
|
///
|
||||||
/// Analogous to the synchronous [predicate] matcher.
|
/// Analogous to the synchronous [predicate] matcher.
|
||||||
///
|
AngelMatcher predicateWithAngel(
|
||||||
/// For example:
|
FutureOr<bool> Function(String, Object, Angel) f,
|
||||||
///
|
|
||||||
/// expect(v, predicate((x) => ((x % 2) == 0), "is even"))
|
|
||||||
AngelMatcher predicateWithAngel(FutureOr<bool> Function(Object, Angel) f,
|
|
||||||
[String description = 'satisfies function']) =>
|
[String description = 'satisfies function']) =>
|
||||||
new _PredicateWithAngel(f, description);
|
new _PredicateWithAngel(f, description);
|
||||||
|
|
||||||
|
@ -19,26 +17,38 @@ AngelMatcher predicateWithAngel(FutureOr<bool> Function(Object, Angel) f,
|
||||||
/// to the input.
|
/// to the input.
|
||||||
///
|
///
|
||||||
/// Use this to match values against configuration, injections, etc.
|
/// Use this to match values against configuration, injections, etc.
|
||||||
AngelMatcher matchWithAngel(FutureOr<Matcher> Function(Object, Angel) f,
|
AngelMatcher matchWithAngel(FutureOr<Matcher> Function(Object, Map, Angel) f,
|
||||||
[String description = 'satisfies asynchronously created matcher']) =>
|
[String description = 'satisfies asynchronously created matcher']) =>
|
||||||
new _MatchWithAngel(f, description);
|
new _MatchWithAngel(f, description);
|
||||||
|
|
||||||
/// Calls [matchWithAngel] without the initial parameter.
|
/// Calls [matchWithAngel] without the initial parameter.
|
||||||
|
AngelMatcher matchWithAngelBinary(
|
||||||
|
FutureOr<Matcher> Function(Map context, Angel) f,
|
||||||
|
[String description = 'satisfies asynchronously created matcher']) =>
|
||||||
|
matchWithAngel((_, context, app) => f(context, app));
|
||||||
|
|
||||||
|
/// Calls [matchWithAngel] without the initial two parameters.
|
||||||
AngelMatcher matchWithAngelUnary(FutureOr<Matcher> Function(Angel) f,
|
AngelMatcher matchWithAngelUnary(FutureOr<Matcher> Function(Angel) f,
|
||||||
[String description = 'satisfies asynchronously created matcher']) =>
|
[String description = 'satisfies asynchronously created matcher']) =>
|
||||||
matchWithAngel((_, app) => f(app));
|
matchWithAngelBinary((_, app) => f(app));
|
||||||
|
|
||||||
|
/// Calls [matchWithAngel] without any parameters.
|
||||||
|
AngelMatcher matchWithAngelNullary(FutureOr<Matcher> Function() f,
|
||||||
|
[String description = 'satisfies asynchronously created matcher']) =>
|
||||||
|
matchWithAngelUnary((_) => f());
|
||||||
|
|
||||||
/// Returns an [AngelMatcher] that represents [x].
|
/// Returns an [AngelMatcher] that represents [x].
|
||||||
///
|
///
|
||||||
/// If [x] is an [AngelMatcher], then it is returned, unmodified.
|
/// If [x] is an [AngelMatcher], then it is returned, unmodified.
|
||||||
AngelMatcher wrapAngelMatcher(x) {
|
AngelMatcher wrapAngelMatcher(x) {
|
||||||
if (x is AngelMatcher) return x;
|
if (x is AngelMatcher) return x;
|
||||||
return matchWithAngel((_, app) => wrapMatcher(x));
|
if (x is ContextAwareMatcher) return new _WrappedAngelMatcher(x);
|
||||||
|
return wrapAngelMatcher(wrapContextAwareMatcher(x));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an [AngelMatcher] that asynchronously resolves a [feature], builds a [matcher], and executes it.
|
/// Returns an [AngelMatcher] that asynchronously resolves a [feature], builds a [matcher], and executes it.
|
||||||
AngelMatcher matchAsync(
|
AngelMatcher matchAsync(FutureOr<Matcher> Function(String, Object) matcher,
|
||||||
FutureOr<Matcher> Function(Object) matcher, FutureOr Function() feature,
|
FutureOr Function() feature,
|
||||||
[String description = 'satisfies asynchronously created matcher']) {
|
[String description = 'satisfies asynchronously created matcher']) {
|
||||||
return new _MatchAsync(matcher, feature, description);
|
return new _MatchAsync(matcher, feature, description);
|
||||||
}
|
}
|
||||||
|
@ -48,7 +58,7 @@ AngelMatcher matchAsync(
|
||||||
AngelMatcher idExistsInService(String servicePath,
|
AngelMatcher idExistsInService(String servicePath,
|
||||||
{String idField: 'id', String description}) {
|
{String idField: 'id', String description}) {
|
||||||
return predicateWithAngel(
|
return predicateWithAngel(
|
||||||
(item, app) async {
|
(key, item, app) async {
|
||||||
try {
|
try {
|
||||||
var result = await app.service(servicePath)?.read(item);
|
var result = await app.service(servicePath)?.read(item);
|
||||||
return result != null;
|
return result != null;
|
||||||
|
@ -65,17 +75,34 @@ AngelMatcher idExistsInService(String servicePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An asynchronous [Matcher] that runs in the context of an [Angel] app.
|
/// An asynchronous [Matcher] that runs in the context of an [Angel] app.
|
||||||
abstract class AngelMatcher extends Matcher {
|
abstract class AngelMatcher extends ContextAwareMatcher {
|
||||||
Future<bool> matchesAsync(item, Map matchState, Angel app);
|
Future<bool> matchesWithAngel(
|
||||||
|
item, String key, Map context, Map matchState, Angel app);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool matches(item, Map matchState) {
|
bool matchesWithContext(item, String key, Map context, Map matchState) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _WrappedAngelMatcher extends AngelMatcher {
|
||||||
|
final ContextAwareMatcher matcher;
|
||||||
|
|
||||||
|
_WrappedAngelMatcher(this.matcher);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Description describe(Description description) =>
|
||||||
|
matcher.describe(description);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> matchesWithAngel(
|
||||||
|
item, String key, Map context, Map matchState, Angel app) async {
|
||||||
|
return matcher.matchesWithContext(item, key, context, matchState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _MatchWithAngel extends AngelMatcher {
|
class _MatchWithAngel extends AngelMatcher {
|
||||||
final FutureOr<Matcher> Function(Object, Angel) f;
|
final FutureOr<Matcher> Function(Object, Map, Angel) f;
|
||||||
final String description;
|
final String description;
|
||||||
|
|
||||||
_MatchWithAngel(this.f, this.description);
|
_MatchWithAngel(this.f, this.description);
|
||||||
|
@ -86,15 +113,16 @@ class _MatchWithAngel extends AngelMatcher {
|
||||||
: description.add(this.description);
|
: description.add(this.description);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> matchesAsync(item, Map matchState, Angel app) {
|
Future<bool> matchesWithAngel(
|
||||||
return new Future.sync(() => f(item, app)).then((result) {
|
item, String key, Map context, Map matchState, Angel app) {
|
||||||
|
return new Future.sync(() => f(item, context, app)).then((result) {
|
||||||
return result.matches(item, matchState);
|
return result.matches(item, matchState);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PredicateWithAngel extends AngelMatcher {
|
class _PredicateWithAngel extends AngelMatcher {
|
||||||
final FutureOr<bool> Function(Object, Angel) predicate;
|
final FutureOr<bool> Function(String, Object, Angel) predicate;
|
||||||
final String description;
|
final String description;
|
||||||
|
|
||||||
_PredicateWithAngel(this.predicate, this.description);
|
_PredicateWithAngel(this.predicate, this.description);
|
||||||
|
@ -105,13 +133,14 @@ class _PredicateWithAngel extends AngelMatcher {
|
||||||
: description.add(this.description);
|
: description.add(this.description);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> matchesAsync(item, Map matchState, Angel app) {
|
Future<bool> matchesWithAngel(
|
||||||
return new Future<bool>.sync(() => predicate(item, app));
|
item, String key, Map context, Map matchState, Angel app) {
|
||||||
|
return new Future<bool>.sync(() => predicate(key, item, app));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MatchAsync extends AngelMatcher {
|
class _MatchAsync extends AngelMatcher {
|
||||||
final FutureOr<Matcher> Function(Object) matcher;
|
final FutureOr<Matcher> Function(String, Object) matcher;
|
||||||
final FutureOr Function() feature;
|
final FutureOr Function() feature;
|
||||||
final String description;
|
final String description;
|
||||||
|
|
||||||
|
@ -123,9 +152,11 @@ class _MatchAsync extends AngelMatcher {
|
||||||
: description.add(this.description);
|
: description.add(this.description);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> matchesAsync(item, Map matchState, Angel app) async {
|
Future<bool> matchesWithAngel(
|
||||||
|
item, String key, Map context, Map matchState, Angel app) async {
|
||||||
var f = await feature();
|
var f = await feature();
|
||||||
var m = await matcher(f);
|
var m = await matcher(key, f);
|
||||||
return m.matches(item, matchState);
|
var c = wrapAngelMatcher(m);
|
||||||
|
return await c.matchesWithAngel(item, key, context, matchState, app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
53
lib/src/context_aware.dart
Normal file
53
lib/src/context_aware.dart
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import 'package:matcher/matcher.dart';
|
||||||
|
|
||||||
|
/// Returns a [ContextAwareMatcher] for the given predicate.
|
||||||
|
ContextAwareMatcher predicateWithContext(
|
||||||
|
bool Function(Object, String, Map, Map) f,
|
||||||
|
[String description = 'satisfies function']) {
|
||||||
|
return new _PredicateWithContext(f, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps [x] in a [ContextAwareMatcher].
|
||||||
|
ContextAwareMatcher wrapContextAwareMatcher(x) {
|
||||||
|
if (x is ContextAwareMatcher)
|
||||||
|
return x;
|
||||||
|
else if (x is Matcher) return new _WrappedContextAwareMatcher(x);
|
||||||
|
return wrapContextAwareMatcher(wrapMatcher(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A special [Matcher] that is aware of the context in which it is being executed.
|
||||||
|
abstract class ContextAwareMatcher extends Matcher {
|
||||||
|
bool matchesWithContext(item, String key, Map context, Map matchState);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool matches(item, Map matchState) => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WrappedContextAwareMatcher extends ContextAwareMatcher {
|
||||||
|
final Matcher matcher;
|
||||||
|
|
||||||
|
_WrappedContextAwareMatcher(this.matcher);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Description describe(Description description) =>
|
||||||
|
matcher.describe(description);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool matchesWithContext(item, String key, Map context, Map matchState) =>
|
||||||
|
matcher.matches(item, matchState);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PredicateWithContext extends ContextAwareMatcher {
|
||||||
|
final bool Function(Object, String, Map, Map) f;
|
||||||
|
final String desc;
|
||||||
|
|
||||||
|
_PredicateWithContext(this.f, this.desc);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Description describe(Description description) =>
|
||||||
|
desc == null ? description : description.add(desc);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool matchesWithContext(item, String key, Map context, Map matchState) =>
|
||||||
|
f(item, key, context, matchState);
|
||||||
|
}
|
16
lib/src/context_validator.dart
Normal file
16
lib/src/context_validator.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:matcher/matcher.dart';
|
||||||
|
|
||||||
|
/// A [Matcher] directly invoked by `package:angel_serialize` to validate the context.
|
||||||
|
class ContextValidator extends Matcher {
|
||||||
|
final bool Function(String, Map) validate;
|
||||||
|
final Description Function(Description, String, Map) errorMessage;
|
||||||
|
|
||||||
|
ContextValidator(this.validate, this.errorMessage);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Description describe(Description description) => description;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool matches(item, Map matchState) => true;
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
import 'package:matcher/matcher.dart';
|
import 'package:matcher/matcher.dart';
|
||||||
|
import 'context_aware.dart';
|
||||||
|
import 'context_validator.dart';
|
||||||
|
|
||||||
final RegExp _alphaDash = new RegExp(r'^[A-Za-z0-9_-]+$');
|
final RegExp _alphaDash = new RegExp(r'^[A-Za-z0-9_-]+$');
|
||||||
final RegExp _alphaNum = new RegExp(r'^[A-Za-z0-9]+$');
|
final RegExp _alphaNum = new RegExp(r'^[A-Za-z0-9]+$');
|
||||||
|
@ -13,6 +15,7 @@ final Matcher isAlphaDash = predicate(
|
||||||
'alphanumeric (dashes and underscores are allowed)');
|
'alphanumeric (dashes and underscores are allowed)');
|
||||||
|
|
||||||
/// Asserts that a `String` is alphanumeric, but also lets it contain dashes or underscores.
|
/// Asserts that a `String` is alphanumeric, but also lets it contain dashes or underscores.
|
||||||
|
///
|
||||||
final Matcher isAlphaNum = predicate(
|
final Matcher isAlphaNum = predicate(
|
||||||
(value) => value is String && _alphaNum.hasMatch(value), 'alphanumeric');
|
(value) => value is String && _alphaNum.hasMatch(value), 'alphanumeric');
|
||||||
|
|
||||||
|
@ -21,7 +24,8 @@ final Matcher isBool = predicate((value) => value is bool, 'a bool');
|
||||||
|
|
||||||
/// Asserts that a `String` complies to the RFC 5322 e-mail standard.
|
/// Asserts that a `String` complies to the RFC 5322 e-mail standard.
|
||||||
final Matcher isEmail = predicate(
|
final Matcher isEmail = predicate(
|
||||||
(value) => value is String && _email.hasMatch(value), 'a valid e-mail');
|
(value) => value is String && _email.hasMatch(value),
|
||||||
|
'a valid e-mail address');
|
||||||
|
|
||||||
/// Asserts that a value is an `int`.
|
/// Asserts that a value is an `int`.
|
||||||
final Matcher isInt = predicate((value) => value is int, 'an integer');
|
final Matcher isInt = predicate((value) => value is int, 'an integer');
|
||||||
|
@ -30,12 +34,28 @@ final Matcher isInt = predicate((value) => value is int, 'an integer');
|
||||||
final Matcher isNum = predicate((value) => value is num, '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');
|
||||||
|
|
||||||
/// Asserts that a value is a non-empty `String`.
|
/// Asserts that a value is a non-empty `String`.
|
||||||
final Matcher isNonEmptyString = predicate(
|
final Matcher isNonEmptyString = predicate(
|
||||||
(value) => value is String && value.trim().isNotEmpty,
|
(value) => value is String && value.trim().isNotEmpty,
|
||||||
'a non-empty String');
|
'a non-empty string');
|
||||||
|
|
||||||
|
/// Asserts that a value, presumably from a checkbox, is positive.
|
||||||
|
final Matcher isChecked =
|
||||||
|
isIn(const ['yes', 'checked', 'on', '1', 1, 1.0, true, 'true']);
|
||||||
|
|
||||||
|
/// Ensures that a string is an ISO-8601 date string.
|
||||||
|
final Matcher isIso8601DateString = predicate(
|
||||||
|
(x) {
|
||||||
|
try {
|
||||||
|
return x is String && DateTime.parse(x) != null;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'a valid ISO-8601 date string.',
|
||||||
|
);
|
||||||
|
|
||||||
/// Asserts that a `String` is an `http://` or `https://` URL.
|
/// Asserts that a `String` is an `http://` or `https://` URL.
|
||||||
///
|
///
|
||||||
|
@ -60,3 +80,54 @@ Matcher minLength(int length) => predicate(
|
||||||
Matcher maxLength(int length) => predicate(
|
Matcher maxLength(int length) => predicate(
|
||||||
(value) => value is String && value.length >= length,
|
(value) => value is String && value.length >= length,
|
||||||
'a string no longer than $length character(s) long');
|
'a string no longer than $length character(s) long');
|
||||||
|
|
||||||
|
/// Asserts that for a key `x`, the context contains an identical item `x_confirmed`.
|
||||||
|
ContextAwareMatcher isConfirmed = predicateWithContext(
|
||||||
|
(item, key, context, matchState) {
|
||||||
|
return equals(item).matches(context['${key}_confirmed'], matchState);
|
||||||
|
},
|
||||||
|
'is confirmed',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Asserts that for a key `x`, the value of `x` is **not equal to** the value for [key].
|
||||||
|
ContextAwareMatcher differentFrom(String key) {
|
||||||
|
return predicateWithContext(
|
||||||
|
(item, key, context, matchState) {
|
||||||
|
return !equals(item).matches(context[key], matchState);
|
||||||
|
},
|
||||||
|
'is different from the value of "$key"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asserts that for a key `x`, the value of `x` is **equal to** the value for [key].
|
||||||
|
ContextAwareMatcher sameAs(String key) {
|
||||||
|
return predicateWithContext(
|
||||||
|
(item, key, context, matchState) {
|
||||||
|
return equals(item).matches(context[key], matchState);
|
||||||
|
},
|
||||||
|
'is equal to the value of "$key"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert that a key `x` is present, if *all* of the given [keys] are as well.
|
||||||
|
ContextValidator requiredIf(Iterable<String> keys) =>
|
||||||
|
_require((ctx) => keys.every(ctx.containsKey));
|
||||||
|
|
||||||
|
/// Assert that a key `x` is present, if *any* of the given [keys] are as well.
|
||||||
|
ContextValidator requiredAny(Iterable<String> keys) =>
|
||||||
|
_require((ctx) => keys.any(ctx.containsKey));
|
||||||
|
|
||||||
|
/// Assert that a key `x` is present, if *at least one* of the given [keys] is not.
|
||||||
|
ContextValidator requiredWithout(Iterable<String> keys) =>
|
||||||
|
_require((ctx) => !keys.every(ctx.containsKey));
|
||||||
|
|
||||||
|
/// Assert that a key `x` is present, if *none* of the given [keys] are.
|
||||||
|
ContextValidator requiredWithoutAll(Iterable<String> keys) =>
|
||||||
|
_require((ctx) => !keys.any(ctx.containsKey));
|
||||||
|
|
||||||
|
ContextValidator _require(bool Function(Map) f) {
|
||||||
|
return new ContextValidator(
|
||||||
|
(key, context) => f(context) && context.containsKey(key),
|
||||||
|
(desc, key, _) => new StringDescription('Missing required field "$key".'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:angel_http_exception/angel_http_exception.dart';
|
import 'package:angel_http_exception/angel_http_exception.dart';
|
||||||
import 'package:matcher/matcher.dart';
|
import 'package:matcher/matcher.dart';
|
||||||
|
import 'context_aware.dart';
|
||||||
|
import 'context_validator.dart';
|
||||||
|
|
||||||
final RegExp _asterisk = new RegExp(r'\*$');
|
final RegExp _asterisk = new RegExp(r'\*$');
|
||||||
final RegExp _forbidden = new RegExp(r'!$');
|
final RegExp _forbidden = new RegExp(r'!$');
|
||||||
|
@ -113,6 +115,9 @@ class Validator extends Matcher {
|
||||||
_importSchema(schema);
|
_importSchema(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool _hasContextValidators(Iterable it) =>
|
||||||
|
it.any((x) => x is ContextValidator);
|
||||||
|
|
||||||
/// Validates, and filters input data.
|
/// Validates, and filters input data.
|
||||||
ValidationResult check(Map inputData) {
|
ValidationResult check(Map inputData) {
|
||||||
List<String> errors = [];
|
List<String> errors = [];
|
||||||
|
@ -136,42 +141,68 @@ class Validator extends Matcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String field in requiredFields) {
|
for (String field in requiredFields) {
|
||||||
if (!input.containsKey(field)) {
|
if (!_hasContextValidators(rules[field] ?? [])) {
|
||||||
if (!customErrorMessages.containsKey(field))
|
if (!input.containsKey(field)) {
|
||||||
errors.add("'$field' is required.");
|
if (!customErrorMessages.containsKey(field))
|
||||||
else
|
errors.add("'$field' is required.");
|
||||||
errors.add(customError(field, 'none'));
|
else
|
||||||
|
errors.add(customError(field, 'none'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run context validators.
|
||||||
|
|
||||||
for (var key in input.keys) {
|
for (var key in input.keys) {
|
||||||
if (key is String && rules.containsKey(key)) {
|
if (key is String && rules.containsKey(key)) {
|
||||||
var valid = true;
|
var valid = true;
|
||||||
var value = input[key];
|
var value = input[key];
|
||||||
var description = new StringDescription("'$key': expected ");
|
var description = new StringDescription("'$key': expected ");
|
||||||
|
|
||||||
for (Matcher matcher in rules[key]) {
|
for (var matcher in rules[key]) {
|
||||||
try {
|
if (matcher is ContextValidator) {
|
||||||
if (matcher is Validator) {
|
if (!matcher.validate(key, input)) {
|
||||||
var result = matcher.check(value as Map);
|
errors.add(matcher
|
||||||
|
.errorMessage(description, key, input)
|
||||||
if (result.errors.isNotEmpty) {
|
.toString()
|
||||||
errors.addAll(result.errors);
|
.trim());
|
||||||
valid = false;
|
valid = false;
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
if (!matcher.matches(value, {})) {
|
|
||||||
if (!customErrorMessages.containsKey(key))
|
if (valid) {
|
||||||
errors.add(matcher.describe(description).toString().trim());
|
for (Matcher matcher in rules[key]) {
|
||||||
valid = false;
|
try {
|
||||||
break;
|
if (matcher is Validator) {
|
||||||
}
|
var result = matcher.check(value as Map);
|
||||||
|
|
||||||
|
if (result.errors.isNotEmpty) {
|
||||||
|
errors.addAll(result.errors);
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bool result;
|
||||||
|
|
||||||
|
if (matcher is ContextAwareMatcher) {
|
||||||
|
result = matcher.matchesWithContext(value, key, input, {});
|
||||||
|
} else {
|
||||||
|
result = matcher.matches(value, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
if (!customErrorMessages.containsKey(key))
|
||||||
|
errors.add(matcher.describe(description).toString().trim());
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errors.add(e.toString());
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
errors.add(e.toString());
|
|
||||||
valid = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +218,7 @@ class Validator extends Matcher {
|
||||||
return new ValidationResult().._errors.addAll(errors);
|
return new ValidationResult().._errors.addAll(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ValidationResult().._data = data;
|
return new ValidationResult().._data.addAll(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates, and filters input data after running [autoParse].
|
/// Validates, and filters input data after running [autoParse].
|
||||||
|
@ -320,16 +351,23 @@ class Validator extends Matcher {
|
||||||
|
|
||||||
/// The result of attempting to validate input data.
|
/// The result of attempting to validate input data.
|
||||||
class ValidationResult {
|
class ValidationResult {
|
||||||
Map<String, dynamic> _data;
|
final Map<String, dynamic> _data = {};
|
||||||
final List<String> _errors = [];
|
final List<String> _errors = [];
|
||||||
|
|
||||||
/// The successfully validated data, filtered from the original input.
|
/// The successfully validated data, filtered from the original input.
|
||||||
Map<String, dynamic> get data => _data;
|
Map<String, dynamic> get data => new Map<String, dynamic>.unmodifiable(_data);
|
||||||
|
|
||||||
/// A list of errors that resulted in the given data being marked invalid.
|
/// A list of errors that resulted in the given data being marked invalid.
|
||||||
///
|
///
|
||||||
/// This is empty if validation was successful.
|
/// This is empty if validation was successful.
|
||||||
List<String> get errors => new List<String>.unmodifiable(_errors);
|
List<String> get errors => new List<String>.unmodifiable(_errors);
|
||||||
|
|
||||||
|
ValidationResult withData(Map<String, dynamic> data) => new ValidationResult()
|
||||||
|
.._data.addAll(data)
|
||||||
|
.._errors.addAll(_errors);
|
||||||
|
|
||||||
|
ValidationResult withErrors(Iterable<String> errors) =>
|
||||||
|
new ValidationResult().._data.addAll(_data).._errors.addAll(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Occurs when user-provided data is invalid.
|
/// Occurs when user-provided data is invalid.
|
||||||
|
|
|
@ -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: 1.0.4
|
version: 1.0.5-beta
|
||||||
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:
|
||||||
|
|
0
test/async_test.dart
Normal file
0
test/async_test.dart
Normal file
|
@ -31,6 +31,7 @@ main() {
|
||||||
expect(() {
|
expect(() {
|
||||||
todoSchema
|
todoSchema
|
||||||
.enforce({'id': 'fool', 'text': 'Hello, world!', 'completed': 4});
|
.enforce({'id': 'fool', 'text': 'Hello, world!', 'completed': 4});
|
||||||
|
// ignore: deprecated_member_use
|
||||||
}, throwsA(new isInstanceOf<ValidationException>()));
|
}, throwsA(new isInstanceOf<ValidationException>()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
0
test/context_aware_test.dart
Normal file
0
test/context_aware_test.dart
Normal file
Loading…
Reference in a new issue