Add 'packages/validate/' from commit 'db616d128ad76796f43e66b8214aa54dd20e51d2'

git-subtree-dir: packages/validate
git-subtree-mainline: 5eef4314ec
git-subtree-split: db616d128a
This commit is contained in:
Tobe O 2020-02-15 18:22:19 -05:00
commit 71aa3464b9
16 changed files with 1000 additions and 0 deletions

32
packages/validate/.gitignore vendored Normal file
View file

@ -0,0 +1,32 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.buildlog
.packages
.project
.pub/
build/
**/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
doc/api/
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
log.txt
.idea
.dart_tool

View file

@ -0,0 +1 @@
language: dart

View file

@ -0,0 +1,27 @@
# 3.0.0-alpha
* Rewrite, based on `Field` and `Form`, rather than strings.
# 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`.

21
packages/validate/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,82 @@
# 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)
Strongly-typed form handlers and validators for Angel.
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?
For convenience's sake, this library also exports `matcher`.
# Field
The basic unit is the `Field` class, which is a type-safe way to read
values from a `RequestContext`. Here is a simple example of using a
`TextField` instance to read a value from the URL query parameters:
```dart
app.get('/hello', (req, res) async {
var nameField = TextField('name');
var name = await nameField.getValue(req, query: true); // String
return 'Hello, $name!';
});
```
There are several included field types:
* `TextField`
* `BoolField`
* `NumField`
* `DoubleField`
* `IntField`
* `DateTimeField`
* `FileField`
* `ImageField`
# Forms
The `Form` class lets you combine `Field` instances, and decode
request bodies into `Map<String, dynamic>`. Unrecognized fields are
stripped out of the body, so a `Form` is effectively a whitelist.
```dart
var todoForm = Form(fields: [
TextField('text'),
BoolField('is_complete'),
]);
// 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;
}
```
## Form Rendering
TODO: Docs about this
# 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://www.dartdocs.org/documentation/matcher/latest/matcher/matcher-library.html).

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,96 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_validate/angel_validate.dart';
import 'package:http_parser/http_parser.dart';
import 'package:logging/logging.dart';
import 'package:pretty_logging/pretty_logging.dart';
main() async {
Logger.root
..level = Level.ALL
..onRecord.listen(prettyLog);
var app = Angel(logger: Logger('angel_validate'));
var http = AngelHttp(app);
var todos = <Todo>[];
/// We can combine fields into a form; this is most
/// useful when we immediately deserialize the form into
/// something else.
var todoForm = Form(fields: [
TextField('text'),
BoolField('is_complete'),
]);
/// We can directly use a `Form` to deserialize a
/// request body into a `Map<String, dynamic>`.
///
/// By calling `deserialize` or `decode`, we can populate
/// concrete Dart objects.
app.post('/', (req, res) async {
var todo = await todoForm.deserialize(req, Todo.fromMap);
todos.add(todo);
await res.redirect('/');
});
/// You can also use `Field`s to read directly from the
/// request, without `as` casts.
///
/// In this handler, we read the value of `name` from the query.
app.get('/hello', (req, res) async {
var nameField = TextField('name');
var name = await nameField.getValue(req, query: true);
return 'Hello, $name!';
});
/// Simple page displaying a form and some state.
app.get('/', (req, res) {
res
..contentType = MediaType('text', 'html')
..write('''
<!doctype html>
<html>
<body>
<h1>angel_validate</h1>
<ul>
${todos.map((t) {
return '<li>${t.text} (isComplete=${t.isComplete})</li>';
}).join()}
</ul>
<form method="POST">
<label for="text">Text:</label>
<input id="text" name="text">
<br>
<label for="is_complete">Complete?</label>
<input id="is_complete" name="is_complete" type="checkbox">
<br>
<button type="submit">Add Todo</button>
</form>
</body>
</html>
''');
});
app.fallback((req, res) => throw AngelHttpException.notFound());
app.errorHandler = (e, req, res) {
res.writeln('Error ${e.statusCode}: ${e.message}');
for (var error in e.errors) {
res.writeln('* $error');
}
};
await http.startServer('127.0.0.1', 3000);
print('Listening at ${http.uri}');
}
class Todo {
final String text;
final bool isComplete;
Todo(this.text, this.isComplete);
static Todo fromMap(Map map) {
return Todo(map['text'] as String, map['is_complete'] as bool);
}
}

View file

@ -0,0 +1,2 @@
export 'package:matcher/matcher.dart';
export 'without_matcher.dart';

View file

@ -0,0 +1,352 @@
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import 'package:http_parser/http_parser.dart';
import 'package:image/image.dart';
import 'field.dart';
import 'form_renderer.dart';
/// A [Field] that accepts plain text.
class TextField extends Field<String> {
/// If `true` (default), then renderers will produce a `<textarea>` element.
final bool isTextArea;
/// If `true` (default), then the input will be trimmed before validation.
final bool trim;
/// If not `null`, then if the value of [confirmedAs] in the body is not
/// identical, an error will be returned.
final String confirmedAs;
TextField(String name,
{String label,
bool isRequired = true,
this.isTextArea = false,
this.trim = true,
this.confirmedAs,
String type = 'text'})
: super(name, type, label: label, isRequired: isRequired);
@override
FutureOr<U> accept<U>(FormRenderer<U> renderer) =>
renderer.visitTextField(this);
String _normalize(String s) {
if (trim) {
s = s?.trim();
}
return s;
}
@override
FutureOr<FieldReadResult<String>> read(RequestContext req,
Map<String, dynamic> fields, Iterable<UploadedFile> files) {
var value = _normalize(fields[name] as String);
if (value == null) {
return null;
} else if (trim && value.isEmpty) {
return null;
} else {
if (confirmedAs != null) {
var confirmed = _normalize(fields[confirmedAs] as String);
if (confirmed != value) {
return FieldReadResult.failure(
['"$name" and "$confirmedAs" must be identical.']);
}
}
return FieldReadResult.success(value);
}
}
}
/// A [Field] that checks simply for its presence in the given data.
/// Typically used for checkboxes.
class BoolField extends Field<bool> {
BoolField(String name,
{String label, bool isRequired = true, String type = 'checkbox'})
: super(name, type, label: label, isRequired: isRequired);
@override
FutureOr<U> accept<U>(FormRenderer<U> renderer) =>
renderer.visitBoolField(this);
@override
FutureOr<FieldReadResult<bool>> read(RequestContext req,
Map<String, dynamic> fields, Iterable<UploadedFile> files) {
if (fields.containsKey(name)) {
return FieldReadResult.success(true);
} else {
return FieldReadResult.success(false);
}
}
}
/// A [Field] that parses its value as a [num].
class NumField<T extends num> extends Field<T> {
// Reuse text validation logic.
TextField _textField;
/// The minimum/maximum value for the field.
final T min, max;
/// The amount for a form field to increment by.
final num step;
NumField(String name,
{String label,
String type = 'number',
bool isRequired = true,
this.max,
this.min,
this.step})
: super(name, type, label: label, isRequired: isRequired) {
_textField = TextField(name, label: label, isRequired: isRequired);
}
@override
FutureOr<U> accept<U>(FormRenderer<U> renderer) =>
renderer.visitNumField(this);
@override
Future<FieldReadResult<T>> read(RequestContext req,
Map<String, dynamic> fields, Iterable<UploadedFile> files) async {
var result = await _textField.read(req, fields, files);
if (result == null) {
return null;
} else if (result.isSuccess != true) {
return FieldReadResult.failure(result.errors);
} else {
var value = num.tryParse(result.value);
if (value != null) {
if (min != null && value < min) {
return FieldReadResult.failure(['"$name" can be no less than $min.']);
} else if (max != null && value > max) {
return FieldReadResult.failure(
['"$name" can be no greater than $max.']);
} else {
return FieldReadResult.success(value as T);
}
} else {
return FieldReadResult.failure(['"$name" must be a number.']);
}
}
}
}
/// A [NumField] that coerces its value to a [double].
class DoubleField extends NumField<double> {
DoubleField(String name,
{String label,
String type = 'number',
bool isRequired = true,
num step,
double min,
double max})
: super(name,
type: type,
label: label,
isRequired: isRequired,
step: step,
min: min,
max: max);
@override
Future<FieldReadResult<double>> read(RequestContext req,
Map<String, dynamic> fields, Iterable<UploadedFile> files) async {
var result = await super.read(req, fields, files);
if (result == null) {
return null;
} else if (!result.isSuccess) {
return FieldReadResult.failure(result.errors);
} else {
return FieldReadResult.success(result.value.toDouble());
}
}
}
/// A [NumField] that requires its value to be an [int].
/// Passing a [double] will result in an error, so [step] defaults to 1.
class IntField extends NumField<int> {
IntField(String name,
{String label,
String type = 'number',
bool isRequired = true,
num step = 1,
int min,
int max})
: super(name,
label: label,
type: type,
isRequired: isRequired,
step: step,
min: min,
max: max);
@override
Future<FieldReadResult<int>> read(RequestContext req,
Map<String, dynamic> fields, Iterable<UploadedFile> files) async {
var result = await super.read(req, fields, files);
if (result == null) {
return null;
} else if (!result.isSuccess) {
return FieldReadResult.failure(result.errors);
} else {
var value = result.value;
if (value is int) {
return FieldReadResult.success(result.value);
} else {
return FieldReadResult.failure(['"$name" must be an integer.']);
}
}
}
}
/// A [Field] that parses its value as an ISO6801 [DateTime].
class DateTimeField extends Field<DateTime> {
// Reuse text validation logic.
TextField _textField;
/// The minimum/maximum value for the field.
final DateTime min, max;
/// The amount for a form field to increment by.
final num step;
DateTimeField(String name,
{String label,
bool isRequired = true,
this.max,
this.min,
this.step,
String type = 'datetime-local'})
: super(name, type, label: label, isRequired: isRequired) {
_textField = TextField(name, label: label, isRequired: isRequired);
}
@override
FutureOr<U> accept<U>(FormRenderer<U> renderer) =>
renderer.visitDateTimeField(this);
@override
Future<FieldReadResult<DateTime>> read(RequestContext req,
Map<String, dynamic> fields, Iterable<UploadedFile> files) async {
var result = await _textField.read(req, fields, files);
if (result == null) {
return null;
} else if (result.isSuccess != true) {
return FieldReadResult.failure(result.errors);
} else {
var value = DateTime.tryParse(result.value);
if (value != null) {
return FieldReadResult.success(value);
} else {
return FieldReadResult.failure(
['"$name" must be a properly-formatted date.']);
}
}
}
}
/// A [Field] that validates an [UploadedFile].
class FileField extends Field<UploadedFile> {
/// If `true` (default), then the file must have a `content-type`.
final bool requireContentType;
/// If `true` (default: `false`), then the file must have an associated
/// filename.
final bool requireFilename;
/// If provided, then the `content-type` must be present in this [Iterable].
final Iterable<MediaType> allowedContentTypes;
FileField(String name,
{String label,
bool isRequired = true,
this.requireContentType = true,
this.requireFilename = false,
this.allowedContentTypes})
: super(name, 'file', label: label, isRequired: isRequired) {
assert(allowedContentTypes == null || allowedContentTypes.isNotEmpty);
}
@override
FutureOr<U> accept<U>(FormRenderer<U> renderer) =>
renderer.visitFileField(this);
@override
FutureOr<FieldReadResult<UploadedFile>> read(RequestContext req,
Map<String, dynamic> fields, Iterable<UploadedFile> files) {
var file = files.firstWhere((f) => f.name == name, orElse: () => null);
if (file == null) {
return null;
} else if ((requireContentType || allowedContentTypes != null) &&
file.contentType == null) {
return FieldReadResult.failure(
['A content type must be given for file "$name".']);
} else if (requireFilename && file.filename == null) {
return FieldReadResult.failure(
['A filename must be given for file "$name".']);
} else if (allowedContentTypes != null &&
!allowedContentTypes.contains(file.contentType)) {
return FieldReadResult.failure([
'File "$name" cannot have content type '
'"${file.contentType}". Allowed types: '
'${allowedContentTypes.join(', ')}'
]);
} else {
return FieldReadResult.success(file);
}
}
}
/// A wrapper around [FileField] that reads its input into an [Image].
///
/// **CAUTION**: The uploaded file will be read in memory.
class ImageField extends Field<Image> {
FileField _fileField;
/// The underlying [FileField].
FileField get fileField => _fileField;
ImageField(String name,
{String label,
bool isRequired = true,
bool requireContentType = true,
bool requireFilename = false,
Iterable<MediaType> allowedContentTypes})
: super(name, 'file', label: label, isRequired: isRequired) {
_fileField = FileField(name,
label: label,
isRequired: isRequired,
requireContentType: requireContentType,
requireFilename: requireFilename,
allowedContentTypes: allowedContentTypes);
}
@override
FutureOr<U> accept<U>(FormRenderer<U> renderer) =>
renderer.visitImageField(this);
@override
FutureOr<FieldReadResult<Image>> read(RequestContext req,
Map<String, dynamic> fields, Iterable<UploadedFile> files) async {
var result = await fileField.read(req, fields, files);
if (result == null) {
return null;
} else if (!result.isSuccess) {
return FieldReadResult.failure(result.errors);
} else {
try {
var image = decodeImage(await result.value.readAsBytes());
if (image == null) {
return FieldReadResult.failure(['"$name" must be an image file.']);
} else {
return FieldReadResult.success(image);
}
} on ImageException catch (e) {
return FieldReadResult.failure(
['Error in image file "$name": ${e.message}']);
}
}
}
}

View file

@ -0,0 +1,126 @@
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import 'package:matcher/matcher.dart';
import 'form.dart';
import 'form_renderer.dart';
/// Holds the result of validating a field.
class FieldReadResult<T> {
/// If `true`, then validation was successful.
/// If `false`, [errors] must not be empty.
final bool isSuccess;
/// The value provided by the user.
final T value;
/// Any errors that arose during validation.
final Iterable<String> errors;
FieldReadResult.success(this.value)
: isSuccess = true,
errors = [];
FieldReadResult.failure(this.errors)
: isSuccess = false,
value = null;
}
/// An abstraction used to fetch values from request bodies, in a type-safe manner.
abstract class Field<T> {
/// The name of this field. This is the name that users should include in
/// request bodies.
final String name;
/// An optional label for the field.
final String label;
/// Whether the field is required. If `true`, then if it is not
/// present, an error will be generated.
final bool isRequired;
/// The input `type` attribute, if applicable.
final String type;
Field(this.name, this.type, {this.label, this.isRequired = true});
/// Reads the value from the request body.
///
/// If it returns `null` and [isRequired] is `true`, an error must
/// be generated.
FutureOr<FieldReadResult<T>> read(RequestContext req,
Map<String, dynamic> fields, Iterable<UploadedFile> files);
/// Accepts a form renderer.
FutureOr<U> accept<U>(FormRenderer<U> renderer);
/// Wraps this instance in one that throws an error if any of the
/// [matchers] fails.
Field<T> match(Iterable<Matcher> matchers) => _MatchedField(this, matchers);
/// Calls [read], and returns the retrieve value from the body.
///
/// If [query] is `true` (default: `false`), then the value will
/// be read from the request `queryParameters` instead.
///
/// If there is an error, then an [AngelHttpException] is thrown.
/// If a [defaultValue] is provided, it will be returned in case of an
/// error or missing value.
Future<T> getValue(RequestContext req,
{String errorMessage, T defaultValue, bool query = false}) async {
var uploadedFiles = <UploadedFile>[];
if (req.hasParsedBody || !query) {
await req.parseBody();
uploadedFiles = req.uploadedFiles;
}
var result = await read(
req, query ? req.queryParameters : req.bodyAsMap, uploadedFiles);
if (result?.isSuccess != true && defaultValue != null) {
return defaultValue;
} else if (result == null) {
errorMessage ??= Form.reportMissingField(name, query: query);
throw AngelHttpException.badRequest(message: errorMessage);
} else if (!result.isSuccess) {
errorMessage ??= result.errors.first;
throw AngelHttpException.badRequest(
message: errorMessage, errors: result.errors.toList());
} else {
return result.value;
}
}
}
class _MatchedField<T> extends Field<T> {
final Field<T> inner;
final Iterable<Matcher> matchers;
_MatchedField(this.inner, this.matchers)
: super(inner.name, inner.type,
label: inner.label, isRequired: inner.isRequired) {
assert(matchers.isNotEmpty);
}
@override
FutureOr<U> accept<U>(FormRenderer<U> renderer) => inner.accept(renderer);
@override
Future<FieldReadResult<T>> read(RequestContext req,
Map<String, dynamic> fields, Iterable<UploadedFile> files) async {
var result = await inner.read(req, fields, files);
if (!result.isSuccess) {
return result;
} else {
var errors = <String>[];
for (var matcher in matchers) {
if (!matcher.matches(result.value, {})) {
var desc = matcher.describe(StringDescription());
errors.add('Expected $desc for field "${inner.name}".');
}
}
if (errors.isEmpty) {
return result;
} else {
return FieldReadResult.failure(errors);
}
}
}
}

View file

@ -0,0 +1,128 @@
import 'dart:async';
import 'dart:convert';
import 'package:angel_framework/angel_framework.dart';
import 'package:matcher/matcher.dart';
import 'field.dart';
/// A utility that combines multiple [Field]s to read and
/// validate web forms in a type-safe manner.
///
/// Example:
/// ```dart
/// import 'package:angel_validate/angel_validate.dart';
///
/// var myForm = Form(fields: [
/// TextField('username').match([minLength(8)]),
/// TextField('password', confirmedAs: 'confirm_password'),
/// ])
///
/// app.post('/login', (req, res) async {
/// var loginBody =
/// await myForm.decode(req, loginBodySerializer);
/// // Do something with the decoded object...
/// });
/// ```
class Form {
/// A custom error message to provide the user if validation fails.
final String errorMessage;
final List<Field> _fields = [];
static const String defaultErrorMessage =
'There were errors in your submission. '
'Please make sure all fields entered correctly, and submit it again.';
/// Computes an error message in the case of a missing required field.
static String reportMissingField(String fieldName, {bool query = false}) {
var type = query ? 'query parameter' : 'field';
return 'The $type "$fieldName" is required.';
}
Form({this.errorMessage = defaultErrorMessage, Iterable<Field> fields}) {
fields?.forEach(addField);
}
/// Returns the fields in this form.
List<Field> get fields => _fields;
/// Helper for adding fields. Passing [matchers] will result in them
/// being applied to the [field].
Field<T> addField<T>(Field<T> field, {Iterable<Matcher> matchers}) {
if (matchers != null) {
field = field.match(matchers);
}
_fields.add(field);
return field;
}
/// Deserializes the result of calling [validate].
///
/// If [query] is `true` (default: `false`), then the value will
/// be read from the request `queryParameters` instead.
Future<T> deserialize<T>(
RequestContext req, T Function(Map<String, dynamic>) f,
{bool query = false}) {
return validate(req, query: query).then(f);
}
/// Uses the [codec] to [deserialize] the result of calling [validate].
///
/// If [query] is `true` (default: `false`), then the value will
/// be read from the request `queryParameters` instead.
Future<T> decode<T>(RequestContext req, Codec<T, Map> codec,
{bool query = false}) {
return deserialize(req, codec.decode, query: query);
}
/// Calls [read], and returns the filtered request body.
/// If there is even one error, then an [AngelHttpException] is thrown.
///
/// If [query] is `true` (default: `false`), then the value will
/// be read from the request `queryParameters` instead.
Future<Map<String, dynamic>> validate(RequestContext req,
{bool query = false}) async {
var result = await read(req, query: query);
if (!result.isSuccess) {
throw AngelHttpException.badRequest(
message: errorMessage, errors: result.errors.toList());
} else {
return result.value;
}
}
/// Reads the body of the [RequestContext], and returns an object detailing
/// whether valid values were provided for all [fields].
///
/// In most cases, you'll want to use [validate] instead.
///
/// If [query] is `true` (default: `false`), then the value will
/// be read from the request `queryParameters` instead.
Future<FieldReadResult<Map<String, dynamic>>> read(RequestContext req,
{bool query = false}) async {
var out = <String, dynamic>{};
var errors = <String>[];
var uploadedFiles = <UploadedFile>[];
if (req.hasParsedBody || !query) {
await req.parseBody();
uploadedFiles = req.uploadedFiles;
}
for (var field in fields) {
var result = await field.read(
req, query ? req.queryParameters : req.bodyAsMap, uploadedFiles);
if (result == null && field.isRequired) {
errors.add(reportMissingField(field.name, query: query));
} else if (!result.isSuccess) {
errors.addAll(result.errors);
} else {
out[field.name] = result.value;
}
}
if (errors.isNotEmpty) {
return FieldReadResult.failure(errors);
} else {
return FieldReadResult.success(out);
}
}
}

View file

@ -0,0 +1,21 @@
import 'dart:async';
import 'common_fields.dart';
import 'field.dart';
abstract class FormRenderer<T> {
const FormRenderer();
FutureOr<T> visit(Field<T> field) => field.accept(this);
FutureOr<T> visitBoolField(BoolField field);
FutureOr<T> visitDateTimeField(DateTimeField field);
FutureOr<T> visitFileField(FileField field);
FutureOr<T> visitImageField(ImageField field);
FutureOr<T> visitNumField(NumField field);
FutureOr<T> visitTextField(TextField field);
}

View file

@ -0,0 +1,76 @@
import 'package:matcher/matcher.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(
(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 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 address');
/// Asserts that a value is an `int`.
final Matcher isInt = predicate((value) => value is int, 'an integer');
/// Asserts that a value is a `num`.
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');
/// Asserts that a value is a non-empty `String`.
final Matcher isNonEmptyString = predicate(
(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(
(x) {
try {
return x is String && DateTime.parse(x) != null;
} 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(
(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(
(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(
(value) => value is String && value.length <= length,
'a string no longer than $length character(s) long');

View 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';

View file

@ -0,0 +1,23 @@
name: angel_validate
version: 3.0.0-alpha
description: Strongly-typed form handlers and validators for the Angel framework.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/validate
environment:
sdk: ">=2.0.0 <3.0.0"
dependencies:
angel_framework: ^2.0.0
duration: ^2.0.0
html_builder: ^1.0.0
http_parser: ^3.0.0
image: ^2.0.0
matcher: ^0.12.5
dev_dependencies:
angel_orm: ^2.1.0-beta
angel_orm_generator: ^2.1.0-beta
angel_serialize: ^2.0.0
angel_serialize_generator: ^2.0.0
build_runner: ^1.0.0
pedantic: ^1.0.0
pretty_logging: ^1.0.0
test: ^1.0.0

View file

@ -0,0 +1,4 @@
import 'package:angel_validate/angel_validate.dart';
import 'package:test/test.dart';
void main() {}