New readme
This commit is contained in:
parent
2023cf221d
commit
7420a9e090
3 changed files with 51 additions and 283 deletions
322
README.md
322
README.md
|
@ -2,225 +2,67 @@
|
||||||
[![Pub](https://img.shields.io/pub/v/angel_validate.svg)](https://pub.dartlang.org/packages/angel_validate)
|
[![Pub](https://img.shields.io/pub/v/angel_validate.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)
|
[![build status](https://travis-ci.org/angel-dart/validate.svg)](https://travis-ci.org/angel-dart/validate)
|
||||||
|
|
||||||
[Live Example](https://angel-dart.github.io/validate)
|
Strongly-typed form handlers and validators for Angel.
|
||||||
|
|
||||||
Validation library based on the `matcher` library, with Angel support.
|
Validation library based on the `matcher` library, with Angel support.
|
||||||
Why re-invent the wheel, when you can use the same validators you already
|
Why re-invent the wheel, when you can use the same validators you already
|
||||||
use for tests?
|
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.
|
|
||||||
|
|
||||||
For convenience's sake, this library also exports `matcher`.
|
For convenience's sake, this library also exports `matcher`.
|
||||||
|
|
||||||
* [Examples](#examples)
|
# Field
|
||||||
* [Creating a Validator](#creating-a-validator)
|
The basic unit is the `Field` class, which is a type-safe way to read
|
||||||
* [Validating Data](#validating-data)
|
values from a `RequestContext`. Here is a simple example of using a
|
||||||
* [Required Fields](#required-fields)
|
`TextField` instance to read a value from the URL query parameters:
|
||||||
* [Forbidden Fields](#forbidden-fields)
|
|
||||||
* [Default Values](#default-values)
|
|
||||||
* [Custom Validator Functions](#custom-validator-functions)
|
|
||||||
* [Auto-parsing Numbers](#autoparse)
|
|
||||||
* [Filtering Maps](#filter)
|
|
||||||
* [Custom Error Messages](#custom-error-messages)
|
|
||||||
* [Extending Validators](#extending-validators)
|
|
||||||
* [Bundled Matchers](#bundled-matchers)
|
|
||||||
* [Nested Validators](#nested-validators)
|
|
||||||
* [Use with Angel](#use-with-angel)
|
|
||||||
|
|
||||||
# Examples
|
|
||||||
|
|
||||||
## Creating a Validator
|
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
import 'package:angel_validate/angel_validate.dart';
|
app.get('/hello', (req, res) async {
|
||||||
|
var nameField = TextField('name');
|
||||||
main() {
|
var name = await nameField.getValue(req, query: true); // String
|
||||||
var validator = Validator({
|
return 'Hello, $name!';
|
||||||
'username': isAlphaNum,
|
|
||||||
'multiple,keys,with,same,rules': [isString, isNotEmpty],
|
|
||||||
'balance': [
|
|
||||||
greaterThanOrEqualTo(0),
|
|
||||||
lessThan(1000000)
|
|
||||||
],
|
|
||||||
'nested': [
|
|
||||||
foo,
|
|
||||||
[bar, baz]
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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 = Validator({
|
|
||||||
'googleId*': isString,
|
|
||||||
|
|
||||||
// You can also use `requireField`
|
|
||||||
requireField('googleId'): isString,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Forbidden Fields
|
|
||||||
To prevent a field from showing up in valid data, suffix it
|
|
||||||
with a `'!'`.
|
|
||||||
|
|
||||||
|
|
||||||
## 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 = Validator({
|
|
||||||
'text*': isString,
|
|
||||||
'completed*': isBool
|
|
||||||
}, defaultValues: {
|
|
||||||
'completed': false
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Default values can also be parameterless, *synchronous* functions
|
There are several included field types:
|
||||||
that return a single value.
|
* `TextField`
|
||||||
|
* `BoolField`
|
||||||
|
* `NumField`
|
||||||
|
* `DoubleField`
|
||||||
|
* `IntField`
|
||||||
|
* `DateTimeField`
|
||||||
|
* `FileField`
|
||||||
|
* `ImageField`
|
||||||
|
|
||||||
## Custom Validator Functions
|
# Forms
|
||||||
Creating a whole `Matcher` class is sometimes cumbersome, but if
|
The `Form` class lets you combine `Field` instances, and decode
|
||||||
you pass a function to the constructor, it will be wrapped in a
|
request bodies into `Map<String, dynamic>`. Unrecognized fields are
|
||||||
`Matcher` instance.
|
stripped out of the body, so a `Form` is effectively a whitelist.
|
||||||
|
|
||||||
(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
|
```dart
|
||||||
main() {
|
var todoForm = Form(fields: [
|
||||||
var validator = Validator({
|
TextField('text'),
|
||||||
'key*': (key) {
|
BoolField('is_complete'),
|
||||||
var file = File('whitelist.txt');
|
]);
|
||||||
return file.readFileSync().contains(key);
|
|
||||||
}
|
// Validate a request body, and deserialize it immediately.
|
||||||
});
|
var todo = await todoForm.deserialize(req, TodoSerializer.fromMap);
|
||||||
|
|
||||||
|
// Same as above, but with a Codec<Todo, Map> (i.e. via `angel_serialize`).
|
||||||
|
var todo = await todoForm.decode(req, todoSerializer);
|
||||||
|
|
||||||
|
// Lower-level functionality, typically not called directly.
|
||||||
|
// Use it if you want to handle validation errors directly, without
|
||||||
|
// throwing exceptions.
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
class _Todo {
|
||||||
|
String text;
|
||||||
|
bool isComplete;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
# Custom Error Messages
|
## Form Rendering
|
||||||
If these are not present, `angel_validate` will *attempt* to generate
|
TODO: Docs about this
|
||||||
a coherent error message on its own.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
Validator({
|
|
||||||
'age': [greaterThanOrEqualTo(18)]
|
|
||||||
}, customErrorMessages: {
|
|
||||||
'age': 'You must be an adult to see this page.'
|
|
||||||
});
|
|
||||||
```
|
|
||||||
The string `{{value}}` will be replaced inside your error message automatically.
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
# filter
|
|
||||||
This is a helper function to extract only the desired keys from a `Map`.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
var inputData = {'foo': 'bar', 'a': 'b', '1': 2};
|
|
||||||
var only = filter(inputData, ['foo']);
|
|
||||||
|
|
||||||
print(only); // { foo: bar }
|
|
||||||
```
|
|
||||||
|
|
||||||
# Extending Validators
|
|
||||||
You can add situation-specific rules within a child validator.
|
|
||||||
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 = 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, validation rules are simply appended 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
|
# Bundled Matchers
|
||||||
This library includes some `Matcher`s for common validations,
|
This library includes some `Matcher`s for common validations,
|
||||||
|
@ -237,80 +79,4 @@ including:
|
||||||
* `isUrl`: Asserts that a `String` is an HTTPS or HTTP URL.
|
* `isUrl`: Asserts that a `String` is an HTTPS or HTTP URL.
|
||||||
|
|
||||||
The remaining functionality is
|
The remaining functionality is
|
||||||
[effectively implemented by the `matcher` package](https://www.dartdocs.org/documentation/matcher/0.12.0%2B2/matcher/matcher-library.html).
|
[effectively implemented by the `matcher` package](https://www.dartdocs.org/documentation/matcher/latest/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, because it extends the
|
|
||||||
`Matcher` class.
|
|
||||||
|
|
||||||
```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;
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
var author = Validator({
|
|
||||||
'bio*': bio,
|
|
||||||
'books*': [
|
|
||||||
isList,
|
|
||||||
everyElement(book)
|
|
||||||
]
|
|
||||||
}, defaultValues: {
|
|
||||||
'books': []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Use with Angel
|
|
||||||
|
|
||||||
`server.dart` exposes seven helper middleware:
|
|
||||||
* `validate(validator)`: Validates and filters `req.bodyAsMap`, 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 on `req.query`.
|
|
||||||
* `autoParseBody(fields)`: Auto-parses numbers in `req.bodyAsMap`.
|
|
||||||
* `autoParseQuery(fields)`: Same as `autoParseBody`, but operates on `req.query`.
|
|
||||||
* `filterBody(only)`: Filters unwanted data out of `req.bodyAsMap`.
|
|
||||||
* `filterQuery(only)`: Same as `filterBody`, but operates on `req.query`.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'package:angel_validate/server.dart';
|
|
||||||
|
|
||||||
final Validator echo = Validator({
|
|
||||||
'message*': (String message) => message.length >= 5
|
|
||||||
});
|
|
||||||
|
|
||||||
final Validator todo = Validator({
|
|
||||||
'text*': isString,
|
|
||||||
'completed*': isBool
|
|
||||||
}, defaultValues: {
|
|
||||||
'completed': false
|
|
||||||
});
|
|
||||||
|
|
||||||
main() async {
|
|
||||||
var app = Angel();
|
|
||||||
|
|
||||||
app.chain([validate(echo)]).post('/echo', (req, res) async {
|
|
||||||
res.write('You said: "${req.bodyAsMap["message"]}"');
|
|
||||||
});
|
|
||||||
|
|
||||||
app.service('api/todos')
|
|
||||||
..beforeCreated.listen(validateEvent(todo))
|
|
||||||
..beforeUpdated.listen(validateEvent(todo));
|
|
||||||
|
|
||||||
await app.startServer();
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,5 +1,2 @@
|
||||||
export 'src/common_fields.dart';
|
export 'package:matcher/matcher.dart';
|
||||||
export 'src/field.dart';
|
export 'without_matcher.dart';
|
||||||
export 'src/form.dart';
|
|
||||||
export 'src/form_renderer.dart';
|
|
||||||
export 'src/matchers.dart';
|
|
||||||
|
|
5
lib/without_matcher.dart
Normal file
5
lib/without_matcher.dart
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export 'src/common_fields.dart';
|
||||||
|
export 'src/field.dart';
|
||||||
|
export 'src/form.dart';
|
||||||
|
export 'src/form_renderer.dart';
|
||||||
|
export 'src/matchers.dart';
|
Loading…
Reference in a new issue