diff --git a/example/main.dart b/example/main.dart index 8b137891..3d747d7a 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1 +1,62 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_framework/http.dart'; +import 'package:angel_validate/angel_validate.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:logging/logging.dart'; +import 'package:pretty_logging/pretty_logging.dart'; +main() async { + Logger.root + ..level = Level.ALL + ..onRecord.listen(prettyLog); + + var app = Angel(logger: Logger('angel_validate')); + var http = AngelHttp(app); + + var todoForm = Form(fields: [ + + ]); + + app.get('/', (req, res) { + res + ..contentType = MediaType('text', 'html') + ..write(''' + + + +
+ + +
+ + +
+ +
+ + + '''); + }); + + app.fallback((req, res) => throw AngelHttpException.notFound()); + + app.errorHandler = (e, req, res) { + res.writeln('Error ${e.statusCode}: ${e.message}'); + for (var error in e.errors) { + res.writeln('* $error'); + } + }; + + await http.startServer('127.0.0.1', 3000); +} + +class Todo { + final String text; + final bool isComplete; + + Todo(this.text, this.isComplete); + + static Todo fromMap(Map map) { + return Todo(map['text'] as String, map['is_complete'] as bool); + } +} diff --git a/lib/angel_forms.dart b/lib/angel_forms.dart deleted file mode 100644 index da383990..00000000 --- a/lib/angel_forms.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'src/field.dart'; -export 'src/form.dart'; diff --git a/lib/angel_validate.dart b/lib/angel_validate.dart new file mode 100644 index 00000000..70a535c6 --- /dev/null +++ b/lib/angel_validate.dart @@ -0,0 +1,4 @@ +export 'src/field.dart'; +export 'src/form.dart'; +export 'src/form_renderer.dart'; +export 'src/matchers.dart'; diff --git a/lib/src/form.dart b/lib/src/form.dart index 15e1f37f..6910874e 100644 --- a/lib/src/form.dart +++ b/lib/src/form.dart @@ -9,25 +9,24 @@ import 'field.dart'; /// /// Example: /// ```dart -/// import 'package:angel_forms/angel_forms.dart'; /// import 'package:angel_validate/angel_validate.dart'; /// /// var myForm = Form(fields: [ -/// TextField('username').match([]), +/// TextField('username').match([minLength(8)]), /// TextField('password', confirmedAs: 'confirm_password'), /// ]) /// /// app.post('/login', (req, res) async { -/// var loginRequest = -/// await myForm.decode(req, loginRequestSerializer); -/// // Do something with the decode object... +/// var loginBody = +/// await myForm.decode(req, loginBodySerializer); +/// // Do something with the decoded object... /// }); /// ``` class Form { + /// A custom error message to provide the user if validation fails. final String errorMessage; final List _fields = []; - final List _errors = []; static const String defaultErrorMessage = 'There were errors in your submission. ' @@ -37,10 +36,11 @@ class Form { fields?.forEach(addField); } + /// Returns the fields in this form. List get fields => _fields; - List get errors => _errors; - + /// Helper for adding fields. Passing [matchers] will result in them + /// being applied to the [field]. Field addField(Field field, {Iterable matchers}) { if (matchers != null) { field = field.match(matchers); @@ -49,15 +49,19 @@ class Form { return field; } + /// Deserializes the result of calling [validate]. Future deserialize( RequestContext req, T Function(Map) f) { return validate(req).then(f); } + /// Uses the [codec] to [deserialize] the result of calling [validate]. Future decode(RequestContext req, Codec codec) { return deserialize(req, codec.decode); } + /// Calls [read], and returns the filtered request body. + /// If there is even one error, then an [AngelHttpException] is thrown. Future> validate(RequestContext req) async { var result = await read(req); if (!result.isSuccess) { diff --git a/lib/src/matchers.dart b/lib/src/matchers.dart new file mode 100644 index 00000000..84b95731 --- /dev/null +++ b/lib/src/matchers.dart @@ -0,0 +1,76 @@ +import 'package:matcher/matcher.dart'; + +final RegExp _alphaDash = RegExp(r'^[A-Za-z0-9_-]+$'); +final RegExp _alphaNum = RegExp(r'^[A-Za-z0-9]+$'); +final RegExp _email = RegExp( + r"^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"); +final RegExp _url = RegExp( + r'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)'); + +/// Asserts that a `String` is alphanumeric, but also lets it contain dashes or underscores. +final Matcher isAlphaDash = predicate( + (value) => value is String && _alphaDash.hasMatch(value), + 'alphanumeric (dashes and underscores are allowed)'); + +/// Asserts that a `String` is alphanumeric, but also lets it contain dashes or underscores. +/// +final Matcher isAlphaNum = predicate( + (value) => value is String && _alphaNum.hasMatch(value), 'alphanumeric'); + +/// Asserts that a value either equals `true` or `false`. +final Matcher isBool = predicate((value) => value is bool, 'a bool'); + +/// Asserts that a `String` complies to the RFC 5322 e-mail standard. +final Matcher isEmail = predicate( + (value) => value is String && _email.hasMatch(value), + 'a valid e-mail address'); + +/// Asserts that a value is an `int`. +final Matcher isInt = predicate((value) => value is int, 'an integer'); + +/// Asserts that a value is a `num`. +final Matcher isNum = predicate((value) => value is num, 'a number'); + +/// Asserts that a value is a `String`. +final Matcher isString = predicate((value) => value is String, 'a string'); + +/// Asserts that a value is a non-empty `String`. +final Matcher isNonEmptyString = predicate( + (value) => value is String && value.trim().isNotEmpty, + 'a non-empty string'); + +/// Asserts that a value, presumably from a checkbox, is positive. +final Matcher isChecked = + isIn(const ['yes', 'checked', 'on', '1', 1, 1.0, true, 'true']); + +/// Ensures that a string is an ISO-8601 date string. +final Matcher isIso8601DateString = predicate( + (x) { + try { + return x is String && DateTime.parse(x) != null; + } catch (_) { + return false; + } + }, + 'a valid ISO-8601 date string.', +); + +/// Asserts that a `String` is an `http://` or `https://` URL. +/// +/// The regular expression used: +/// ``` +/// https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*) +/// ``` +final Matcher isUrl = predicate( + (value) => value is String && _url.hasMatch(value), + 'a valid url, starting with http:// or https://'); + +/// Enforces a minimum length on a string. +Matcher minLength(int length) => predicate( + (value) => value is String && value.length >= length, + 'a string at least $length character(s) long'); + +/// Limits the maximum length of a string. +Matcher maxLength(int length) => predicate( + (value) => value is String && value.length <= length, + 'a string no longer than $length character(s) long');