add: adding validate package
This commit is contained in:
parent
0f70a5bc8e
commit
7a680c5017
21 changed files with 1822 additions and 0 deletions
71
packages/validate/.gitignore
vendored
Normal file
71
packages/validate/.gitignore
vendored
Normal file
|
@ -0,0 +1,71 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
||||
|
||||
### Dart template
|
||||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
|
||||
# SDK 1.20 and later (no longer creates packages directories)
|
||||
|
||||
# Older SDK versions
|
||||
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
|
||||
.project
|
||||
.buildlog
|
||||
**/packages/
|
||||
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
|
||||
# differentiate from explicit Javascript files)
|
||||
*.dart.js
|
||||
*.part.js
|
||||
*.js.deps
|
||||
*.js.map
|
||||
*.info.json
|
||||
|
||||
# Directory created by dartdoc
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
|
||||
## VsCode
|
||||
.vscode/
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
.idea/
|
||||
/out/
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
12
packages/validate/AUTHORS.md
Normal file
12
packages/validate/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
97
packages/validate/CHANGELOG.md
Normal file
97
packages/validate/CHANGELOG.md
Normal file
|
@ -0,0 +1,97 @@
|
|||
# Change Log
|
||||
|
||||
## 8.2.0
|
||||
|
||||
* Require Dart >= 3.3
|
||||
* Updated `lints` to 4.0.0
|
||||
|
||||
## 8.1.1
|
||||
|
||||
* Updated repository link
|
||||
|
||||
## 8.1.0
|
||||
|
||||
* Updated `lints` to 3.0.0
|
||||
|
||||
## 8.0.2
|
||||
|
||||
* Fixed incorrect mismatch message handling
|
||||
|
||||
## 8.0.1
|
||||
|
||||
* Fixed missing mismatch errors
|
||||
|
||||
## 8.0.0
|
||||
|
||||
* Require Dart >= 3.0
|
||||
|
||||
## 7.0.1
|
||||
|
||||
* Fixed linter warnings
|
||||
|
||||
## 7.0.0
|
||||
|
||||
* Require Dart >= 2.17
|
||||
|
||||
## 6.0.0
|
||||
|
||||
* Require Dart >= 2.16
|
||||
|
||||
## 5.0.0
|
||||
|
||||
* Skipped release
|
||||
|
||||
## 4.1.0
|
||||
|
||||
* Updated linter to `package:lints`
|
||||
* Updated `build_runner` to major version 2.1.0
|
||||
* Updated `build_web_compilers` to major version 3.2.0
|
||||
|
||||
## 4.0.2
|
||||
|
||||
* Updated broken links in README
|
||||
* All 7 unit test passed
|
||||
|
||||
## 4.0.1
|
||||
|
||||
* Updated README
|
||||
|
||||
## 4.0.0
|
||||
|
||||
* Migrated to support Dart >= 2.12 NNBD
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Migrated to work with Dart >= 2.12 Non NNBD
|
||||
|
||||
## 2.0.2
|
||||
|
||||
* Deduplicate error messages.
|
||||
|
||||
## 2.0.1+1
|
||||
|
||||
* Fix bug in the implementation of `maxLength`.
|
||||
|
||||
## 2.0.1
|
||||
|
||||
* Patch for updated body parsing.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Finish update for Angel 2.
|
||||
|
||||
## 2.0.0-alpha.1
|
||||
|
||||
* Update for Angel 2.
|
||||
|
||||
## 1.0.5-beta
|
||||
|
||||
* Use `wrapMatcher` on explicit values instead of throwing.
|
||||
* Add async matchers.
|
||||
* Add context-aware matchers.
|
||||
|
||||
## 1.0.4
|
||||
|
||||
* `isNonEmptyString` trims strings.
|
||||
* `ValidationException` extends `AngelHttpException`.
|
||||
* Added `requireField` and `requireFields`.
|
29
packages/validate/LICENSE
Normal file
29
packages/validate/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2021, dukefirehawk.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
299
packages/validate/README.md
Normal file
299
packages/validate/README.md
Normal file
|
@ -0,0 +1,299 @@
|
|||
# Angel3 Validate
|
||||
|
||||
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/angel3_validate?include_prereleases)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![Discord](https://img.shields.io/discord/1060322353214660698)](https://discord.gg/3X6bxTUdCM)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/angel)](https://github.com/dart-backend/angel/tree/master/packages/validate/LICENSE)
|
||||
|
||||
This validator library is based on the `matcher` library and comes with build in support for Angel3 framework. It can be run on both server and client side. Thus, the same validation rules apply to forms on both backend and frontend code.
|
||||
|
||||
For convenience's sake, this library also exports `matcher`.
|
||||
|
||||
- [Angel3 Validate](#angel3-validate)
|
||||
- [Examples](#examples)
|
||||
- [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)
|
||||
- [Custom Error Messages](#custom-error-messages)
|
||||
- [autoParse](#autoparse)
|
||||
- [filter](#filter)
|
||||
- [Extending Validators](#extending-validators)
|
||||
- [Bundled Matchers](#bundled-matchers)
|
||||
- [Nested Validators](#nested-validators)
|
||||
- [Use with Angel3](#use-with-angel3)
|
||||
|
||||
## Examples
|
||||
|
||||
### Creating a Validator
|
||||
|
||||
```dart
|
||||
import 'package:angel3_validate/angel3_validate.dart';
|
||||
|
||||
main() {
|
||||
var validator = Validator({
|
||||
'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 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://pub.dev/documentation/matcher/latest/matcher/predicate.html).)
|
||||
|
||||
The function must *synchronously* return a `bool`.
|
||||
|
||||
```dart
|
||||
main() {
|
||||
var validator = Validator({
|
||||
'key*': (key) {
|
||||
var file = File('whitelist.txt');
|
||||
return file.readFileSync().contains(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Error Messages
|
||||
|
||||
If these are not present, `angel3_validate` will *attempt* to generate 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
|
||||
|
||||
This library includes some `Matcher`s for common validations, including:
|
||||
|
||||
- `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 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`.
|
||||
- `isNonEmptyString`: Asserts that a value is a non-empty `String`.
|
||||
- `isUrl`: Asserts that a `String` is an HTTPS or HTTP URL.
|
||||
|
||||
The remaining functionality is [effectively implemented by the `matcher` package](https://pub.dev/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 Angel3
|
||||
|
||||
`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:angel3_framework/angel3_framework.dart';
|
||||
import 'package:angel3_validate/server.dart';
|
||||
|
||||
final Validator echo = Validator({
|
||||
'message*': (String message) => message.length >= 5
|
||||
});
|
||||
|
||||
final Validator todo = Validator({
|
||||
'text*': isString,
|
||||
'completed*': isBool
|
||||
}, defaultValues: {
|
||||
'completed': false
|
||||
});
|
||||
|
||||
void 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
packages/validate/analysis_options.yaml
Normal file
1
packages/validate/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
27
packages/validate/example/main.dart
Normal file
27
packages/validate/example/main.dart
Normal file
|
@ -0,0 +1,27 @@
|
|||
import 'package:angel3_validate/angel3_validate.dart';
|
||||
|
||||
void 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': []
|
||||
});
|
||||
}
|
14
packages/validate/lib/angel3_validate.dart
Normal file
14
packages/validate/lib/angel3_validate.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
/// Cross-platform validation library based on `matcher`.
|
||||
library angel3_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(', ');
|
148
packages/validate/lib/server.dart
Normal file
148
packages/validate/lib/server.dart
Normal file
|
@ -0,0 +1,148 @@
|
|||
/// Support for using `angel_validate` with the Angel Framework.
|
||||
library angel3_validate.server;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:platform_foundation/core.dart';
|
||||
import 'src/async.dart';
|
||||
import 'angel3_validate.dart';
|
||||
export 'src/async.dart';
|
||||
export 'angel3_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 app = req.app;
|
||||
if (app != null) {
|
||||
var result = await asyncApplyValidator(validator, req.bodyAsMap, app);
|
||||
|
||||
if (result.errors.isNotEmpty) {
|
||||
throw PlatformHttpException.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 app = req.app;
|
||||
if (app != null) {
|
||||
var result =
|
||||
await asyncApplyValidator(validator, req.queryParameters, app);
|
||||
|
||||
if (result.errors.isNotEmpty) {
|
||||
throw PlatformHttpException.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 app = e.request?.app ?? e.service.app;
|
||||
var result = await asyncApplyValidator(validator, e.data as Map, app);
|
||||
|
||||
if (result.errors.isNotEmpty) {
|
||||
throw PlatformHttpException.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, Application 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);
|
||||
}
|
159
packages/validate/lib/src/async.dart
Normal file
159
packages/validate/lib/src/async.dart
Normal file
|
@ -0,0 +1,159 @@
|
|||
import 'dart:async';
|
||||
import 'package:platform_foundation/core.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, Application) 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, Application) f,
|
||||
[String description = 'satisfies asynchronously created matcher']) =>
|
||||
_MatchWithAngel(f, description);
|
||||
|
||||
/// Calls [matchWithAngel] without the initial parameter.
|
||||
AngelMatcher matchWithAngelBinary(
|
||||
FutureOr<Matcher> Function(Map context, Application) 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(Application) 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 PlatformHttpException 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, Application 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, Application app) async {
|
||||
return matcher.matchesWithContext(item, key, context, matchState);
|
||||
}
|
||||
}
|
||||
|
||||
class _MatchWithAngel extends AngelMatcher {
|
||||
final FutureOr<Matcher> Function(Object, Map, Application) f;
|
||||
final String description;
|
||||
|
||||
_MatchWithAngel(this.f, this.description);
|
||||
|
||||
@override
|
||||
Description describe(Description description) =>
|
||||
description.add(this.description);
|
||||
|
||||
@override
|
||||
Future<bool> matchesWithAngel(
|
||||
item, String key, Map context, Map matchState, Application app) {
|
||||
return Future.sync(() => f(item as Object, context, app)).then((result) {
|
||||
return result.matches(item, matchState);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _PredicateWithAngel extends AngelMatcher {
|
||||
final FutureOr<bool> Function(String, Object, Application) predicate;
|
||||
final String description;
|
||||
|
||||
_PredicateWithAngel(this.predicate, this.description);
|
||||
|
||||
@override
|
||||
Description describe(Description description) =>
|
||||
description.add(this.description);
|
||||
|
||||
@override
|
||||
Future<bool> matchesWithAngel(
|
||||
item, String key, Map context, Map matchState, Application app) {
|
||||
return Future<bool>.sync(() => predicate(key, item as Object, 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) =>
|
||||
description.add(this.description);
|
||||
|
||||
@override
|
||||
Future<bool> matchesWithAngel(
|
||||
item, String key, Map context, Map matchState, Application app) async {
|
||||
var f = await feature();
|
||||
var m = await matcher(key, f as Object);
|
||||
var c = wrapAngelMatcher(m);
|
||||
return await c.matchesWithAngel(item, key, context, matchState, app);
|
||||
}
|
||||
}
|
54
packages/validate/lib/src/context_aware.dart
Normal file
54
packages/validate/lib/src/context_aware.dart
Normal file
|
@ -0,0 +1,54 @@
|
|||
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) => description.add(desc);
|
||||
|
||||
@override
|
||||
bool matchesWithContext(item, String key, Map context, Map matchState) =>
|
||||
f(item as Object, key, context, matchState);
|
||||
}
|
15
packages/validate/lib/src/context_validator.dart
Normal file
15
packages/validate/lib/src/context_validator.dart
Normal file
|
@ -0,0 +1,15 @@
|
|||
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;
|
||||
}
|
131
packages/validate/lib/src/matchers.dart
Normal file
131
packages/validate/lib/src/matchers.dart
Normal file
|
@ -0,0 +1,131 @@
|
|||
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(
|
||||
(dynamic 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(
|
||||
(dynamic value) => value is String && _alphaNum.hasMatch(value),
|
||||
'alphanumeric');
|
||||
|
||||
/// Asserts that a value either equals `true` or `false`.
|
||||
final Matcher isBool = predicate((dynamic value) => value is bool, 'a bool');
|
||||
|
||||
/// Asserts that a `String` complies to the RFC 5322 e-mail standard.
|
||||
final Matcher isEmail = predicate(
|
||||
(dynamic value) => value is String && _email.hasMatch(value),
|
||||
'a valid e-mail address');
|
||||
|
||||
/// Asserts that a value is an `int`.
|
||||
final Matcher isInt = predicate((dynamic value) => value is int, 'an integer');
|
||||
|
||||
/// Asserts that a value is a `num`.
|
||||
final Matcher isNum = predicate((dynamic value) => value is num, 'a number');
|
||||
|
||||
/// Asserts that a value is a `String`.
|
||||
final Matcher isString =
|
||||
predicate((dynamic value) => value is String, 'a string');
|
||||
|
||||
/// Asserts that a value is a non-empty `String`.
|
||||
final Matcher isNonEmptyString = predicate(
|
||||
(dynamic 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(
|
||||
(dynamic x) {
|
||||
try {
|
||||
return x is String;
|
||||
} 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(
|
||||
(dynamic value) => value is String && _url.hasMatch(value),
|
||||
'a valid url, starting with http:// or https://');
|
||||
|
||||
/// Enforces a minimum length on a string.
|
||||
Matcher minLength(int length) => predicate(
|
||||
(dynamic 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(
|
||||
(dynamic 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".'),
|
||||
);
|
||||
}
|
432
packages/validate/lib/src/validator.dart
Normal file
432
packages/validate/lib/src/validator.dart
Normal file
|
@ -0,0 +1,432 @@
|
|||
import 'package:platform_support/exceptions.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 = Function();
|
||||
|
||||
/// Generates an error message based on the given input.
|
||||
typedef CustomErrorMessageFunction = String Function(dynamic item);
|
||||
|
||||
/// Determines if a value is valid.
|
||||
typedef Filter = bool Function(dynamic value);
|
||||
|
||||
/// Converts the desired fields to their numeric representations, if present.
|
||||
Map<String, dynamic> autoParse(Map inputData, Iterable<String> fields) {
|
||||
var data = <String, dynamic>{};
|
||||
|
||||
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 = [];
|
||||
|
||||
/// Validation error messages.
|
||||
final List<String> errorMessages = [];
|
||||
|
||||
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 tmpIterable =
|
||||
schema[keys] is Iterable ? schema[keys] : [schema[keys]];
|
||||
var iterable = [];
|
||||
|
||||
void addTo(x) {
|
||||
if (x is Iterable) {
|
||||
x.forEach(addTo);
|
||||
} else {
|
||||
iterable.add(x);
|
||||
}
|
||||
}
|
||||
|
||||
tmpIterable.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) {
|
||||
var errors = <String>[];
|
||||
var input = Map.from(inputData);
|
||||
var data = <String, dynamic>{};
|
||||
|
||||
for (var key in defaultValues.keys) {
|
||||
if (!input.containsKey(key)) {
|
||||
var value = defaultValues[key];
|
||||
input[key] = value is DefaultValueFunction ? value() : value;
|
||||
}
|
||||
}
|
||||
|
||||
for (var field in forbiddenFields) {
|
||||
if (input.containsKey(field)) {
|
||||
if (!customErrorMessages.containsKey(field)) {
|
||||
errors.add("'$field' is forbidden.");
|
||||
} else {
|
||||
errors.add(customError(field, input[field]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var 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 ");
|
||||
|
||||
var rulesList = rules[key] ?? [];
|
||||
for (var matcher in rulesList) {
|
||||
if (matcher is ContextValidator) {
|
||||
if (!matcher.validate(key, input)) {
|
||||
errors.add(matcher
|
||||
.errorMessage(description, key, input)
|
||||
.toString()
|
||||
.trim());
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
var rulesList = rules[key] ?? [];
|
||||
for (var matcher in rulesList) {
|
||||
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) {
|
||||
if (e is ValidationException) {
|
||||
errors.add(e.errors.first);
|
||||
} else {
|
||||
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}) {
|
||||
var tmpSchema = <String, dynamic>{};
|
||||
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);
|
||||
}
|
||||
|
||||
tmpSchema[fieldName] = schema[key];
|
||||
}
|
||||
|
||||
return child.._importSchema(tmpSchema);
|
||||
}
|
||||
|
||||
/// Adds a [rule].
|
||||
void addRule(String key, Matcher rule) {
|
||||
if (!rules.containsKey(key)) {
|
||||
rules[key] = [rule];
|
||||
return;
|
||||
}
|
||||
|
||||
if (rules[key] == null) {
|
||||
rules[key] = List.empty(growable: true);
|
||||
}
|
||||
rules[key]?.add(rule);
|
||||
}
|
||||
|
||||
/// Adds all given [rules].
|
||||
void addRules(String key, Iterable<Matcher> rules) {
|
||||
for (var rule in rules) {
|
||||
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) {
|
||||
for (var rule in rules) {
|
||||
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 PlatformHttpException {
|
||||
/// A list of errors that resulted in the given data being marked invalid.
|
||||
//@override
|
||||
//final List<String> errors = [];
|
||||
|
||||
/// A descriptive message describing the error.
|
||||
//@override
|
||||
//final String message;
|
||||
|
||||
ValidationException(String message, {Iterable<String> errors = const []})
|
||||
: super(
|
||||
message: message,
|
||||
statusCode: 400,
|
||||
errors: (errors).toSet().toList(),
|
||||
stackTrace: StackTrace.current) {
|
||||
//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',
|
||||
...errors.map((error) => '* $error')
|
||||
];
|
||||
|
||||
return messages.join('\n');
|
||||
}
|
||||
}
|
31
packages/validate/pubspec.yaml
Normal file
31
packages/validate/pubspec.yaml
Normal file
|
@ -0,0 +1,31 @@
|
|||
name: angel3_validate
|
||||
description: Cross-platform HTTP request body validator library based on `matcher`.
|
||||
version: 8.2.0
|
||||
homepage: https://angel3-framework.web.app/
|
||||
repository: https://github.com/dart-backend/angel/tree/master/packages/validate
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
dependencies:
|
||||
platform_foundation: ^8.0.0
|
||||
platform_support: ^8.0.0
|
||||
matcher: ^0.12.0
|
||||
dev_dependencies:
|
||||
platform_testing: ^8.0.0
|
||||
build_runner: ^2.4.0
|
||||
build_web_compilers: ^4.0.0
|
||||
logging: ^1.2.0
|
||||
lints: ^4.0.0
|
||||
test: ^1.24.0
|
||||
# dependency_overrides:
|
||||
# angel3_container:
|
||||
# path: ../container/angel_container
|
||||
# angel3_framework:
|
||||
# path: ../framework
|
||||
# angel3_http_exception:
|
||||
# path: ../http_exception
|
||||
# angel3_model:
|
||||
# path: ../model
|
||||
# angel3_route:
|
||||
# path: ../route
|
||||
# angel3_mock_request:
|
||||
# path: ../mock_request
|
48
packages/validate/test/basic_data_test.dart
Normal file
48
packages/validate/test/basic_data_test.dart
Normal file
|
@ -0,0 +1,48 @@
|
|||
import 'package:angel3_validate/angel3_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
|
||||
});
|
||||
|
||||
void 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});
|
||||
}, throwsA(isA<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((dynamic x) => x == isTrue)));
|
||||
});
|
||||
}
|
66
packages/validate/test/complex_data_test.dart
Normal file
66
packages/validate/test/complex_data_test.dart
Normal file
|
@ -0,0 +1,66 @@
|
|||
import 'package:angel3_validate/angel3_validate.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
final Validator orderItemSchema = Validator({
|
||||
'id': [isInt, isPositive],
|
||||
'item_no': isString,
|
||||
'item_name': isString,
|
||||
'quantity': isInt,
|
||||
'description?': isString
|
||||
});
|
||||
|
||||
final Validator orderSchema = Validator({
|
||||
'id': [isInt, isPositive],
|
||||
'order_no': isString,
|
||||
'order_items*': [isList, everyElement(orderItemSchema)]
|
||||
}, defaultValues: {
|
||||
'order_items': []
|
||||
});
|
||||
|
||||
group('json data', () {
|
||||
test('validate with child element', () {
|
||||
var orderItem = {
|
||||
'id': 1,
|
||||
'item_no': 'a1',
|
||||
'item_name': 'Apple',
|
||||
'quantity': 1
|
||||
};
|
||||
|
||||
var formData = {
|
||||
'id': 1,
|
||||
'order_no': '2',
|
||||
'order_items': [orderItem]
|
||||
};
|
||||
var result = orderSchema.check(formData);
|
||||
|
||||
expect(result.errors.isEmpty, true);
|
||||
});
|
||||
|
||||
test('validate empty child', () {
|
||||
var formData = {'id': 1, 'order_no': '2'};
|
||||
var result = orderSchema.check(formData);
|
||||
|
||||
expect(result.errors.isEmpty, true);
|
||||
});
|
||||
|
||||
test('validate invalid child field', () {
|
||||
var orderItem = {
|
||||
'id': 1,
|
||||
'item_no': 'a1',
|
||||
'item_name': 'Apple',
|
||||
'quantity': 1,
|
||||
'description': 1
|
||||
};
|
||||
|
||||
var formData = {
|
||||
'id': 1,
|
||||
'order_no': '2',
|
||||
'order_items': [orderItem]
|
||||
};
|
||||
var result = orderSchema.check(formData);
|
||||
|
||||
expect(result.errors.isEmpty, false);
|
||||
});
|
||||
});
|
||||
}
|
69
packages/validate/test/server_test.dart
Normal file
69
packages/validate/test/server_test.dart
Normal file
|
@ -0,0 +1,69 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:platform_foundation/http.dart';
|
||||
//import 'package:angel_test/angel_test.dart';
|
||||
import 'package:angel3_validate/server.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:platform_testing/http.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
final Validator echoSchema = Validator({'message*': isString});
|
||||
|
||||
void printRecord(LogRecord rec) {
|
||||
print('${rec.time}: ${rec.level.name}: ${rec.loggerName}: ${rec.message}');
|
||||
if (rec.error != null) print(rec.error);
|
||||
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||
}
|
||||
|
||||
void main() {
|
||||
late Application app;
|
||||
late PlatformHttp http;
|
||||
//TestClient client;
|
||||
|
||||
setUp(() async {
|
||||
app = Application();
|
||||
http = PlatformHttp(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('angel3')..onRecord.listen(printRecord);
|
||||
//client = await connectTo(app);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
//await client.close();
|
||||
await http.close();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
15
packages/validate/validate.iml
Normal file
15
packages/validate/validate.iml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?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>
|
37
packages/validate/web/index.html
Normal file
37
packages/validate/web/index.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>angel3_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>
|
67
packages/validate/web/main.dart
Normal file
67
packages/validate/web/main.dart
Normal file
|
@ -0,0 +1,67 @@
|
|||
import 'dart:html';
|
||||
|
||||
import 'package:angel3_validate/angel3_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}}'..."
|
||||
});
|
||||
|
||||
void main() {
|
||||
$form!.onSubmit.listen((e) {
|
||||
e.preventDefault();
|
||||
$errors!.children.clear();
|
||||
|
||||
var formData = {};
|
||||
|
||||
for (var key in ['firstName', 'lastName', 'age', 'familySize']) {
|
||||
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;
|
Loading…
Reference in a new issue