Field.getValue
This commit is contained in:
parent
c090e9878e
commit
b3e2e8a401
4 changed files with 132 additions and 26 deletions
|
@ -12,11 +12,38 @@ main() async {
|
||||||
|
|
||||||
var app = Angel(logger: Logger('angel_validate'));
|
var app = Angel(logger: Logger('angel_validate'));
|
||||||
var http = AngelHttp(app);
|
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: [
|
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) {
|
app.get('/', (req, res) {
|
||||||
res
|
res
|
||||||
..contentType = MediaType('text', 'html')
|
..contentType = MediaType('text', 'html')
|
||||||
|
@ -24,6 +51,12 @@ main() async {
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
|
<h1>angel_validate</h1>
|
||||||
|
<ul>
|
||||||
|
${todos.map((t) {
|
||||||
|
return '<li>${t.text} (isComplete=${t.isComplete})</li>';
|
||||||
|
}).join()}
|
||||||
|
</ul>
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<label for="text">Text:</label>
|
<label for="text">Text:</label>
|
||||||
<input id="text" name="text">
|
<input id="text" name="text">
|
||||||
|
@ -31,7 +64,7 @@ main() async {
|
||||||
<label for="is_complete">Complete?</label>
|
<label for="is_complete">Complete?</label>
|
||||||
<input id="is_complete" name="is_complete" type="checkbox">
|
<input id="is_complete" name="is_complete" type="checkbox">
|
||||||
<br>
|
<br>
|
||||||
<input type="submit" value="submit">
|
<button type="submit">Add Todo</button>
|
||||||
</form>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -48,6 +81,7 @@ main() async {
|
||||||
};
|
};
|
||||||
|
|
||||||
await http.startServer('127.0.0.1', 3000);
|
await http.startServer('127.0.0.1', 3000);
|
||||||
|
print('Listening at ${http.uri}');
|
||||||
}
|
}
|
||||||
|
|
||||||
class Todo {
|
class Todo {
|
||||||
|
|
|
@ -5,11 +5,17 @@ import 'form_renderer.dart';
|
||||||
|
|
||||||
/// A [Field] that accepts plain text.
|
/// A [Field] that accepts plain text.
|
||||||
class TextField extends Field<String> {
|
class TextField extends Field<String> {
|
||||||
/// If `true`, then renderers will produce a `<textarea>` element.
|
/// If `true` (default), then renderers will produce a `<textarea>` element.
|
||||||
final bool isTextArea;
|
final bool isTextArea;
|
||||||
|
|
||||||
|
/// If `true` (default), then the input will be trimmed before validation.
|
||||||
|
final bool trim;
|
||||||
|
|
||||||
TextField(String name,
|
TextField(String name,
|
||||||
{String label, bool isRequired = false, this.isTextArea = false})
|
{String label,
|
||||||
|
bool isRequired = true,
|
||||||
|
this.isTextArea = false,
|
||||||
|
this.trim = true})
|
||||||
: super(name, label: label, isRequired: isRequired);
|
: super(name, label: label, isRequired: isRequired);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -17,10 +23,16 @@ class TextField extends Field<String> {
|
||||||
renderer.visitTextField(this);
|
renderer.visitTextField(this);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<FieldReadResult<String>> read(RequestContext req) {
|
FutureOr<FieldReadResult<String>> read(
|
||||||
var value = req.bodyAsMap[name] as String;
|
Map<String, dynamic> fields, Iterable<UploadedFile> files) {
|
||||||
|
var value = fields[name] as String;
|
||||||
|
if (trim) {
|
||||||
|
value = value?.trim();
|
||||||
|
}
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return null;
|
return null;
|
||||||
|
} else if (trim && value.isEmpty) {
|
||||||
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return FieldReadResult.success(value);
|
return FieldReadResult.success(value);
|
||||||
}
|
}
|
||||||
|
@ -30,7 +42,7 @@ class TextField extends Field<String> {
|
||||||
/// A [Field] that checks simply for its presence in the given data.
|
/// A [Field] that checks simply for its presence in the given data.
|
||||||
/// Typically used for checkboxes.
|
/// Typically used for checkboxes.
|
||||||
class BoolField extends Field<bool> {
|
class BoolField extends Field<bool> {
|
||||||
BoolField(String name, {String label, bool isRequired = false})
|
BoolField(String name, {String label, bool isRequired = true})
|
||||||
: super(name, label: label, isRequired: isRequired);
|
: super(name, label: label, isRequired: isRequired);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -38,13 +50,12 @@ class BoolField extends Field<bool> {
|
||||||
renderer.visitBoolField(this);
|
renderer.visitBoolField(this);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<FieldReadResult<bool>> read(RequestContext req) {
|
FutureOr<FieldReadResult<bool>> read(
|
||||||
if (req.bodyAsMap.containsKey(name)) {
|
Map<String, dynamic> fields, Iterable<UploadedFile> files) {
|
||||||
|
if (fields.containsKey(name)) {
|
||||||
return FieldReadResult.success(true);
|
return FieldReadResult.success(true);
|
||||||
} else if (!isRequired) {
|
|
||||||
return FieldReadResult.success(false);
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return FieldReadResult.success(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:matcher/matcher.dart';
|
import 'package:matcher/matcher.dart';
|
||||||
|
import 'form.dart';
|
||||||
import 'form_renderer.dart';
|
import 'form_renderer.dart';
|
||||||
|
|
||||||
/// Holds the result of validating a field.
|
/// Holds the result of validating a field.
|
||||||
|
@ -37,13 +38,14 @@ abstract class Field<T> {
|
||||||
/// present, an error will be generated.
|
/// present, an error will be generated.
|
||||||
final bool isRequired;
|
final bool isRequired;
|
||||||
|
|
||||||
Field(this.name, {this.label, this.isRequired = false});
|
Field(this.name, {this.label, this.isRequired = true});
|
||||||
|
|
||||||
/// Reads the value from the request body.
|
/// Reads the value from the request body.
|
||||||
///
|
///
|
||||||
/// If it returns `null` and [isRequired] is `true`, an error must
|
/// If it returns `null` and [isRequired] is `true`, an error must
|
||||||
/// be generated.
|
/// be generated.
|
||||||
FutureOr<FieldReadResult<T>> read(RequestContext req);
|
FutureOr<FieldReadResult<T>> read(
|
||||||
|
Map<String, dynamic> fields, Iterable<UploadedFile> files);
|
||||||
|
|
||||||
/// Accepts a form renderer.
|
/// Accepts a form renderer.
|
||||||
FutureOr<U> accept<U>(FormRenderer<U> renderer);
|
FutureOr<U> accept<U>(FormRenderer<U> renderer);
|
||||||
|
@ -51,6 +53,37 @@ abstract class Field<T> {
|
||||||
/// Wraps this instance in one that throws an error if any of the
|
/// Wraps this instance in one that throws an error if any of the
|
||||||
/// [matchers] fails.
|
/// [matchers] fails.
|
||||||
Field<T> match(Iterable<Matcher> matchers) => _MatchedField(this, matchers);
|
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(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> {
|
class _MatchedField<T> extends Field<T> {
|
||||||
|
@ -66,8 +99,9 @@ class _MatchedField<T> extends Field<T> {
|
||||||
FutureOr<U> accept<U>(FormRenderer<U> renderer) => inner.accept(renderer);
|
FutureOr<U> accept<U>(FormRenderer<U> renderer) => inner.accept(renderer);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FieldReadResult<T>> read(RequestContext req) async {
|
Future<FieldReadResult<T>> read(
|
||||||
var result = await inner.read(req);
|
Map<String, dynamic> fields, Iterable<UploadedFile> files) async {
|
||||||
|
var result = await inner.read(fields, files);
|
||||||
if (!result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -32,6 +32,12 @@ class Form {
|
||||||
'There were errors in your submission. '
|
'There were errors in your submission. '
|
||||||
'Please make sure all fields entered correctly, and submit it again.';
|
'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}) {
|
Form({this.errorMessage = defaultErrorMessage, Iterable<Field> fields}) {
|
||||||
fields?.forEach(addField);
|
fields?.forEach(addField);
|
||||||
}
|
}
|
||||||
|
@ -50,20 +56,32 @@ class Form {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deserializes the result of calling [validate].
|
/// 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>(
|
Future<T> deserialize<T>(
|
||||||
RequestContext req, T Function(Map<String, dynamic>) f) {
|
RequestContext req, T Function(Map<String, dynamic>) f,
|
||||||
return validate(req).then(f);
|
{bool query = false}) {
|
||||||
|
return validate(req, query: query).then(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Uses the [codec] to [deserialize] the result of calling [validate].
|
/// Uses the [codec] to [deserialize] the result of calling [validate].
|
||||||
Future<T> decode<T>(RequestContext req, Codec<T, Map> codec) {
|
///
|
||||||
return deserialize(req, codec.decode);
|
/// 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.
|
/// Calls [read], and returns the filtered request body.
|
||||||
/// If there is even one error, then an [AngelHttpException] is thrown.
|
/// If there is even one error, then an [AngelHttpException] is thrown.
|
||||||
Future<Map<String, dynamic>> validate(RequestContext req) async {
|
///
|
||||||
var result = await read(req);
|
/// 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) {
|
if (!result.isSuccess) {
|
||||||
throw AngelHttpException.badRequest(
|
throw AngelHttpException.badRequest(
|
||||||
message: errorMessage, errors: result.errors.toList());
|
message: errorMessage, errors: result.errors.toList());
|
||||||
|
@ -76,15 +94,24 @@ class Form {
|
||||||
/// whether valid values were provided for all [fields].
|
/// whether valid values were provided for all [fields].
|
||||||
///
|
///
|
||||||
/// In most cases, you'll want to use [validate] instead.
|
/// In most cases, you'll want to use [validate] instead.
|
||||||
Future<FieldReadResult<Map<String, dynamic>>> read(RequestContext req) async {
|
///
|
||||||
|
/// 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 out = <String, dynamic>{};
|
||||||
var errors = <String>[];
|
var errors = <String>[];
|
||||||
await req.parseBody();
|
var uploadedFiles = <UploadedFile>[];
|
||||||
|
if (req.hasParsedBody || !query) {
|
||||||
|
await req.parseBody();
|
||||||
|
uploadedFiles = req.uploadedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
for (var field in fields) {
|
for (var field in fields) {
|
||||||
var result = await field.read(req);
|
var result = await field.read(
|
||||||
|
query ? req.queryParameters : req.bodyAsMap, uploadedFiles);
|
||||||
if (result == null && field.isRequired) {
|
if (result == null && field.isRequired) {
|
||||||
errors.add('The field "${field.name}" is required.');
|
errors.add(reportMissingField(field.name, query: query));
|
||||||
} else if (!result.isSuccess) {
|
} else if (!result.isSuccess) {
|
||||||
errors.addAll(result.errors);
|
errors.addAll(result.errors);
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue