diff --git a/.analysis-options b/.analysis-options new file mode 100644 index 00000000..7b8e6002 --- /dev/null +++ b/.analysis-options @@ -0,0 +1,3 @@ +analyzer: + strong-mode: true + exclude: ./scripts-bin/**/*.dart \ No newline at end of file diff --git a/README.md b/README.md index ac37cbb3..f8112fd3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,244 @@ # validate -Request validation middleware based on the `matcher` library. +[![version 0.0.0](https://img.shields.io/badge/pub-v0.0.0-red.svg)](https://pub.dartlang.org/packages/angel_validate) +[![build status](https://travis-ci.org/angel-dart/validate.svg)](https://travis-ci.org/angel-dart/validate) + +Validation library based on the `matcher` library, with Angel support. +Why re-invent the wheel, when you can use the same validators you already +use for tests? + +This library runs both on the server, and on the client. Thus, you can use +the same validation rules for forms on the server, and on the frontend. + +# Examples + +## Creating a Validator + +```dart +import 'package:angel_validate/angel_validate.dart'; + +main() { + var validator = new Validator({ + 'username': isAlphaNum, + 'balance': [ + greaterThanOrEqualTo(0), + lessThan(1000000) + ] + }); +} +``` + +## Validating data + +The `Validator` will filter out fields that have no validation rules. +You can rest easy knowing that attackers cannot slip extra data into +your applications. + +```dart +main() { + var result = validator.check(formData); + + if (!result.errors.isNotEmpty) { + // Invalid data + } else { + // Safely handle filtered data + return someSecureOperation(result.data); + } +} +``` + +You can `enforce` validation rules, and throw an error if validation fails. + +```dart +main() { + try { + // `enforce` will return the filtered data. + var safeData = validator.enforce(formData); + } on ValidationException catch(e) { + print(e.errors); + } +} +``` + +## Required Fields +Fields are optional by default. + +Suffix a field name with a `'*'` to mark it as required, and +to throw an error if it is not present. + +```dart +main() { + var validator = new Validator({ + 'googleId*': isString + }); +} +``` + + +## Default values + +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({ + 'text*': isString, + 'completed*': isBool +}, defaultValues: { + 'completed': false +}); +``` + +Default values can also be parameterless, *synchronous* functions +that return a single value. + +## Custom Validator Functions +Creating a whole `Matcher` class is sometimes cumbersome, but if +you pass a function to the constructor, it will be wrapped in a +`Matcher` instance. + +(It simply returns the value of calling +[`predicate`](https://www.dartdocs.org/documentation/matcher/0.12.0%2B2/matcher/predicate.html).) + +The function must *synchronously* return a `bool`. + +```dart +main() { + var validator = new Validator({ + 'key*': (key) { + var file = new File('whitelist.txt'); + return file.readFileSync().contains(key); + } + }); +} +``` + +# 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. + +```dart +final Validator userValidator = new Validator({ + 'username': isString, + 'age': [ + isNum, + greaterThanOrEqualTo(18) + ] +}); +``` + +To mark a field as now optional, and no longer required, +suffix its name with a `'?'`. + +```dart +var ageIsOptional = userValidator.extend({ + 'age?': [ + isNum, + greaterThanOrEqualTo(13) + ] +}); +``` + +Note that by default, new validation rules are simply prepended to +the existing list. To completely overwrite existing rules, set the +`overwrite` flag to `true`. + +```dart +register(Map userData) { + var teenUser = userValidator.extend({ + 'age': lessThan(18) + }, overwrite: true); +} +``` + +# Bundled Matchers +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]$/``` +* `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`. +* `isNegative`: Asserts a `num` is less than `0`. +* `isNum`: Asserts a value is a `num`. +* `isPositive`: Asserts a `num` is greater than `0`. +* `isString`: Asserts that a value is a `String`. + +The remaining functionality is +[effectively implemented by the `matcher` package](https://www.dartdocs.org/documentation/matcher/0.12.0%2B2/matcher/matcher-library.html). + +# Nested Validators +Very often, the data we validate contains other data within. You can pass +a `Validator` instance to the constructor, and it will be wrapped within +a `Matcher` instance. + +The class also exposes a `toMatcher()` method that creates a Matcher that +validates data using the instance. + +```dart +main() { + var bio = new Validator({ + 'age*': [isInteger, greaterThanOrEqualTo(0)], + 'birthYear*': isInteger, + 'countryOfOrigin': isString + }); + + var book = new Validator({ + 'title*': isString, + 'year*': [ + isNum, + (year) { + return year <= new DateTime.now().year; + } + ] + }); + + var author = new Validator({ + 'bio*': bio, + 'books*': [ + isList, + everyElement(book.toMatcher()) + ] + }, defaultValues: { + 'books': [] + }); +} +``` + +# Use with Angel + +`server.dart` exposes three 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`. + +```dart +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_validate/server.dart'; + +final Validator echo = new Validator({ + 'message*': (String message) => message.length >= 5 +}); + +final Validator todo = new Validator({ + 'text*': isString, + 'completed*': isBool +}, defaultValues: { + 'completed': false +}); + +main() async { + var app = new Angel(); + + app.chain(validate(echo)).post('/echo', (req, res) async { + res.write('You said: "${req.body["message"]}"'); + }); + + app.service('api/todos') + ..beforeCreated.listen(validateEvent(todo)) + ..beforeUpdated.listen(validateEvent(todo)); + + await app.startServer(); +} +``` \ No newline at end of file diff --git a/lib/angel_validate.dart b/lib/angel_validate.dart new file mode 100644 index 00000000..28b741ba --- /dev/null +++ b/lib/angel_validate.dart @@ -0,0 +1,4 @@ +/// Cross-platform validation library based on `matcher`. +library angel_validate; + +export 'src/validator.dart'; \ No newline at end of file diff --git a/lib/server.dart b/lib/server.dart new file mode 100644 index 00000000..7a2ce194 --- /dev/null +++ b/lib/server.dart @@ -0,0 +1,25 @@ +/// Support for using `angel_validate` with the Angel Framework. +library angel_validate.server; + +import 'package:angel_framework/angel_framework.dart'; +import 'angel_validate.dart'; +export 'angel_validate.dart'; + +/// Validates the data in `req.body`, and sets the body to +/// filtered data before continuing the response. +RequestMiddleware validate(Validator validator, {String errorMessage}) { + +} + +/// Validates the data in `req.body`, and sets the query to +/// filtered data before continuing the response. +RequestMiddleware validateQuery(Validator validator, {String errorMessage}) { + +} + +/// Validates the data in `e.data`, and sets the data to +/// filtered data before continuing the service event. +HookedServiceEventListener validateEvent(Validator validator) { + +} + diff --git a/lib/src/validator.dart b/lib/src/validator.dart new file mode 100644 index 00000000..137b6ccd --- /dev/null +++ b/lib/src/validator.dart @@ -0,0 +1,53 @@ +/// Enforces the validity of input data, according to [Matcher]s. +class Validator { + /// Validates, and filters input data. + ValidationResult check(Map inputData) {} + + /// 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.'}) {} +} + +/// The result of attempting to validate input data. +class ValidationResult { + Map _data; + final List _errors = []; + + /// The successfully validated data, filtered from the original input. + Map get data => _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); +} + +/// Occurs when user-provided data is invalid. +class ValidationException { + /// A list of errors that resulted in the given data being marked invalid. + final List errors = []; + + /// A descriptive message describing the error. + final String message; + + ValidationException(this.message, {List errors: const []}) { + if (errors != null) this.errors.addAll(errors); + } + + @override + String get 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'); + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..05701825 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,12 @@ +name: angel_validate +description: Cross-platform validation library based on `matcher`. +version: 0.0.0 +author: Tobe O +homepage: https://github.com/angel-dart/validate +environment: + sdk: ">=1.19.0" +dependencies: + angel_framework: ^1.0.0-dev + matcher: ^0.12.0 +dev_dependencies: + test: ^0.12.18 \ No newline at end of file