Wipe out for v3

This commit is contained in:
Tobe O 2019-10-16 19:52:50 -04:00
parent 3b956230c6
commit 8f025080ac
20 changed files with 184 additions and 1207 deletions

View file

@ -1,27 +1 @@
import 'package:angel_validate/angel_validate.dart';
main() {
var bio = Validator({
'age*': [isInt, greaterThanOrEqualTo(0)],
'birthYear*': isInt,
'countryOfOrigin': isString
});
var book = Validator({
'title*': isString,
'year*': [
isNum,
(year) {
return year <= DateTime.now().year;
}
]
});
// ignore: unused_local_variable
var author = Validator({
'bio*': bio,
'books*': [isList, everyElement(book)]
}, defaultValues: {
'books': []
});
}

2
lib/angel_forms.dart Normal file
View file

@ -0,0 +1,2 @@
export 'src/field.dart';
export 'src/form.dart';

View file

@ -1,14 +0,0 @@
/// Cross-platform validation library based on `matcher`.
library angel_validate;
export 'package:matcher/matcher.dart';
export 'src/context_aware.dart';
export 'src/matchers.dart';
export 'src/validator.dart';
/// Marks a field name as required.
String requireField(String field) => '$field*';
/// Marks multiple fields as required.
String requireFields(Iterable<String> fields) =>
fields.map(requireField).join(', ');

View file

@ -1,144 +0,0 @@
/// Support for using `angel_validate` with the Angel Framework.
library angel_validate.server;
import 'dart:async';
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.bodyAsMap`.
RequestHandler autoParseBody(List<String> fields) {
return (RequestContext req, res) async {
await req.parseBody();
req.bodyAsMap.addAll(autoParse(req.bodyAsMap, fields));
return true;
};
}
/// Auto-parses numbers in `req.queryParameters`.
RequestHandler autoParseQuery(List<String> fields) {
return (RequestContext req, res) async {
req.queryParameters.addAll(autoParse(req.queryParameters, fields));
return true;
};
}
/// Filters unwanted data out of `req.bodyAsMap`.
RequestHandler filterBody(Iterable<String> only) {
return (RequestContext req, res) async {
await req.parseBody();
var filtered = filter(req.bodyAsMap, only);
req.bodyAsMap
..clear()
..addAll(filtered);
return true;
};
}
/// Filters unwanted data out of `req.queryParameters`.
RequestHandler filterQuery(Iterable<String> only) {
return (RequestContext req, res) async {
var filtered = filter(req.queryParameters, only);
req.queryParameters
..clear()
..addAll(filtered);
return true;
};
}
/// Validates the data in `req.bodyAsMap`, and sets the body to
/// filtered data before continuing the response.
RequestHandler validate(Validator validator,
{String errorMessage = 'Invalid data.'}) {
return (RequestContext req, res) async {
await req.parseBody();
var result = await asyncApplyValidator(validator, req.bodyAsMap, req.app);
if (result.errors.isNotEmpty) {
throw AngelHttpException.badRequest(
message: errorMessage, errors: result.errors);
}
req.bodyAsMap
..clear()
..addAll(result.data);
return true;
};
}
/// Validates the data in `req.queryParameters`, and sets the query to
/// filtered data before continuing the response.
RequestHandler validateQuery(Validator validator,
{String errorMessage = 'Invalid data.'}) {
return (RequestContext req, res) async {
var result =
await asyncApplyValidator(validator, req.queryParameters, req.app);
if (result.errors.isNotEmpty) {
throw AngelHttpException.badRequest(
message: errorMessage, errors: result.errors);
}
req.queryParameters
..clear()
..addAll(result.data);
return true;
};
}
/// Validates the data in `e.data`, and sets the data to
/// filtered data before continuing the service event.
HookedServiceEventListener validateEvent(Validator validator,
{String errorMessage = 'Invalid data.'}) {
return (HookedServiceEvent e) async {
var result = await asyncApplyValidator(
validator, e.data as Map, (e.request?.app ?? e.service.app));
if (result.errors.isNotEmpty) {
throw AngelHttpException.badRequest(
message: errorMessage, errors: result.errors);
}
e.data
..clear()
..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 = 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 = Map<String, dynamic>.from(result.data);
for (var key in errantKeys) {
m.remove(key);
}
return result.withData(m).withErrors(errors);
}

View file

@ -1,162 +0,0 @@
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_http_exception/angel_http_exception.dart';
import 'package:matcher/matcher.dart';
import 'context_aware.dart';
/// Returns an [AngelMatcher] that uses an arbitrary function that returns
/// true or false for the actual value.
///
/// Analogous to the synchronous [predicate] matcher.
AngelMatcher predicateWithAngel(
FutureOr<bool> Function(String, Object, Angel) f,
[String description = 'satisfies function']) =>
_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<Matcher> Function(Object, Map, Angel) f,
[String description = 'satisfies asynchronously created matcher']) =>
_MatchWithAngel(f, description);
/// 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,
[String description = 'satisfies asynchronously created matcher']) =>
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].
///
/// If [x] is an [AngelMatcher], then it is returned, unmodified.
AngelMatcher wrapAngelMatcher(x) {
if (x is AngelMatcher) return x;
if (x is ContextAwareMatcher) return _WrappedAngelMatcher(x);
return wrapAngelMatcher(wrapContextAwareMatcher(x));
}
/// Returns an [AngelMatcher] that asynchronously resolves a [feature], builds a [matcher], and executes it.
AngelMatcher matchAsync(FutureOr<Matcher> Function(String, Object) matcher,
FutureOr Function() feature,
[String description = 'satisfies asynchronously created matcher']) {
return _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(
(key, item, app) async {
try {
var result = await app.findService(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 ContextAwareMatcher {
Future<bool> matchesWithAngel(
item, String key, Map context, Map matchState, Angel app);
@override
bool matchesWithContext(item, String key, Map context, Map matchState) {
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 {
final FutureOr<Matcher> Function(Object, Map, 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<bool> matchesWithAngel(
item, String key, Map context, Map matchState, Angel app) {
return Future.sync(() => f(item, context, app)).then((result) {
return result.matches(item, matchState);
});
}
}
class _PredicateWithAngel extends AngelMatcher {
final FutureOr<bool> Function(String, 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<bool> matchesWithAngel(
item, String key, Map context, Map matchState, Angel app) {
return Future<bool>.sync(() => predicate(key, item, app));
}
}
class _MatchAsync extends AngelMatcher {
final FutureOr<Matcher> Function(String, 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<bool> matchesWithAngel(
item, String key, Map context, Map matchState, Angel app) async {
var f = await feature();
var m = await matcher(key, f);
var c = wrapAngelMatcher(m);
return await c.matchesWithAngel(item, key, context, matchState, app);
}
}

View file

@ -1,53 +0,0 @@
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 _PredicateWithContext(f, description);
}
/// Wraps [x] in a [ContextAwareMatcher].
ContextAwareMatcher wrapContextAwareMatcher(x) {
if (x is ContextAwareMatcher) {
return x;
} else if (x is Matcher) return _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);
}

View file

@ -1,15 +0,0 @@
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;
}

66
lib/src/field.dart Normal file
View file

@ -0,0 +1,66 @@
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import 'package:matcher/matcher.dart';
import 'form_renderer.dart';
class FieldReadResult<T> {
final bool isSuccess;
final T value;
final Iterable<String> errors;
FieldReadResult.success(this.value)
: isSuccess = true,
errors = null;
FieldReadResult.failure(this.errors)
: isSuccess = false,
value = null;
}
abstract class Field<T> {
final String name;
final String label;
final bool isRequired;
Field(this.name, {this.label, this.isRequired = false});
FutureOr<FieldReadResult<T>> read(RequestContext req);
FutureOr<U> accept<U>(FormRenderer<U> renderer);
Field<T> match(Iterable<Matcher> matchers) => _MatchedField(this, matchers);
}
class _MatchedField<T> extends Field<T> {
final Field<T> inner;
final Iterable<Matcher> matchers;
_MatchedField(this.inner, this.matchers)
: super(inner.name, label: inner.label, isRequired: inner.isRequired) {
assert(matchers.isNotEmpty);
}
@override
FutureOr<U> accept<U>(FormRenderer<U> renderer) => inner.accept(renderer);
@override
Future<FieldReadResult<T>> read(RequestContext req) async {
var result = await inner.read(req);
if (!result.isSuccess) {
return result;
} else {
var errors = <String>[];
for (var matcher in matchers) {
if (!matcher.matches(result.value, {})) {
var desc = matcher.describe(StringDescription());
errors.add('Expected $desc for field "${inner.name}".');
}
}
if (errors.isEmpty) {
return result;
} else {
return FieldReadResult.failure(errors);
}
}
}
}

97
lib/src/form.dart Normal file
View file

@ -0,0 +1,97 @@
import 'dart:async';
import 'dart:convert';
import 'package:angel_framework/angel_framework.dart';
import 'package:matcher/matcher.dart';
import 'field.dart';
/// A utility that combines multiple [Field]s to read and
/// validate web forms in a type-safe manner.
///
/// Example:
/// ```dart
/// import 'package:angel_forms/angel_forms.dart';
/// import 'package:angel_validate/angel_validate.dart';
///
/// var myForm = Form(fields: [
/// TextField('username').match([]),
/// TextField('password', confirmedAs: 'confirm_password'),
/// ])
///
/// app.post('/login', (req, res) async {
/// var loginRequest =
/// await myForm.decode(req, loginRequestSerializer);
/// // Do something with the decode object...
/// });
/// ```
class Form {
final String errorMessage;
final List<Field> _fields = [];
final List<String> _errors = [];
static const String defaultErrorMessage =
'There were errors in your submission. '
'Please make sure all fields entered correctly, and submit it again.';
Form({this.errorMessage = defaultErrorMessage, Iterable<Field> fields}) {
fields?.forEach(addField);
}
List<Field> get fields => _fields;
List<String> get errors => _errors;
Field<T> addField<T>(Field<T> field, {Iterable<Matcher> matchers}) {
if (matchers != null) {
field = field.match(matchers);
}
_fields.add(field);
return field;
}
Future<T> deserialize<T>(
RequestContext req, T Function(Map<String, dynamic>) f) {
return validate(req).then(f);
}
Future<T> decode<T>(RequestContext req, Codec<T, Map> codec) {
return deserialize(req, codec.decode);
}
Future<Map<String, dynamic>> validate(RequestContext req) async {
var result = await read(req);
if (!result.isSuccess) {
throw AngelHttpException.badRequest(
message: errorMessage, errors: result.errors.toList());
} else {
return result.value;
}
}
/// Reads the body of the [RequestContext], and returns an object detailing
/// whether valid values were provided for all [fields].
///
/// In most cases, you'll want to use [validate] instead.
Future<FieldReadResult<Map<String, dynamic>>> read(RequestContext req) async {
var out = <String, dynamic>{};
var errors = <String>[];
await req.parseBody();
for (var field in fields) {
var result = await field.read(req);
if (result == null && field.isRequired) {
errors.add('The field "${field.name}" is required.');
} else if (!result.isSuccess) {
errors.addAll(result.errors);
} else {
out[field.name] = result.value;
}
}
if (errors.isNotEmpty) {
return FieldReadResult.failure(errors);
} else {
return FieldReadResult.success(out);
}
}
}

View file

@ -0,0 +1,6 @@
import 'dart:async';
import 'field.dart';
abstract class FormRenderer<T> {
const FormRenderer();
}

View file

@ -1,133 +0,0 @@
import 'package:matcher/matcher.dart';
import 'context_aware.dart';
import 'context_validator.dart';
final RegExp _alphaDash = RegExp(r'^[A-Za-z0-9_-]+$');
final RegExp _alphaNum = RegExp(r'^[A-Za-z0-9]+$');
final RegExp _email = 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])?$");
final RegExp _url = RegExp(
r'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-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 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 address');
/// Asserts that a value is an `int`.
final Matcher isInt = predicate((value) => value is int, 'an integer');
/// Asserts that a value is a `num`.
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');
/// Asserts that a value is a non-empty `String`.
final Matcher isNonEmptyString = predicate(
(value) => value is String && value.trim().isNotEmpty,
'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.
///
/// The regular expression used:
/// ```
/// https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)
/// ```
final Matcher isUrl = predicate(
(value) => value is String && _url.hasMatch(value),
'a valid url, starting with http:// or https://');
/// Use [isUrl] instead.
@deprecated
final Matcher isurl = isUrl;
/// Enforces a minimum length on a string.
Matcher minLength(int length) => predicate(
(value) => value is String && value.length >= length,
'a string at least $length character(s) long');
/// Limits the maximum length of a string.
Matcher maxLength(int length) => predicate(
(value) => value is String && value.length <= length,
'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 ContextValidator(
(key, context) => f(context) && context.containsKey(key),
(desc, key, _) => StringDescription('Missing required field "$key".'),
);
}

View file

@ -1,408 +0,0 @@
import 'package:angel_http_exception/angel_http_exception.dart';
import 'package:matcher/matcher.dart';
import 'context_aware.dart';
import 'context_validator.dart';
final RegExp _asterisk = RegExp(r'\*$');
final RegExp _forbidden = RegExp(r'!$');
final RegExp _optional = RegExp(r'\?$');
/// Returns a value based the result of a computation.
typedef DefaultValueFunction();
/// Generates an error message based on the given input.
typedef String CustomErrorMessageFunction(item);
/// 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, Iterable<String> fields) {
Map<String, dynamic> data = {};
for (var key in inputData.keys) {
if (!fields.contains(key)) {
data[key.toString()] = inputData[key];
} else {
try {
var n = inputData[key] is num
? inputData[key]
: num.parse(inputData[key].toString());
data[key.toString()] = n == n.toInt() ? n.toInt() : n;
} catch (e) {
// Invalid number, don't pass it
}
}
}
return data;
}
/// Removes undesired fields from a `Map`.
Map<String, dynamic> filter(Map inputData, Iterable<String> only) {
return inputData.keys.fold(<String, dynamic>{}, (map, key) {
if (only.contains(key.toString())) map[key.toString()] = inputData[key];
return map;
});
}
/// Enforces the validity of input data, according to [Matcher]s.
class Validator extends Matcher {
/// Pre-defined error messages for certain fields.
final Map<String, dynamic> 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 = {};
/// Fields that must be present for data to be considered valid.
final List<String> requiredFields = [];
void _importSchema(Map<String, dynamic> schema) {
for (var keys in schema.keys) {
for (var key in keys.split(',').map((s) => s.trim())) {
var fieldName = key
.replaceAll(_asterisk, '')
.replaceAll(_forbidden, '')
.replaceAll(_optional, '');
var isForbidden = _forbidden.hasMatch(key),
isRequired = _asterisk.hasMatch(key);
if (isForbidden) {
forbiddenFields.add(fieldName);
} else if (isRequired) {
requiredFields.add(fieldName);
}
var _iterable =
schema[keys] is Iterable ? schema[keys] : [schema[keys]];
var iterable = [];
_addTo(x) {
if (x is Iterable) {
x.forEach(_addTo);
} else {
iterable.add(x);
}
}
_iterable.forEach(_addTo);
for (var rule in iterable) {
if (rule is Matcher) {
addRule(fieldName, rule);
} else if (rule is Filter) {
addRule(fieldName, predicate(rule));
} else {
addRule(fieldName, wrapMatcher(rule));
}
}
}
}
}
Validator.empty();
Validator(Map<String, dynamic> schema,
{Map<String, dynamic> defaultValues = const {},
Map<String, dynamic> customErrorMessages = const {}}) {
this.defaultValues.addAll(defaultValues ?? {});
this.customErrorMessages.addAll(customErrorMessages ?? {});
_importSchema(schema);
}
static bool _hasContextValidators(Iterable it) =>
it.any((x) => x is ContextValidator);
/// Validates, and filters input data.
ValidationResult check(Map inputData) {
List<String> errors = [];
var input = 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 forbiddenFields) {
if (input.containsKey(field)) {
if (!customErrorMessages.containsKey(field)) {
errors.add("'$field' is forbidden.");
} else {
errors.add(customError(field, input[field]));
}
}
}
for (String field in requiredFields) {
if (!_hasContextValidators(rules[field] ?? [])) {
if (!input.containsKey(field)) {
if (!customErrorMessages.containsKey(field)) {
errors.add("'$field' is required.");
} else {
errors.add(customError(field, 'none'));
}
}
}
}
// Run context validators.
for (var key in input.keys) {
if (key is String && rules.containsKey(key)) {
var valid = true;
var value = input[key];
var description = StringDescription("'$key': expected ");
for (var matcher in rules[key]) {
if (matcher is ContextValidator) {
if (!matcher.validate(key, input)) {
errors.add(matcher
.errorMessage(description, key, input)
.toString()
.trim());
valid = false;
}
}
}
if (valid) {
for (Matcher matcher in rules[key]) {
try {
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;
}
}
}
if (valid) {
data[key] = value;
} else if (customErrorMessages.containsKey(key)) {
errors.add(customError(key, input[key]));
}
}
}
if (errors.isNotEmpty) {
return ValidationResult().._errors.addAll(errors);
}
return ValidationResult().._data.addAll(data);
}
/// Validates, and filters input data after running [autoParse].
ValidationResult checkParsed(Map inputData, List<String> fields) =>
check(autoParse(inputData, fields));
/// Renders the given custom error.
String customError(String key, value) {
if (!customErrorMessages.containsKey(key)) {
throw ArgumentError("No custom error message registered for '$key'.");
}
var msg = customErrorMessages[key];
if (msg is String) {
return msg.replaceAll('{{value}}', value.toString());
} else if (msg is CustomErrorMessageFunction) {
return msg(value);
}
throw ArgumentError("Invalid custom error message '$key': $msg");
}
/// Validates input data, and throws an error if it is invalid.
///
/// Otherwise, the filtered data is returned.
Map<String, dynamic> enforce(Map inputData,
{String errorMessage = 'Invalid data.'}) {
var result = check(inputData);
if (result._errors.isNotEmpty) {
throw ValidationException(errorMessage, errors: result._errors);
}
return result.data;
}
/// Validates, and filters input data after running [autoParse], and throws an error if it is invalid.
///
/// Otherwise, the filtered data is returned.
Map<String, dynamic> enforceParsed(Map inputData, List<String> fields) =>
enforce(autoParse(inputData, fields));
/// Creates a copy with additional validation rules.
Validator extend(Map<String, dynamic> schema,
{Map<String, dynamic> defaultValues = const {},
Map<String, dynamic> customErrorMessages = const {},
bool overwrite = false}) {
Map<String, dynamic> _schema = {};
var child = Validator.empty()
..defaultValues.addAll(this.defaultValues)
..defaultValues.addAll(defaultValues ?? {})
..customErrorMessages.addAll(this.customErrorMessages)
..customErrorMessages.addAll(customErrorMessages ?? {})
..requiredFields.addAll(requiredFields)
..rules.addAll(rules);
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 (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);
}
if (overwrite) {
if (child.rules.containsKey(fieldName)) child.rules.remove(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 as Map);
return true;
}
@override
String toString() => 'Validation schema: $rules';
}
/// The result of attempting to validate input data.
class ValidationResult {
final Map<String, dynamic> _data = {};
final List<String> _errors = [];
/// The successfully validated data, filtered from the original input.
Map<String, dynamic> get data => Map<String, dynamic>.unmodifiable(_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 => List<String>.unmodifiable(_errors);
ValidationResult withData(Map<String, dynamic> data) =>
ValidationResult().._data.addAll(data).._errors.addAll(_errors);
ValidationResult withErrors(Iterable<String> errors) =>
ValidationResult().._data.addAll(_data).._errors.addAll(errors);
}
/// Occurs when user-provided data is invalid.
class ValidationException extends AngelHttpException {
/// A list of errors that resulted in the given data being marked invalid.
final List<String> errors = [];
/// A descriptive message describing the error.
final String message;
ValidationException(this.message, {Iterable<String> errors = const []})
: super(FormatException(message),
statusCode: 400,
errors: (errors ?? <String>[]).toSet().toList(),
stackTrace: StackTrace.current) {
if (errors != null) this.errors.addAll(errors.toSet());
}
@override
String toString() {
if (errors.isEmpty) {
return message;
}
if (errors.length == 1) {
return 'Validation error: ${errors.first}';
}
var messages = ['${errors.length} validation errors:\n']
..addAll(errors.map((error) => '* $error'));
return messages.join('\n');
}
}

View file

@ -1,19 +1,19 @@
name: angel_validate
description: Cross-platform request body validation library based on `matcher`.
version: 2.0.2
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/validate
version: 3.0.0-alpha
description: Strongly-typed form handlers and validators for Angel.
homepage: https://github/angel-dart/validate
environment:
sdk: ">=2.0.0-dev <3.0.0"
sdk: ">=2.0.0 <3.0.0"
dependencies:
angel_framework: ^2.0.0-alpha
angel_http_exception: ^1.0.0
matcher: ^0.12.0
angel_framework: ^2.0.0
html_builder: ^1.0.0
matcher: ^0.12.5
dev_dependencies:
angel_test: ^2.0.0-alpha
build_runner: ^0.10.0
build_web_compilers: ^0.4.0
logging: ^0.11.0
mock_request:
angel_orm: ^2.1.0-beta
angel_orm_generator: ^2.1.0-beta
angel_serialize: ^2.0.0
angel_serialize_generator: ^2.0.0
build_runner: ^1.0.0
pedantic: ^1.0.0
pretty_logging: ^1.0.0
test: ^1.0.0

View file

@ -1 +0,0 @@
void main() {}

View file

@ -1,49 +0,0 @@
import 'package:angel_validate/angel_validate.dart';
import 'package:test/test.dart';
final Validator emailSchema =
Validator({'to': isEmail}, customErrorMessages: {'to': 'Hello, world!'});
final Validator todoSchema = Validator({
'id': [isInt, isPositive],
'text*': isString,
'completed*': isBool,
'foo,bar': [isTrue]
}, defaultValues: {
'completed': false
});
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('requireField', () => expect(requireField('foo'), 'foo*'));
test('requireFields',
() => expect(requireFields(['foo', 'bar']), 'foo*, bar*'));
test('todo', () {
expect(() {
todoSchema
.enforce({'id': 'fool', 'text': 'Hello, world!', 'completed': 4});
// ignore: deprecated_member_use
}, throwsA(isInstanceOf<ValidationException>()));
});
test('filter', () {
var inputData = {'foo': 'bar', 'a': 'b', '1': 2};
var only = filter(inputData, ['foo']);
expect(only, equals({'foo': 'bar'}));
});
test('comma in schema', () {
expect(todoSchema.rules.keys, allOf(contains('foo'), contains('bar')));
expect([todoSchema.rules['foo'].first, todoSchema.rules['bar'].first],
everyElement(predicate((x) => x == isTrue)));
});
}

View file

@ -1 +0,0 @@
void main() {}

View file

@ -1,70 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_test/angel_test.dart';
import 'package:angel_validate/server.dart';
import 'package:logging/logging.dart';
import 'package:mock_request/mock_request.dart';
import 'package:test/test.dart';
final Validator echoSchema = Validator({'message*': isString});
void printRecord(LogRecord rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
}
main() {
Angel app;
AngelHttp http;
TestClient client;
setUp(() async {
app = Angel();
http = AngelHttp(app, useZone: false);
app.chain([validate(echoSchema)]).post('/echo',
(RequestContext req, res) async {
await req.parseBody();
res.write('Hello, ${req.bodyAsMap['message']}!');
});
app.logger = Logger('angel')..onRecord.listen(printRecord);
client = await connectTo(app);
});
tearDown(() async {
await client.close();
await http.close();
app = null;
client = null;
});
group('echo', () {
test('validate', () async {
var response = await client.post('/echo',
body: {'message': 'world'}, headers: {'accept': '*/*'});
print('Response: ${response.body}');
expect(response, hasStatus(200));
expect(response.body, equals('Hello, world!'));
});
test('enforce', () async {
var rq = MockHttpRequest('POST', Uri(path: '/echo'))
..headers.add('accept', '*/*')
..headers.add('content-type', 'application/json')
..write(json.encode({'foo': 'bar'}));
scheduleMicrotask(() async {
await rq.close();
await http.handleRequest(rq);
});
var responseBody = await rq.response.transform(utf8.decoder).join();
print('Response: ${responseBody}');
expect(rq.response.statusCode, 400);
});
});
}

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -1,37 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>angel_validate</title>
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<style>
#errors li {
color: red;
}
#errors li.success {
color: green;
}
</style>
</head>
<body>
<h1>Passport Registration</h1>
<i>Validation Example</i>
<ul id="errors"></ul>
<form id="form">
<input placeholder="First Name*" name="firstName" type="text">
<input placeholder="Last Name*" name="lastName" type="text">
<br><br>
<input placeholder="Age*" name="age" type="number">
<br><br>
<input placeholder="Family Size" name="familySize" type="number">
<br><br>
<input placeholder="LEAVE THIS BLANK" name="blank">
<br><br>
<input type="submit" value="Submit">
</form>
<script src="main.dart" type="application/dart"></script>
<script src="packages/browser/dart.js"></script>
</body>
</html>

View file

@ -1,66 +0,0 @@
import 'dart:html';
import 'package:angel_validate/angel_validate.dart';
final $errors = querySelector('#errors') as UListElement;
final $form = querySelector('#form') as FormElement;
final $blank = querySelector('[name="blank"]') as InputElement;
final Validator formSchema = Validator({
'firstName*': [isString, isNotEmpty],
'lastName*': [isString, isNotEmpty],
'age*': [isInt, greaterThanOrEqualTo(18)],
'familySize': [isInt, greaterThanOrEqualTo(1)],
'blank!': []
}, defaultValues: {
'familySize': 1
}, customErrorMessages: {
'age': (age) {
if (age is int && age < 18) {
return 'Only adults can register for passports. Sorry, kid!';
} else if (age == null || (age is String && age.trim().isEmpty)) {
return 'Age is required.';
} else {
return 'Age must be a positive integer. Unless you are a monster...';
}
},
'blank':
"I told you to leave that field blank, but instead you typed '{{value}}'..."
});
main() {
$form.onSubmit.listen((e) {
e.preventDefault();
$errors.children.clear();
var formData = {};
['firstName', 'lastName', 'age', 'familySize'].forEach((key) {
formData[key] = (querySelector('[name="$key"]') as InputElement).value;
});
if ($blank.value.isNotEmpty) formData['blank'] = $blank.value;
print('Form data: $formData');
try {
var passportInfo =
formSchema.enforceParsed(formData, ['age', 'familySize']);
$errors.children
..add(success('Successfully registered for a passport.'))
..add(success('First Name: ${passportInfo["firstName"]}'))
..add(success('Last Name: ${passportInfo["lastName"]}'))
..add(success('Age: ${passportInfo["age"]} years old'))
..add(success(
'Number of People in Family: ${passportInfo["familySize"]}'));
} on ValidationException catch (e) {
$errors.children.addAll(e.errors.map((error) {
return LIElement()..text = error;
}));
}
});
}
LIElement success(String str) => LIElement()
..classes.add('success')
..text = str;