diff --git a/.gitignore b/.gitignore index 00eaddbb..8160f445 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ pubspec.lock log.txt -.idea \ No newline at end of file +.idea +.dart_tool \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4566b79b..109c3eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 1.0.5 +* Use `wrapMatcher` on explicit values instead of throwing. + # 1.0.4 * `isNonEmptyString` trims strings. * `ValidationException` extends `AngelHttpException`. diff --git a/lib/server.dart b/lib/server.dart index fa983fc7..b4e5d18d 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -2,7 +2,9 @@ library angel_validate.server; import 'package:angel_framework/angel_framework.dart'; +import 'src/async.dart'; import 'angel_validate.dart'; +export 'src/async.dart'; export 'angel_validate.dart'; /// Auto-parses numbers in `req.body`. diff --git a/lib/src/async.dart b/lib/src/async.dart new file mode 100644 index 00000000..538dd22e --- /dev/null +++ b/lib/src/async.dart @@ -0,0 +1,131 @@ +import 'dart:async'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_http_exception/angel_http_exception.dart'; +import 'package:matcher/matcher.dart'; + +/// Returns an [AngelMatcher] that uses an arbitrary function that returns +/// true or false for the actual value. +/// +/// Analogous to the synchronous [predicate] matcher. +/// +/// For example: +/// +/// expect(v, predicate((x) => ((x % 2) == 0), "is even")) +AngelMatcher predicateWithAngel(FutureOr Function(Object, Angel) f, + [String description = 'satisfies function']) => + new _PredicateWithAngel(f, description); + +/// Returns an [AngelMatcher] that applies an asynchronously-created [Matcher] +/// to the input. +/// +/// Use this to match values against configuration, injections, etc. +AngelMatcher matchWithAngel(FutureOr Function(Object, Angel) f, + [String description = 'satisfies asynchronously created matcher']) => + new _MatchWithAngel(f, description); + +/// Calls [matchWithAngel] without the initial parameter. +AngelMatcher matchWithAngelUnary(FutureOr Function(Angel) f, + [String description = 'satisfies asynchronously created matcher']) => + matchWithAngel((_, app) => f(app)); + +/// Returns an [AngelMatcher] that represents [x]. +/// +/// If [x] is an [AngelMatcher], then it is returned, unmodified. +AngelMatcher wrapAngelMatcher(x) { + if (x is AngelMatcher) return x; + return matchWithAngel((_, app) => wrapMatcher(x)); +} + +/// Returns an [AngelMatcher] that asynchronously resolves a [feature], builds a [matcher], and executes it. +AngelMatcher matchAsync( + FutureOr Function(Object) matcher, FutureOr Function() feature, + [String description = 'satisfies asynchronously created matcher']) { + return new _MatchAsync(matcher, feature, description); +} + +/// Returns an [AngelMatcher] that verifies that an item with the given [idField] +/// exists in the service at [servicePath], without throwing a `404` or returning `null`. +AngelMatcher idExistsInService(String servicePath, + {String idField: 'id', String description}) { + return predicateWithAngel( + (item, app) async { + try { + var result = await app.service(servicePath)?.read(item); + return result != null; + } on AngelHttpException catch (e) { + if (e.statusCode == 404) { + return false; + } else { + rethrow; + } + } + }, + description ?? 'exists in service $servicePath', + ); +} + +/// An asynchronous [Matcher] that runs in the context of an [Angel] app. +abstract class AngelMatcher extends Matcher { + Future matchesAsync(item, Map matchState, Angel app); + + @override + bool matches(item, Map matchState) { + return true; + } +} + +class _MatchWithAngel extends AngelMatcher { + final FutureOr Function(Object, Angel) f; + final String description; + + _MatchWithAngel(this.f, this.description); + + @override + Description describe(Description description) => this.description == null + ? description + : description.add(this.description); + + @override + Future matchesAsync(item, Map matchState, Angel app) { + return new Future.sync(() => f(item, app)).then((result) { + return result.matches(item, matchState); + }); + } +} + +class _PredicateWithAngel extends AngelMatcher { + final FutureOr Function(Object, Angel) predicate; + final String description; + + _PredicateWithAngel(this.predicate, this.description); + + @override + Description describe(Description description) => this.description == null + ? description + : description.add(this.description); + + @override + Future matchesAsync(item, Map matchState, Angel app) { + return new Future.sync(() => predicate(item, app)); + } +} + +class _MatchAsync extends AngelMatcher { + final FutureOr Function(Object) matcher; + final FutureOr Function() feature; + final String description; + + _MatchAsync(this.matcher, this.feature, this.description); + + @override + Description describe(Description description) => this.description == null + ? description + : description.add(this.description); + + @override + Future matchesAsync(item, Map matchState, Angel app) async { + var f = await feature(); + var m = await matcher(f); + return m.matches(item, matchState); + } +} diff --git a/lib/src/validator.dart b/lib/src/validator.dart index 9ae82a0e..fea5eac4 100644 --- a/lib/src/validator.dart +++ b/lib/src/validator.dart @@ -96,8 +96,7 @@ class Validator extends Matcher { } else if (rule is Filter) { addRule(fieldName, predicate(rule)); } else { - throw new ArgumentError( - 'Cannot use a(n) ${rule.runtimeType} as a validation rule.'); + addRule(fieldName, wrapMatcher(rule)); } } }