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 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')
|
||||
|
@ -24,6 +51,12 @@ main() async {
|
|||
<!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">
|
||||
|
@ -31,7 +64,7 @@ main() async {
|
|||
<label for="is_complete">Complete?</label>
|
||||
<input id="is_complete" name="is_complete" type="checkbox">
|
||||
<br>
|
||||
<input type="submit" value="submit">
|
||||
<button type="submit">Add Todo</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -48,6 +81,7 @@ main() async {
|
|||
};
|
||||
|
||||
await http.startServer('127.0.0.1', 3000);
|
||||
print('Listening at ${http.uri}');
|
||||
}
|
||||
|
||||
class Todo {
|
||||
|
|
|
@ -5,11 +5,17 @@ import 'form_renderer.dart';
|
|||
|
||||
/// A [Field] that accepts plain text.
|
||||
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;
|
||||
|
||||
/// If `true` (default), then the input will be trimmed before validation.
|
||||
final bool trim;
|
||||
|
||||
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);
|
||||
|
||||
@override
|
||||
|
@ -17,10 +23,16 @@ class TextField extends Field<String> {
|
|||
renderer.visitTextField(this);
|
||||
|
||||
@override
|
||||
FutureOr<FieldReadResult<String>> read(RequestContext req) {
|
||||
var value = req.bodyAsMap[name] as String;
|
||||
FutureOr<FieldReadResult<String>> read(
|
||||
Map<String, dynamic> fields, Iterable<UploadedFile> files) {
|
||||
var value = fields[name] as String;
|
||||
if (trim) {
|
||||
value = value?.trim();
|
||||
}
|
||||
if (value == null) {
|
||||
return null;
|
||||
} else if (trim && value.isEmpty) {
|
||||
return null;
|
||||
} else {
|
||||
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.
|
||||
/// Typically used for checkboxes.
|
||||
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);
|
||||
|
||||
@override
|
||||
|
@ -38,13 +50,12 @@ class BoolField extends Field<bool> {
|
|||
renderer.visitBoolField(this);
|
||||
|
||||
@override
|
||||
FutureOr<FieldReadResult<bool>> read(RequestContext req) {
|
||||
if (req.bodyAsMap.containsKey(name)) {
|
||||
FutureOr<FieldReadResult<bool>> read(
|
||||
Map<String, dynamic> fields, Iterable<UploadedFile> files) {
|
||||
if (fields.containsKey(name)) {
|
||||
return FieldReadResult.success(true);
|
||||
} else if (!isRequired) {
|
||||
return FieldReadResult.success(false);
|
||||
} else {
|
||||
return null;
|
||||
return FieldReadResult.success(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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.
|
||||
|
@ -37,13 +38,14 @@ abstract class Field<T> {
|
|||
/// present, an error will be generated.
|
||||
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.
|
||||
///
|
||||
/// If it returns `null` and [isRequired] is `true`, an error must
|
||||
/// be generated.
|
||||
FutureOr<FieldReadResult<T>> read(RequestContext req);
|
||||
FutureOr<FieldReadResult<T>> read(
|
||||
Map<String, dynamic> fields, Iterable<UploadedFile> files);
|
||||
|
||||
/// Accepts a form 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
|
||||
/// [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(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> {
|
||||
|
@ -66,8 +99,9 @@ class _MatchedField<T> extends Field<T> {
|
|||
FutureOr<U> accept<U>(FormRenderer<U> renderer) => inner.accept(renderer);
|
||||
|
||||
@override
|
||||
Future<FieldReadResult<T>> read(RequestContext req) async {
|
||||
var result = await inner.read(req);
|
||||
Future<FieldReadResult<T>> read(
|
||||
Map<String, dynamic> fields, Iterable<UploadedFile> files) async {
|
||||
var result = await inner.read(fields, files);
|
||||
if (!result.isSuccess) {
|
||||
return result;
|
||||
} else {
|
||||
|
|
|
@ -32,6 +32,12 @@ class Form {
|
|||
'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);
|
||||
}
|
||||
|
@ -50,20 +56,32 @@ class Form {
|
|||
}
|
||||
|
||||
/// 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) {
|
||||
return validate(req).then(f);
|
||||
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].
|
||||
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.
|
||||
/// 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) {
|
||||
throw AngelHttpException.badRequest(
|
||||
message: errorMessage, errors: result.errors.toList());
|
||||
|
@ -76,15 +94,24 @@ class Form {
|
|||
/// whether valid values were provided for all [fields].
|
||||
///
|
||||
/// 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 errors = <String>[];
|
||||
await req.parseBody();
|
||||
var uploadedFiles = <UploadedFile>[];
|
||||
if (req.hasParsedBody || !query) {
|
||||
await req.parseBody();
|
||||
uploadedFiles = req.uploadedFiles;
|
||||
}
|
||||
|
||||
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) {
|
||||
errors.add('The field "${field.name}" is required.');
|
||||
errors.add(reportMissingField(field.name, query: query));
|
||||
} else if (!result.isSuccess) {
|
||||
errors.addAll(result.errors);
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue