Field.getValue

This commit is contained in:
Tobe O 2019-10-16 20:54:38 -04:00
parent c090e9878e
commit b3e2e8a401
4 changed files with 132 additions and 26 deletions

View file

@ -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 {

View file

@ -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);
}
}
}

View file

@ -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 {

View file

@ -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 {