diff --git a/README.md b/README.md index 36b21335..4eca3d6f 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ For convenience's sake, this library also exports `matcher`. import 'package:angel_validate/angel_validate.dart'; main() { - var validator = new Validator({ + var validator = Validator({ 'username': isAlphaNum, 'multiple,keys,with,same,rules': [isString, isNotEmpty], 'balance': [ @@ -91,7 +91,7 @@ to throw an error if it is not present. ```dart main() { - var validator = new Validator({ + var validator = Validator({ 'googleId*': isString, // You can also use `requireField` @@ -111,7 +111,7 @@ If not present, default values will be filled in *before* validation. This means that they can still be used with required fields. ```dart -final Validator todo = new Validator({ +final Validator todo = Validator({ 'text*': isString, 'completed*': isBool }, defaultValues: { @@ -134,9 +134,9 @@ The function must *synchronously* return a `bool`. ```dart main() { - var validator = new Validator({ + var validator = Validator({ 'key*': (key) { - var file = new File('whitelist.txt'); + var file = File('whitelist.txt'); return file.readFileSync().contains(key); } }); @@ -148,7 +148,7 @@ If these are not present, `angel_validate` will *attempt* to generate a coherent error message on its own. ```dart -new Validator({ +Validator({ 'age': [greaterThanOrEqualTo(18)] }, customErrorMessages: { 'age': 'You must be an adult to see this page.' @@ -189,7 +189,7 @@ You can also use `extend` to mark fields as required or forbidden that originall were not. Default value and custom error message extension is also supported. ```dart -final Validator userValidator = new Validator({ +final Validator userValidator = Validator({ 'username': isString, 'age': [ isNum, @@ -210,7 +210,7 @@ var ageIsOptional = userValidator.extend({ }); ``` -Note that by default, new validation rules are simply appended to +Note that by default, validation rules are simply appended to the existing list. To completely overwrite existing rules, set the `overwrite` flag to `true`. @@ -246,23 +246,23 @@ a `Validator` instance to the constructor, because it extends the ```dart main() { - var bio = new Validator({ + var bio = Validator({ 'age*': [isInt, greaterThanOrEqualTo(0)], 'birthYear*': isInt, 'countryOfOrigin': isString }); - var book = new Validator({ + var book = Validator({ 'title*': isString, 'year*': [ isNum, (year) { - return year <= new DateTime.now().year; + return year <= DateTime.now().year; } ] }); - var author = new Validator({ + var author = Validator({ 'bio*': bio, 'books*': [ isList, @@ -289,11 +289,11 @@ main() { import 'package:angel_framework/angel_framework.dart'; import 'package:angel_validate/server.dart'; -final Validator echo = new Validator({ +final Validator echo = Validator({ 'message*': (String message) => message.length >= 5 }); -final Validator todo = new Validator({ +final Validator todo = Validator({ 'text*': isString, 'completed*': isBool }, defaultValues: { @@ -301,7 +301,7 @@ final Validator todo = new Validator({ }); main() async { - var app = new Angel(); + var app = Angel(); app.chain([validate(echo)]).post('/echo', (req, res) async { res.write('You said: "${req.bodyAsMap["message"]}"'); diff --git a/analysis_options.yaml b/analysis_options.yaml index eae1e42a..c230cee7 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,3 +1,4 @@ +include: package:pedantic/analysis_options.yaml analyzer: strong-mode: implicit-casts: false \ No newline at end of file diff --git a/example/main.dart b/example/main.dart index af500750..f9ed89b5 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,24 +1,24 @@ import 'package:angel_validate/angel_validate.dart'; main() { - var bio = new Validator({ + var bio = Validator({ 'age*': [isInt, greaterThanOrEqualTo(0)], 'birthYear*': isInt, 'countryOfOrigin': isString }); - var book = new Validator({ + var book = Validator({ 'title*': isString, 'year*': [ isNum, (year) { - return year <= new DateTime.now().year; + return year <= DateTime.now().year; } ] }); // ignore: unused_local_variable - var author = new Validator({ + var author = Validator({ 'bio*': bio, 'books*': [isList, everyElement(book)] }, defaultValues: { diff --git a/lib/server.dart b/lib/server.dart index 4e03bad5..9ae41185 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -52,13 +52,13 @@ RequestHandler filterQuery(Iterable only) { /// 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.'}) { + {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 new AngelHttpException.badRequest( + throw AngelHttpException.badRequest( message: errorMessage, errors: result.errors); } @@ -73,13 +73,13 @@ RequestHandler validate(Validator validator, /// 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.'}) { + {String errorMessage = 'Invalid data.'}) { return (RequestContext req, res) async { var result = await asyncApplyValidator(validator, req.queryParameters, req.app); if (result.errors.isNotEmpty) { - throw new AngelHttpException.badRequest( + throw AngelHttpException.badRequest( message: errorMessage, errors: result.errors); } @@ -94,13 +94,13 @@ RequestHandler validateQuery(Validator validator, /// 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.'}) { + {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 new AngelHttpException.badRequest( + throw AngelHttpException.badRequest( message: errorMessage, errors: result.errors); } @@ -120,7 +120,7 @@ Future asyncApplyValidator( for (var key in result.data.keys) { var value = result.data[key]; - var description = new StringDescription("'$key': expected "); + var description = StringDescription("'$key': expected "); for (var rule in validator.rules[key]) { if (rule is AngelMatcher) { @@ -135,7 +135,7 @@ Future asyncApplyValidator( } } - var m = new Map.from(result.data); + var m = Map.from(result.data); for (var key in errantKeys) { m.remove(key); } diff --git a/lib/src/async.dart b/lib/src/async.dart index c4a316af..11099fb2 100644 --- a/lib/src/async.dart +++ b/lib/src/async.dart @@ -11,7 +11,7 @@ import 'context_aware.dart'; AngelMatcher predicateWithAngel( FutureOr Function(String, Object, Angel) f, [String description = 'satisfies function']) => - new _PredicateWithAngel(f, description); + _PredicateWithAngel(f, description); /// Returns an [AngelMatcher] that applies an asynchronously-created [Matcher] /// to the input. @@ -19,7 +19,7 @@ AngelMatcher predicateWithAngel( /// Use this to match values against configuration, injections, etc. AngelMatcher matchWithAngel(FutureOr Function(Object, Map, Angel) f, [String description = 'satisfies asynchronously created matcher']) => - new _MatchWithAngel(f, description); + _MatchWithAngel(f, description); /// Calls [matchWithAngel] without the initial parameter. AngelMatcher matchWithAngelBinary( @@ -42,7 +42,7 @@ AngelMatcher matchWithAngelNullary(FutureOr Function() f, /// If [x] is an [AngelMatcher], then it is returned, unmodified. AngelMatcher wrapAngelMatcher(x) { if (x is AngelMatcher) return x; - if (x is ContextAwareMatcher) return new _WrappedAngelMatcher(x); + if (x is ContextAwareMatcher) return _WrappedAngelMatcher(x); return wrapAngelMatcher(wrapContextAwareMatcher(x)); } @@ -50,13 +50,13 @@ AngelMatcher wrapAngelMatcher(x) { AngelMatcher matchAsync(FutureOr Function(String, Object) matcher, FutureOr Function() feature, [String description = 'satisfies asynchronously created matcher']) { - return new _MatchAsync(matcher, feature, description); + 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}) { + {String idField = 'id', String description}) { return predicateWithAngel( (key, item, app) async { try { @@ -115,7 +115,7 @@ class _MatchWithAngel extends AngelMatcher { @override Future matchesWithAngel( item, String key, Map context, Map matchState, Angel app) { - return new Future.sync(() => f(item, context, app)).then((result) { + return Future.sync(() => f(item, context, app)).then((result) { return result.matches(item, matchState); }); } @@ -135,7 +135,7 @@ class _PredicateWithAngel extends AngelMatcher { @override Future matchesWithAngel( item, String key, Map context, Map matchState, Angel app) { - return new Future.sync(() => predicate(key, item, app)); + return Future.sync(() => predicate(key, item, app)); } } diff --git a/lib/src/context_aware.dart b/lib/src/context_aware.dart index 9970d773..d9d3de61 100644 --- a/lib/src/context_aware.dart +++ b/lib/src/context_aware.dart @@ -4,14 +4,14 @@ import 'package:matcher/matcher.dart'; ContextAwareMatcher predicateWithContext( bool Function(Object, String, Map, Map) f, [String description = 'satisfies function']) { - return new _PredicateWithContext(f, description); + return _PredicateWithContext(f, description); } /// Wraps [x] in a [ContextAwareMatcher]. ContextAwareMatcher wrapContextAwareMatcher(x) { - if (x is ContextAwareMatcher) + if (x is ContextAwareMatcher) { return x; - else if (x is Matcher) return new _WrappedContextAwareMatcher(x); + } else if (x is Matcher) return _WrappedContextAwareMatcher(x); return wrapContextAwareMatcher(wrapMatcher(x)); } diff --git a/lib/src/matchers.dart b/lib/src/matchers.dart index 93ded920..3a6c7be0 100644 --- a/lib/src/matchers.dart +++ b/lib/src/matchers.dart @@ -2,11 +2,11 @@ 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 _alphaNum = new RegExp(r'^[A-Za-z0-9]+$'); -final RegExp _email = new RegExp( +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 = new RegExp( +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. @@ -126,8 +126,8 @@ ContextValidator requiredWithoutAll(Iterable keys) => _require((ctx) => !keys.any(ctx.containsKey)); ContextValidator _require(bool Function(Map) f) { - return new ContextValidator( + return ContextValidator( (key, context) => f(context) && context.containsKey(key), - (desc, key, _) => new StringDescription('Missing required field "$key".'), + (desc, key, _) => StringDescription('Missing required field "$key".'), ); } diff --git a/lib/src/validator.dart b/lib/src/validator.dart index 06f25806..98a20047 100644 --- a/lib/src/validator.dart +++ b/lib/src/validator.dart @@ -3,9 +3,9 @@ import 'package:matcher/matcher.dart'; import 'context_aware.dart'; import 'context_validator.dart'; -final RegExp _asterisk = new RegExp(r'\*$'); -final RegExp _forbidden = new RegExp(r'!$'); -final RegExp _optional = new RegExp(r'\?$'); +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(); @@ -84,10 +84,11 @@ class Validator extends Matcher { var iterable = []; _addTo(x) { - if (x is Iterable) + if (x is Iterable) { x.forEach(_addTo); - else + } else { iterable.add(x); + } } _iterable.forEach(_addTo); @@ -108,8 +109,8 @@ class Validator extends Matcher { Validator.empty(); Validator(Map schema, - {Map defaultValues: const {}, - Map customErrorMessages: const {}}) { + {Map defaultValues = const {}, + Map customErrorMessages = const {}}) { this.defaultValues.addAll(defaultValues ?? {}); this.customErrorMessages.addAll(customErrorMessages ?? {}); _importSchema(schema); @@ -121,7 +122,7 @@ class Validator extends Matcher { /// Validates, and filters input data. ValidationResult check(Map inputData) { List errors = []; - var input = new Map.from(inputData); + var input = Map.from(inputData); Map data = {}; for (String key in defaultValues.keys) { @@ -133,20 +134,22 @@ class Validator extends Matcher { for (String field in forbiddenFields) { if (input.containsKey(field)) { - if (!customErrorMessages.containsKey(field)) + if (!customErrorMessages.containsKey(field)) { errors.add("'$field' is forbidden."); - else + } else { errors.add(customError(field, input[field])); + } } } for (String field in requiredFields) { if (!_hasContextValidators(rules[field] ?? [])) { if (!input.containsKey(field)) { - if (!customErrorMessages.containsKey(field)) + if (!customErrorMessages.containsKey(field)) { errors.add("'$field' is required."); - else + } else { errors.add(customError(field, 'none')); + } } } } @@ -157,7 +160,7 @@ class Validator extends Matcher { if (key is String && rules.containsKey(key)) { var valid = true; var value = input[key]; - var description = new StringDescription("'$key': expected "); + var description = StringDescription("'$key': expected "); for (var matcher in rules[key]) { if (matcher is ContextValidator) { @@ -192,8 +195,9 @@ class Validator extends Matcher { } if (!result) { - if (!customErrorMessages.containsKey(key)) + if (!customErrorMessages.containsKey(key)) { errors.add(matcher.describe(description).toString().trim()); + } valid = false; break; } @@ -215,10 +219,10 @@ class Validator extends Matcher { } if (errors.isNotEmpty) { - return new ValidationResult().._errors.addAll(errors); + return ValidationResult().._errors.addAll(errors); } - return new ValidationResult().._data.addAll(data); + return ValidationResult().._data.addAll(data); } /// Validates, and filters input data after running [autoParse]. @@ -227,29 +231,30 @@ class Validator extends Matcher { /// Renders the given custom error. String customError(String key, value) { - if (!customErrorMessages.containsKey(key)) - throw new ArgumentError("No custom error message registered for '$key'."); + if (!customErrorMessages.containsKey(key)) { + throw ArgumentError("No custom error message registered for '$key'."); + } var msg = customErrorMessages[key]; - if (msg is String) + if (msg is String) { return msg.replaceAll('{{value}}', value.toString()); - else if (msg is CustomErrorMessageFunction) { + } else if (msg is CustomErrorMessageFunction) { return msg(value); } - throw new ArgumentError("Invalid custom error message '$key': $msg"); + 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 enforce(Map inputData, - {String errorMessage: 'Invalid data.'}) { + {String errorMessage = 'Invalid data.'}) { var result = check(inputData); if (result._errors.isNotEmpty) { - throw new ValidationException(errorMessage, errors: result._errors); + throw ValidationException(errorMessage, errors: result._errors); } return result.data; @@ -263,11 +268,11 @@ class Validator extends Matcher { /// Creates a copy with additional validation rules. Validator extend(Map schema, - {Map defaultValues: const {}, - Map customErrorMessages: const {}, - bool overwrite: false}) { + {Map defaultValues = const {}, + Map customErrorMessages = const {}, + bool overwrite = false}) { Map _schema = {}; - var child = new Validator.empty() + var child = Validator.empty() ..defaultValues.addAll(this.defaultValues) ..defaultValues.addAll(defaultValues ?? {}) ..customErrorMessages.addAll(this.customErrorMessages) @@ -355,18 +360,18 @@ class ValidationResult { final List _errors = []; /// The successfully validated data, filtered from the original input. - Map get data => new Map.unmodifiable(_data); + Map get data => Map.unmodifiable(_data); /// A list of errors that resulted in the given data being marked invalid. /// /// This is empty if validation was successful. - List get errors => new List.unmodifiable(_errors); + List get errors => List.unmodifiable(_errors); ValidationResult withData(Map data) => - new ValidationResult().._data.addAll(data).._errors.addAll(_errors); + ValidationResult().._data.addAll(data).._errors.addAll(_errors); ValidationResult withErrors(Iterable errors) => - new ValidationResult().._data.addAll(_data).._errors.addAll(errors); + ValidationResult().._data.addAll(_data).._errors.addAll(errors); } /// Occurs when user-provided data is invalid. @@ -377,8 +382,8 @@ class ValidationException extends AngelHttpException { /// A descriptive message describing the error. final String message; - ValidationException(this.message, {List errors: const []}) - : super(new FormatException(message), + ValidationException(this.message, {List errors = const []}) + : super(FormatException(message), statusCode: 400, errors: errors ?? [], stackTrace: StackTrace.current) { diff --git a/pubspec.yaml b/pubspec.yaml index 3ec0cb57..baa18662 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,4 +15,5 @@ dev_dependencies: build_web_compilers: ^0.4.0 logging: ^0.11.0 mock_request: + pedantic: ^1.0.0 test: ^1.0.0 \ No newline at end of file diff --git a/test/basic_test.dart b/test/basic_test.dart index 47f28d3f..567696b2 100644 --- a/test/basic_test.dart +++ b/test/basic_test.dart @@ -1,10 +1,10 @@ import 'package:angel_validate/angel_validate.dart'; import 'package:test/test.dart'; -final Validator emailSchema = new Validator({'to': isEmail}, - customErrorMessages: {'to': 'Hello, world!'}); +final Validator emailSchema = + Validator({'to': isEmail}, customErrorMessages: {'to': 'Hello, world!'}); -final Validator todoSchema = new Validator({ +final Validator todoSchema = Validator({ 'id': [isInt, isPositive], 'text*': isString, 'completed*': isBool, @@ -32,7 +32,7 @@ main() { todoSchema .enforce({'id': 'fool', 'text': 'Hello, world!', 'completed': 4}); // ignore: deprecated_member_use - }, throwsA(new isInstanceOf())); + }, throwsA(isInstanceOf())); }); test('filter', () { diff --git a/test/server_test.dart b/test/server_test.dart index 0bf6451e..3e84f4ce 100644 --- a/test/server_test.dart +++ b/test/server_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/http.dart'; @@ -7,7 +8,7 @@ import 'package:logging/logging.dart'; import 'package:mock_request/mock_request.dart'; import 'package:test/test.dart'; -final Validator echoSchema = new Validator({'message*': isString}); +final Validator echoSchema = Validator({'message*': isString}); void printRecord(LogRecord rec) { print(rec); @@ -21,8 +22,8 @@ main() { TestClient client; setUp(() async { - app = new Angel(); - http = new AngelHttp(app, useZone: false); + app = Angel(); + http = AngelHttp(app, useZone: false); app.chain([validate(echoSchema)]).post('/echo', (RequestContext req, res) async { @@ -30,7 +31,7 @@ main() { res.write('Hello, ${req.bodyAsMap['message']}!'); }); - app.logger = new Logger('angel')..onRecord.listen(printRecord); + app.logger = Logger('angel')..onRecord.listen(printRecord); client = await connectTo(app); }); @@ -51,12 +52,15 @@ main() { }); test('enforce', () async { - var rq = new MockHttpRequest('POST', new Uri(path: '/echo')) + var rq = MockHttpRequest('POST', Uri(path: '/echo')) ..headers.add('accept', '*/*') ..headers.add('content-type', 'application/json') - ..write(json.encode({'foo': 'bar'})) - ..close(); - http.handleRequest(rq); + ..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}'); diff --git a/web/main.dart b/web/main.dart index 08406f27..e2c90e5a 100644 --- a/web/main.dart +++ b/web/main.dart @@ -5,7 +5,7 @@ final $errors = querySelector('#errors') as UListElement; final $form = querySelector('#form') as FormElement; final $blank = querySelector('[name="blank"]') as InputElement; -final Validator formSchema = new Validator({ +final Validator formSchema = Validator({ 'firstName*': [isString, isNotEmpty], 'lastName*': [isString, isNotEmpty], 'age*': [isInt, greaterThanOrEqualTo(18)], @@ -15,12 +15,13 @@ final Validator formSchema = new Validator({ 'familySize': 1 }, customErrorMessages: { 'age': (age) { - if (age is int && age < 18) + 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)) + } else if (age == null || (age is String && age.trim().isEmpty)) { return 'Age is required.'; - else + } 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}}'..." @@ -54,12 +55,12 @@ main() { 'Number of People in Family: ${passportInfo["familySize"]}')); } on ValidationException catch (e) { $errors.children.addAll(e.errors.map((error) { - return new LIElement()..text = error; + return LIElement()..text = error; })); } }); } -LIElement success(String str) => new LIElement() +LIElement success(String str) => LIElement() ..classes.add('success') ..text = str;