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 { /// 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 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 { /// 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> read( Map fields, Iterable files); /// Accepts a form renderer. FutureOr accept(FormRenderer renderer); /// Wraps this instance in one that throws an error if any of the /// [matchers] fails. Field match(Iterable 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 getValue(RequestContext req, {String errorMessage, T defaultValue, bool query = false}) async { var uploadedFiles = []; 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 extends Field { final Field inner; final Iterable matchers; _MatchedField(this.inner, this.matchers) : super(inner.name, inner.type, label: inner.label, isRequired: inner.isRequired) { assert(matchers.isNotEmpty); } @override FutureOr accept(FormRenderer renderer) => inner.accept(renderer); @override Future> read( Map fields, Iterable files) async { var result = await inner.read(fields, files); if (!result.isSuccess) { return result; } else { var errors = []; 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); } } } }