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

View file

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

View file

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

View file

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