From bc2048669eb8423aabb4411c54388459fbf74901 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Mon, 26 Dec 2016 08:04:42 -0500 Subject: [PATCH] 0.0.1 --- README.md | 56 +++++++++++++++---- lib/server.dart | 16 ++++++ lib/src/matchers.dart | 24 +++++++-- lib/src/validator.dart | 118 +++++++++++++++++++++++++++++++---------- pubspec.yaml | 2 +- test/basic_test.dart | 14 +++++ 6 files changed, 188 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 6ddfcd83..e9a52a09 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,11 @@ For convenience's sake, this library also exports `matcher`. * [Creating a Validator](#creating-a-validator) * [Validating Data](#validating-data) * [Required Fields](#required-fields) + * [Forbidden Fields](#forbidden-fields) * [Default Values](#default-values) * [Custom Validator Functions](#custom-validator-functions) +* [Auto-parsing Numbers](#autoparse) +* [Custom Error Messages](#custom-error-messages) * [Extending Validators](#extending-validators) * [Bundled Matchers](#bundled-matchers) * [Nested Validators](#nested-validators) @@ -88,6 +91,10 @@ main() { } ``` +## Forbidden Fields +To prevent a field from showing up in valid data, suffix it +with a `'!'`. + ## Default values @@ -127,10 +134,39 @@ main() { } ``` +# Custom Error Messages +If these are not present, `angel_validate` will *attempt* to generate +a coherent error message on its own. + +```dart +new Validator({ + 'age': [greaterThanOrEqualTo(18)] +}, customErrorMessages: { + 'age': 'You must be an adult to see this page.' +}); +``` + +# autoParse +Oftentimes, fields that we want to validate as numbers are passed as strings. +Calling `autoParse` will correct this before validation. + +```dart +main() { + var parsed = autoParse({ + 'age': '34', + 'weight': '135.6' + }, ['age', 'weight']); + + validator.enforce(parsed); +} +``` + +You can also call `checkParsed` or `enforceParsed` as a shorthand. + # Extending Validators You can add situation-specific rules within a child validator. -You can also use `extend` to mark fields as required that originally -were not. Default value extension is also supported. +You can also use `extend` to mark fields as required or forbidden that originally +were not. Default value and custom error message extension is also supported. ```dart final Validator userValidator = new Validator({ @@ -170,12 +206,12 @@ register(Map userData) { This library includes some `Matcher`s for common validations, including: -* `isAlphaDash`: Asserts a `String` matches the Regular Expression ```/^[A-Za-z0-9_-]$/```. -* `isAlphaNum`: Asserts a `String` matches the Regular Expression ```/^[A-Za-z0-9]$/``` +* `isAlphaDash`: Asserts that a `String` is alphanumeric, but also lets it contain dashes or underscores. +* `isAlphaNum`: Asserts that a `String` is alphanumeric. * `isBool`: Asserts that a value either equals `true` or `false`. -* `isEmail`: Asserts a `String` complies to the RFC 5322 e-mail standard. -* `isInt`: Asserts a value is an `int`. -* `isNum`: Asserts a value is a `num`. +* `isEmail`: Asserts that a `String` complies to the RFC 5322 e-mail standard. +* `isInt`: Asserts that a value is an `int`. +* `isNum`: Asserts that a value is a `num`. * `isString`: Asserts that a value is a `String`. The remaining functionality is @@ -218,10 +254,12 @@ main() { # Use with Angel -`server.dart` exposes three helper middleware: +`server.dart` exposes five helper middleware: * `validate(validator)`: Validates and filters `req.body`, and throws an `AngelHttpException.BadRequest` if data is invalid. * `validateEvent(validator)`: Sets `e.data` to the result of validation on a service event. -* `validateQuery(validator)`: Same as `validate`, but operates `req.query`. +* `validateQuery(validator)`: Same as `validate`, but operates on `req.query`. +* `autoParseBody(fields)`: Auto-parses numbers in `req.body`. +* `autoParseQuery(fields)`: Same as `autoParseBody`, but operates on `req.query`. ```dart import 'package:angel_framework/angel_framework.dart'; diff --git a/lib/server.dart b/lib/server.dart index 02556fd9..397b4b12 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -5,6 +5,22 @@ import 'package:angel_framework/angel_framework.dart'; import 'angel_validate.dart'; export 'angel_validate.dart'; +/// Auto-parses numbers in `req.body`. +RequestMiddleware autoParseBody(List fields) { + return (RequestContext req, res) async { + req.body.addAll(autoParse(req.body, fields)); + return true; + }; +} + +/// Auto-parses numbers in `req.query`. +RequestMiddleware autoParseQuery(List fields) { + return (RequestContext req, res) async { + req.query.addAll(autoParse(req.query, fields)); + return true; + }; +} + /// Validates the data in `req.body`, and sets the body to /// filtered data before continuing the response. RequestMiddleware validate(Validator validator, diff --git a/lib/src/matchers.dart b/lib/src/matchers.dart index 8e1bfebe..f4bd6a8d 100644 --- a/lib/src/matchers.dart +++ b/lib/src/matchers.dart @@ -1,13 +1,31 @@ import 'package:matcher/matcher.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( + 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])?$"); + +/// 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 String, 'a bool '); +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 '); /// Asserts that a value is an `int`. -final Matcher isInt = predicate((value) => value is String, 'an integer '); +final Matcher isInt = predicate((value) => value is int, 'an integer '); /// Asserts that a value is a `num`. -final Matcher isNumber = predicate((value) => value is String, 'a number '); +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 '); diff --git a/lib/src/validator.dart b/lib/src/validator.dart index 390e316d..54a35e56 100644 --- a/lib/src/validator.dart +++ b/lib/src/validator.dart @@ -1,6 +1,7 @@ import 'package:matcher/matcher.dart'; final RegExp _asterisk = new RegExp(r'\*$'); +final RegExp _forbidden = new RegExp(r'\!$'); final RegExp _optional = new RegExp(r'\?$'); /// Returns a value based the result of a computation. @@ -9,11 +10,37 @@ typedef DefaultValueFunction(); /// Determines if a value is valid. typedef bool Filter(value); +/// Converts the desired fields to their numeric representations, if present. +Map autoParse(Map inputData, List fields) { + Map data = {}; + + for (var key in inputData.keys) { + if (!fields.contains(key)) { + data[key] = inputData[key]; + } else { + try { + var n = num.parse(inputData[key].toString()); + data[key] = n == n.toInt() ? n.toInt() : n; + } catch (e) { + // Invalid number, don't pass it + } + } + } + + return data; +} + /// Enforces the validity of input data, according to [Matcher]s. class Validator extends Matcher { + /// Pre-defined error messages for certain fields. + final Map customErrorMessages = {}; + /// Values that will be filled for fields if they are not present. final Map defaultValues = {}; + /// Fields that cannot be present in valid data. + final List forbiddenFields = []; + /// Conditions that must be met for input data to be considered valid. final Map> rules = {}; @@ -22,10 +49,16 @@ class Validator extends Matcher { void _importSchema(Map schema) { for (var key in schema.keys) { - var fieldName = key.replaceAll(_asterisk, ''); - var isRequired = _asterisk.hasMatch(key); + var fieldName = key + .replaceAll(_asterisk, '') + .replaceAll(_forbidden, '') + .replaceAll(_optional, ''); + var isForbidden = _forbidden.hasMatch(key), + isRequired = _asterisk.hasMatch(key); - if (isRequired) { + if (isForbidden) { + forbiddenFields.add(fieldName); + } else if (isRequired) { requiredFields.add(fieldName); } @@ -47,8 +80,10 @@ class Validator extends Matcher { Validator.empty(); Validator(Map schema, - {Map defaultValues: const {}}) { + {Map defaultValues: const {}, + Map customErrorMessages: const {}}) { this.defaultValues.addAll(defaultValues ?? {}); + this.customErrorMessages.addAll(customErrorMessages ?? {}); _importSchema(schema); } @@ -65,9 +100,21 @@ class Validator extends Matcher { } } + for (String field in forbiddenFields) { + if (input.containsKey(field)) { + if (!customErrorMessages.containsKey(field)) + errors.add("'$field' is forbidden."); + else + errors.add(customErrorMessages[field]); + } + } + for (String field in requiredFields) { if (!input.containsKey(field)) { - errors.add("'$field' is required."); + if (!customErrorMessages.containsKey(field)) + errors.add("'$field' is required."); + else + errors.add(customErrorMessages[field]); } } @@ -88,7 +135,8 @@ class Validator extends Matcher { } } else { if (!matcher.matches(value, {})) { - errors.add(matcher.describe(description).toString().trim()); + if (!customErrorMessages.containsKey(key)) + errors.add(matcher.describe(description).toString().trim()); valid = false; } } @@ -100,6 +148,8 @@ class Validator extends Matcher { if (valid) { data[key] = value; + } else if (customErrorMessages.containsKey(key)) { + errors.add(customErrorMessages[key]); } } } @@ -111,6 +161,10 @@ class Validator extends Matcher { return new ValidationResult().._data = data; } + /// Validates, and filters input data after running [autoParse]. + ValidationResult checkParsed(Map inputData, List fields) => + check(autoParse(inputData, fields)); + /// Validates input data, and throws an error if it is invalid. /// /// Otherwise, the filtered data is returned. @@ -127,40 +181,46 @@ class Validator extends Matcher { /// Creates a copy with additional validation rules. Validator extend(Map schema, - {Map defaultValues: const {}, bool overwrite: false}) { + {Map defaultValues: const {}, + Map customErrorMessages: const {}, + bool overwrite: false}) { Map _schema = {}; var child = new Validator.empty() ..defaultValues.addAll(this.defaultValues) ..defaultValues.addAll(defaultValues ?? {}) + ..customErrorMessages.addAll(this.customErrorMessages) + ..customErrorMessages.addAll(customErrorMessages ?? {}) ..requiredFields.addAll(requiredFields) ..rules.addAll(rules); - if (overwrite) { - for (var key in schema.keys) { - var fieldName = key.replaceAll(_asterisk, '').replaceAll(_optional, ''); - var isOptional = _optional.hasMatch(key); - var isRequired = _asterisk.hasMatch(key); + 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 (isOptional) - child.requiredFields.remove(fieldName); - else if (isRequired) child.requiredFields.add(fieldName); - - if (child.rules.containsKey(key)) child.rules.remove(key); - - _schema[fieldName] = schema[key]; + 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); } - } else { - for (var key in schema.keys) { - var fieldName = key.replaceAll(_asterisk, '').replaceAll(_optional, ''); - var isOptional = _optional.hasMatch(key); - var isRequired = _asterisk.hasMatch(key); - if (isOptional) - child.requiredFields.remove(fieldName); - else if (isRequired) child.requiredFields.add(fieldName); - - _schema[fieldName] = schema[key]; + if (overwrite) { + if (child.rules.containsKey(fieldName)) child.rules.remove(fieldName); } + + _schema[fieldName] = schema[key]; } return child.._importSchema(_schema); diff --git a/pubspec.yaml b/pubspec.yaml index 46c7603f..05d8942b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: angel_validate description: Cross-platform validation library based on `matcher`. -version: 0.0.0 +version: 0.0.1 author: Tobe O homepage: https://github.com/angel-dart/validate environment: diff --git a/test/basic_test.dart b/test/basic_test.dart index e3384d19..d7f5ec02 100644 --- a/test/basic_test.dart +++ b/test/basic_test.dart @@ -1,6 +1,12 @@ import 'package:angel_validate/angel_validate.dart'; import 'package:test/test.dart'; +final Validator emailSchema = new Validator({ + 'to': [isNum, isPositive] +}, customErrorMessages: { + 'to': 'Hello, world!' +}); + final Validator todoSchema = new Validator({ 'id': [isInt, isPositive], 'text*': isString, @@ -10,6 +16,14 @@ final Validator todoSchema = new Validator({ }); 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('todo', () { expect(() { todoSchema