From 60799ac5f9644e7df3f8e662aacefef9b640a0a8 Mon Sep 17 00:00:00 2001 From: Artur Assis Alves Date: Fri, 30 Aug 2024 02:07:59 -0300 Subject: [PATCH] implemented the new prototype --- README.md | 1 + example/lib/api_refactoring_main.dart | 157 ++-- example/lib/password_benchmark.dart | 84 +++ example/pubspec.yaml | 1 + lib/form_builder_validators.dart | 3 +- lib/new_api_prototype.dart | 895 +++++++++++++++++++++++ lib/src/base_elementary_validator.dart | 209 ------ lib/src/identity/password_validator.dart | 2 +- 8 files changed, 1079 insertions(+), 273 deletions(-) create mode 100644 example/lib/password_benchmark.dart create mode 100644 lib/new_api_prototype.dart diff --git a/README.md b/README.md index 97cd3d36..45e2db6e 100644 --- a/README.md +++ b/README.md @@ -431,6 +431,7 @@ validator, add a type validator, and then chain as many specialized validators a ```dart // In this example, we build a validator composing a required, with a numeric and then a max. // The logic result is: required && numeric && max(70) + final validator = ValidatorBuilder.required(and: >[ ValidatorBuilder.numeric( errorText: 'La edad debe ser numérica.', diff --git a/example/lib/api_refactoring_main.dart b/example/lib/api_refactoring_main.dart index d4a8cb5c..52e7d217 100644 --- a/example/lib/api_refactoring_main.dart +++ b/example/lib/api_refactoring_main.dart @@ -32,22 +32,6 @@ class MyApp extends StatelessWidget { } } -final class _NonNegativeNum - extends BaseElementaryValidator { - _NonNegativeNum() : super(ignoreErrorMessage: false); - - @override - (bool, T?) transformValueIfValid(T value) { - if (value >= 0) { - return (true, value); - } - return (false, null); - } - - @override - String get translatedErrorText => 'We cannot have a negative age'; -} - /// Represents the home page of the application. class NewAPIHomePage extends StatelessWidget { /// Constructs a new instance of the [NewAPIHomePage] class. @@ -67,14 +51,11 @@ class NewAPIHomePage extends StatelessWidget { decoration: const InputDecoration(labelText: 'Age'), keyboardType: TextInputType.number, autovalidateMode: AutovalidateMode.always, - validator: FormBuilderValidators.compose([ - ValidatorBuilder.required(and: >[ - ValidatorBuilder.numeric( - errorText: 'La edad debe ser numérica.', - and: >[ - ValidatorBuilder.max(70), - ]) - ]).validate, + validator: + FormBuilderValidators.compose(>[ + req( + isNum(max(70), isNumMessage: 'La edad debe ser numérica.'), + ), /// Include your own custom `FormFieldValidator` function, if you want /// Ensures positive values only. We could also have used `FormBuilderValidators.min( 0)` instead @@ -87,7 +68,9 @@ class NewAPIHomePage extends StatelessWidget { return null; } ]), + /* + // Original api FormBuilderValidators.compose(>[ /// Makes this field required FormBuilderValidators.required(), @@ -119,16 +102,19 @@ class NewAPIHomePage extends StatelessWidget { const InputDecoration(labelText: 'Age (better way to do)'), keyboardType: TextInputType.number, autovalidateMode: AutovalidateMode.always, - validator: - // Equivalent to: required && numeric && max(70) && _NonNegativeNum - ValidatorBuilder.required(and: >[ - ValidatorBuilder.numeric( - errorText: 'La edad debe ser numérica.', - and: >[ - ValidatorBuilder.max(70), - _NonNegativeNum(), - ]) - ]).validate, + validator: req( + isNum( + and([ + max(70), + (num value) { + if (value < 0) return 'We cannot have a negative age'; + + return null; + }, + ]), + isNumMessage: 'La edad debe ser numérica.', + ), + ), ), // Required Validator @@ -138,7 +124,7 @@ class NewAPIHomePage extends StatelessWidget { prefixIcon: Icon(Icons.star), ), // validator: FormBuilderValidators.required(), - validator: ValidatorBuilder.required().validate, + validator: req(null), autofillHints: const [AutofillHints.name], textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, @@ -151,11 +137,11 @@ class NewAPIHomePage extends StatelessWidget { ), keyboardType: TextInputType.number, //validator: FormBuilderValidators.numeric(), - validator: ValidatorBuilder.required( - ignoreErrorMessage: true, - and: >[ - ValidatorBuilder.numeric() - ]).validate, + // To maintain the previous behavior, we use msg. Below, there is a more precise way of doing that. + validator: msg( + FormBuilderLocalizations.current.numericErrorText, + req(isNum(null))), + autofillHints: const [AutofillHints.oneTimeCode], textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, @@ -167,15 +153,11 @@ class NewAPIHomePage extends StatelessWidget { ), keyboardType: TextInputType.number, //validator: FormBuilderValidators.numeric(), - validator: ValidatorBuilder.required( - and: >[ - ValidatorBuilder.numeric() - ]).validate, + validator: req(isNum(null)), autofillHints: const [AutofillHints.oneTimeCode], textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, ), - /* // Email Validator TextFormField( decoration: const InputDecoration( @@ -183,7 +165,11 @@ class NewAPIHomePage extends StatelessWidget { prefixIcon: Icon(Icons.email), ), keyboardType: TextInputType.emailAddress, - validator: FormBuilderValidators.email(), + //validator: FormBuilderValidators.email(), + validator: msg( + FormBuilderLocalizations.current.emailErrorText, + req(email()), + ), autofillHints: const [AutofillHints.email], textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, @@ -195,7 +181,11 @@ class NewAPIHomePage extends StatelessWidget { prefixIcon: Icon(Icons.link), ), keyboardType: TextInputType.url, - validator: FormBuilderValidators.url(), + //validator: FormBuilderValidators.url(), + validator: msg( + FormBuilderLocalizations.current.urlErrorText, + req(url()), + ), autofillHints: const [AutofillHints.url], textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, @@ -206,7 +196,11 @@ class NewAPIHomePage extends StatelessWidget { labelText: 'Min Length Field', prefixIcon: Icon(Icons.text_fields), ), - validator: FormBuilderValidators.minLength(5), + //validator: FormBuilderValidators.minLength(5), + validator: msg( + FormBuilderLocalizations.current.minLengthErrorText(5), + req(minLength(5)), + ), textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, ), @@ -216,7 +210,11 @@ class NewAPIHomePage extends StatelessWidget { labelText: 'Max Length Field', prefixIcon: Icon(Icons.text_fields), ), - validator: FormBuilderValidators.maxLength(10), + //validator: FormBuilderValidators.maxLength(10), + validator: msg( + FormBuilderLocalizations.current.maxLengthErrorText(5), + req(maxLength(5)), + ), textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, ), @@ -227,7 +225,11 @@ class NewAPIHomePage extends StatelessWidget { prefixIcon: Icon(Icons.exposure_neg_1), ), keyboardType: TextInputType.number, - validator: FormBuilderValidators.min(10), + //validator: FormBuilderValidators.min(10), + validator: msg( + FormBuilderLocalizations.current.minErrorText(10), + req(isNum(min(10))), + ), textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, ), @@ -238,7 +240,11 @@ class NewAPIHomePage extends StatelessWidget { prefixIcon: Icon(Icons.exposure_plus_1), ), keyboardType: TextInputType.number, - validator: FormBuilderValidators.max(100), + //validator: FormBuilderValidators.max(100), + validator: msg( + FormBuilderLocalizations.current.maxErrorText(100), + req(isNum(max(100))), + ), textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, ), @@ -248,7 +254,8 @@ class NewAPIHomePage extends StatelessWidget { labelText: 'Equal Field', prefixIcon: Icon(Icons.check), ), - validator: FormBuilderValidators.equal('test'), + //validator: FormBuilderValidators.equal('test'), + validator: equal('test'), textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, ), @@ -258,7 +265,11 @@ class NewAPIHomePage extends StatelessWidget { labelText: 'Contains "test"', prefixIcon: Icon(Icons.search), ), - validator: FormBuilderValidators.contains('test'), + //validator: FormBuilderValidators.contains('test'), + validator: msg( + FormBuilderLocalizations.current.containsErrorText('test'), + req(contains('test')), + ), textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, ), @@ -268,8 +279,12 @@ class NewAPIHomePage extends StatelessWidget { labelText: 'Match Pattern', prefixIcon: Icon(Icons.pattern), ), - validator: - FormBuilderValidators.match(RegExp(r'^[a-zA-Z0-9]+$')), + //validator: + // FormBuilderValidators.match(RegExp(r'^[a-zA-Z0-9]+$')), + validator: msg( + FormBuilderLocalizations.current.matchErrorText, + req(match(RegExp(r'^[a-zA-Z0-9]+$'))), + ), textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, ), @@ -280,7 +295,11 @@ class NewAPIHomePage extends StatelessWidget { prefixIcon: Icon(Icons.computer), ), keyboardType: TextInputType.number, - validator: FormBuilderValidators.ip(), + // validator: FormBuilderValidators.ip(), + validator: msg( + FormBuilderLocalizations.current.ipErrorText, + req(ip()), + ), textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, ), @@ -290,7 +309,11 @@ class NewAPIHomePage extends StatelessWidget { labelText: 'UUID Field', prefixIcon: Icon(Icons.code), ), - validator: FormBuilderValidators.uuid(), + //validator: FormBuilderValidators.uuid(), + validator: msg( + FormBuilderLocalizations.current.uuidErrorText, + req(uuid()), + ), textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, ), @@ -301,7 +324,11 @@ class NewAPIHomePage extends StatelessWidget { prefixIcon: Icon(Icons.credit_card), ), keyboardType: TextInputType.number, - validator: FormBuilderValidators.creditCard(), + //validator: FormBuilderValidators.creditCard(), + validator: msg( + FormBuilderLocalizations.current.creditCardErrorText, + req(creditCard()), + ), autofillHints: const [AutofillHints.creditCardNumber], textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, @@ -313,7 +340,11 @@ class NewAPIHomePage extends StatelessWidget { prefixIcon: Icon(Icons.phone), ), keyboardType: TextInputType.phone, - validator: FormBuilderValidators.phoneNumber(), + //validator: FormBuilderValidators.phoneNumber(), + validator: msg( + FormBuilderLocalizations.current.phoneErrorText, + req(phoneNumber()), + ), autofillHints: const [AutofillHints.telephoneNumber], textInputAction: TextInputAction.next, autovalidateMode: AutovalidateMode.always, @@ -325,7 +356,8 @@ class NewAPIHomePage extends StatelessWidget { prefixIcon: Icon(Icons.lock), ), obscureText: true, - validator: FormBuilderValidators.password(), + //validator: FormBuilderValidators.password(), + validator: req(password()), autofillHints: const [AutofillHints.password], textInputAction: TextInputAction.done, autovalidateMode: AutovalidateMode.always, @@ -337,6 +369,7 @@ class NewAPIHomePage extends StatelessWidget { prefixIcon: Icon(Icons.calendar_today), ), keyboardType: TextInputType.number, + /* validator: FormBuilderValidators.compose(>[ FormBuilderValidators.required(), @@ -344,11 +377,11 @@ class NewAPIHomePage extends StatelessWidget { FormBuilderValidators.min(0), FormBuilderValidators.max(120), ]), + */ + validator: req(isNum(and([min(0), max(120)]))), textInputAction: TextInputAction.done, autovalidateMode: AutovalidateMode.always, ), - - */ ], ), ), diff --git a/example/lib/password_benchmark.dart b/example/lib/password_benchmark.dart new file mode 100644 index 00000000..eb048157 --- /dev/null +++ b/example/lib/password_benchmark.dart @@ -0,0 +1,84 @@ +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:form_builder_validators/new_api_prototype.dart' + show req, password; + +import 'package:flutter/material.dart'; +// ignore_for_file: public_member_api_docs + +class ReqPasswordBenchmark extends BenchmarkBase { + const ReqPasswordBenchmark() : super("ReqPasswordBenchmark"); + static void main() { + const ReqPasswordBenchmark().report(); + } + + @override + void run() { + final FormFieldValidator v = req(password()); + v('h'); + v('he'); + v('hel'); + v('hell'); + v('hello'); + v('hello '); + v('hello W'); + v('hello Wo'); + v('hello Wor'); + v('hello Worl'); + v('hello World'); + v('hello World!'); + v('hello World!1'); + v('hello World!12'); + v('hello World!123'); + } +} + +// Benchmark for FormFieldValidators.password() +class FormFieldValidatorsPasswordBenchmark extends BenchmarkBase { + const FormFieldValidatorsPasswordBenchmark() + : super("FormFieldValidatorsPasswordBenchmark"); + + static void main() { + const FormFieldValidatorsPasswordBenchmark().report(); + } + + @override + void run() { + // create the validator + final FormFieldValidator v = FormBuilderValidators.password(); + v('h'); + v('he'); + v('hel'); + v('hell'); + v('hello'); + v('hello '); + v('hello W'); + v('hello Wo'); + v('hello Wor'); + v('hello Worl'); + v('hello World'); + v('hello World!'); + v('hello World!1'); + v('hello World!12'); + v('hello World!123'); + } +} + +void main() { + // Running the benchmarks + print('executing benchmark'); + var i = 0; + while (i < 10) { + ReqPasswordBenchmark.main(); + FormFieldValidatorsPasswordBenchmark.main(); + i++; + } + + print('Finishing app'); + return; + /* + runApp(MaterialApp( + home: Placeholder(), + )); + */ +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 13f1ab39..688bf74c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: path: ../ dev_dependencies: + benchmark_harness: ^2.2.2 flutter_lints: ^4.0.0 flutter_test: sdk: flutter diff --git a/lib/form_builder_validators.dart b/lib/form_builder_validators.dart index 69ddae61..23c6a76f 100644 --- a/lib/form_builder_validators.dart +++ b/lib/form_builder_validators.dart @@ -48,4 +48,5 @@ export 'src/numeric/numeric.dart'; export 'src/string/string.dart'; export 'src/translated_validator.dart'; export 'src/usecase/usecase.dart'; -export 'src/validator_builder.dart'; +//export 'src/validator_builder.dart'; +export 'new_api_prototype.dart'; diff --git a/lib/new_api_prototype.dart b/lib/new_api_prototype.dart new file mode 100644 index 00000000..93609493 --- /dev/null +++ b/lib/new_api_prototype.dart @@ -0,0 +1,895 @@ +// ignore_for_file: always_specify_types, prefer_const_constructors, public_member_api_docs + +import 'localization/l10n.dart'; + +typedef Validator = String? Function(T); + +// Emptiness check validator: +Validator isRequired( + Validator? v, { + String? isRequiredMessage, +}) { + String? finalValidator(T? value) { + final (isValid, transformedValue) = requiredValidatorAndTransformer(value); + if (!isValid) { + return isRequiredMessage ?? + FormBuilderLocalizations.current.requiredErrorText; + } + return v?.call(transformedValue!); + } + + return finalValidator; +} + +Validator isOptional( + Validator? v, { + String? isOptionalMessage, +}) { + String? finalValidator(T? value) { + final (isValid, transformedValue) = requiredValidatorAndTransformer(value); + if (!isValid) { + // field not provided + return null; + } + final vErrorMessage = v?.call(transformedValue!); + if (vErrorMessage == null) { + return null; + } + + return 'The field is optional, otherwise, $vErrorMessage'; + } + + return finalValidator; +} + +const req = isRequired; +const opt = isOptional; + +(bool, T?) requiredValidatorAndTransformer(T? value) { + if (value != null && + (value is! String || value.trim().isNotEmpty) && + (value is! Iterable || value.isNotEmpty) && + (value is! Map || value.isNotEmpty)) { + return (true, value); + } + return (false, null); +} + +// Type validator: + +Validator isString(Validator? v, + {String? isStringMessage}) { + String? finalValidator(T value) { + final (isValid, typeTransformedValue) = + isStringValidatorAndTransformer(value); + if (!isValid) { + return isStringMessage ?? 'This field requires a valid string.'; + } + return v?.call(typeTransformedValue!); + } + + return finalValidator; +} + +(bool, String?) isStringValidatorAndTransformer(T value) { + if (value is String) { + return (true, value); + } + return (false, null); +} + +Validator isInt(Validator? v, + {String? isIntMessage}) { + String? finalValidator(T value) { + final (isValid, typeTransformedValue) = isIntValidatorAndTransformer(value); + if (!isValid) { + return isIntMessage ?? FormBuilderLocalizations.current.integerErrorText; + } + return v?.call(typeTransformedValue!); + } + + return finalValidator; +} + +(bool, int?) isIntValidatorAndTransformer(T value) { + if (value is int) { + return (true, value); + } + if (value is String) { + final int? candidateValue = int.tryParse(value); + if (candidateValue != null) { + return (true, candidateValue); + } + } + return (false, null); +} + +Validator isNum(Validator? v, + {String? isNumMessage}) { + String? finalValidator(T value) { + final (isValid, typeTransformedValue) = isNumValidatorAndTransformer(value); + if (!isValid) { + return isNumMessage ?? FormBuilderLocalizations.current.numericErrorText; + } + return v?.call(typeTransformedValue!); + } + + return finalValidator; +} + +(bool, num?) isNumValidatorAndTransformer(T value) { + if (value is num) { + return (true, value); + } + if (value is String) { + final num? candidateValue = num.tryParse(value); + if (candidateValue != null) { + return (true, candidateValue); + } + } + return (false, null); +} + +Validator isBool(Validator? v, + {String? isBoolMessage}) { + String? finalValidator(T value) { + final (isValid, typeTransformedValue) = + isBoolValidatorAndTransformer(value); + if (!isValid) { + return isBoolMessage ?? FormBuilderLocalizations.current.numericErrorText; + } + return v?.call(typeTransformedValue!); + } + + return finalValidator; +} + +(bool, bool?) isBoolValidatorAndTransformer(T value) { + if (value is bool) { + return (true, value); + } + if (value is String) { + final bool? candidateValue = bool.tryParse(value.toLowerCase()); + if (candidateValue != null) { + return (true, candidateValue); + } + } + return (false, null); +} + +// Placeholder validator +String? placeholder(Object value) { + return null; +} + +const ph = placeholder; + +// Composition validators +Validator and( + List> validators, { + bool printErrorAsSoonAsPossible = true, +}) { + return (T value) { + final errorMessageBuilder = []; + for (final validator in validators) { + final errorMessage = validator(value); + if (errorMessage != null) { + if (printErrorAsSoonAsPossible) { + return errorMessage; + } + errorMessageBuilder.add(errorMessage); + } + } + if (errorMessageBuilder.isNotEmpty) { + return errorMessageBuilder.join(' AND '); + } + + return null; + }; +} + +Validator or(List> validators) { + return (T value) { + final errorMessageBuilder = []; + for (final validator in validators) { + final errorMessage = validator(value); + if (errorMessage == null) { + return null; + } + errorMessageBuilder.add(errorMessage); + } + return errorMessageBuilder.join(' OR '); + }; +} + +// String validators + +const _minL = minLength; +const _maxL = maxLength; +Validator password({ + int minLength = 8, + int maxLength = 32, + int minUppercaseCount = 1, + int minLowercaseCount = 1, + int minNumberCount = 1, + int minSpecialCharCount = 1, + String? passwordMessage, +}) { + final andValidator = and([ + _minL(minLength), + _maxL(maxLength), + hasMinUppercase(min: minUppercaseCount), + hasMinLowercase(min: minLowercaseCount), + hasMinNumeric(min: minNumberCount), + hasMinSpecial(min: minSpecialCharCount), + ]); + return passwordMessage != null + ? msg(passwordMessage, andValidator) + : andValidator; +} + +final _upperCaseRegex = RegExp('[A-Z]'); +final _lowerCaseRegex = RegExp('[a-z]'); +final _numericRegex = RegExp('[0-9]'); +final _specialRegex = RegExp('[^A-Za-z0-9]'); +Validator hasMinUppercase({ + int min = 1, + RegExp? regex, + String Function(int)? hasMinUppercaseMessage, +}) { + return (value) { + return (regex ?? _upperCaseRegex).allMatches(value).length >= min + ? null + : hasMinUppercaseMessage?.call(min) ?? + FormBuilderLocalizations.current + .containsUppercaseCharErrorText(min); + }; +} + +Validator hasMinLowercase({ + int min = 1, + RegExp? regex, + String Function(int)? hasMinLowercaseMessage, +}) { + return (value) { + return (regex ?? _lowerCaseRegex).allMatches(value).length >= min + ? null + : hasMinLowercaseMessage?.call(min) ?? + FormBuilderLocalizations.current + .containsLowercaseCharErrorText(min); + }; +} + +Validator hasMinNumeric({ + int min = 1, + RegExp? regex, + String Function(int)? hasMinNumericMessage, +}) { + return (value) { + return (regex ?? _numericRegex).allMatches(value).length >= min + ? null + : hasMinNumericMessage?.call(min) ?? + FormBuilderLocalizations.current.containsNumberErrorText(min); + }; +} + +Validator hasMinSpecial({ + int min = 1, + RegExp? regex, + String Function(int)? hasMinSpecialMessage, +}) { + return (value) { + return (regex ?? _specialRegex).allMatches(value).length >= min + ? null + : hasMinSpecialMessage?.call(min) ?? + FormBuilderLocalizations.current.containsSpecialCharErrorText(min); + }; +} + +Validator match( + RegExp regex, { + String? matchMessage, +}) { + return (value) { + return regex.hasMatch(value) + ? null + : matchMessage ?? FormBuilderLocalizations.current.matchErrorText; + }; +} + +final _uuidRegex = RegExp( + r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', +); +Validator uuid({ + RegExp? regex, + String? uuidMessage, +}) { + return (value) { + return (regex ?? _uuidRegex).hasMatch(value) + ? null + : uuidMessage ?? FormBuilderLocalizations.current.uuidErrorText; + }; +} + +final _creditCardRegex = RegExp( + r'^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$', +); +Validator creditCard({ + RegExp? regex, + String? creditCardMessage, +}) { + return (value) { + return isCreditCard(value, regex ?? _creditCardRegex) + ? null + : creditCardMessage ?? + FormBuilderLocalizations.current.creditCardErrorText; + }; +} + +final _phoneNumberRegex = RegExp( + r'^\+?(\d{1,4}[\s.-]?)?(\(?\d{1,4}\)?[\s.-]?)?(\d{1,4}[\s.-]?)?(\d{1,4}[\s.-]?)?(\d{1,9})$', +); +Validator phoneNumber({ + RegExp? regex, + String? phoneNumberMessage, +}) { + return (value) { + final String phoneNumber = value.replaceAll(' ', '').replaceAll('-', ''); + return (regex ?? _phoneNumberRegex).hasMatch(phoneNumber) + ? null + : phoneNumberMessage ?? FormBuilderLocalizations.current.phoneErrorText; + }; +} + +Validator contains( + String substring, { + bool caseSensitive = true, + String Function(String)? containsMessage, +}) { + return (value) { + if (substring.isEmpty) { + return null; + } else if (caseSensitive + ? value.contains(substring) + : value.toLowerCase().contains(substring.toLowerCase())) { + return null; + } + return containsMessage?.call(substring) ?? + FormBuilderLocalizations.current.containsErrorText(substring); + }; +} + +Validator email({ + RegExp? regex, + String? emailMessage, +}) { + final defaultRegex = RegExp( + r"^((([a-z]|\d|[!#\$%&'*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$", + ); + return (value) { + return (regex ?? defaultRegex).hasMatch(value.toLowerCase()) + ? null + : emailMessage ?? FormBuilderLocalizations.current.emailErrorText; + }; +} + +Validator url({ + List? protocols, + bool requireTld = true, + bool requireProtocol = false, + bool allowUnderscore = false, + List? hostWhitelist, + List? hostBlacklist, + RegExp? regex, + String? urlMessage, +}) { + const defaultProtocols = ['http', 'https', 'ftp']; + return (value) { + return (regex != null && !regex.hasMatch(value)) || + !isURL( + value, + protocols: protocols ?? defaultProtocols, + requireTld: requireTld, + requireProtocol: requireProtocol, + allowUnderscore: allowUnderscore, + hostWhitelist: hostWhitelist ?? [], + hostBlacklist: hostBlacklist ?? [], + ) + ? urlMessage ?? FormBuilderLocalizations.current.urlErrorText + : null; + }; +} + +Validator ip({ + int version = 4, + RegExp? regex, + String? ipMessage, +}) { + return (value) { + return !isIP(value, version, regex) + ? ipMessage ?? FormBuilderLocalizations.current.ipErrorText + : null; + }; +} + +// T validators +Validator equal(T value, + {String Function(String)? equalMessage}) { + return (input) { + final valueString = value.toString(); + return value == input + ? null + : equalMessage?.call(valueString) ?? + FormBuilderLocalizations.current.equalErrorText(valueString); + }; +} + +Validator minLength(int minLength, + {String Function(int)? minLengthMessage}) { + return (value) { + // I think it makes more sense to say that scalar objects has length 1 instead of 0. + int valueLength = 1; + + if (value is String) valueLength = value.length; + if (value is Iterable) valueLength = value.length; + if (value is Map) valueLength = value.length; + + return valueLength < minLength + ? minLengthMessage?.call(minLength) ?? + FormBuilderLocalizations.current.minLengthErrorText(minLength) + : null; + }; +} + +Validator maxLength(int maxLength, + {String Function(int)? maxLengthMessage}) { + return (value) { + // I think it makes more sense to say that scalar objects has length 1 instead of 0. + int valueLength = 1; + + if (value is String) valueLength = value.length; + if (value is Iterable) valueLength = value.length; + if (value is Map) valueLength = value.length; + + return valueLength > maxLength + ? maxLengthMessage?.call(maxLength) ?? + FormBuilderLocalizations.current.maxLengthErrorText(maxLength) + : null; + }; +} + +// Numeric validators +Validator max(T? max, + {T Function()? dynMax, + bool inclusive = true, + String Function(num)? maxMessage}) { + assert( + max != null && dynMax == null || max == null && dynMax != null, + 'Exactly one of the inputs must be null', + ); + + return (value) { + late final T actualMax; + if (max != null) { + actualMax = max; + } else if (dynMax != null) { + actualMax = dynMax(); + } + + return (inclusive ? (value <= actualMax) : (value < actualMax)) + ? null + : maxMessage?.call(actualMax) ?? + FormBuilderLocalizations.current.maxErrorText(actualMax); + }; +} + +Validator min(T? min, + {T Function()? dynMin, + bool inclusive = true, + String Function(num)? minMessage}) { + assert( + min != null && dynMin == null || min == null && dynMin != null, + 'Exactly one of the inputs must be null', + ); + + return (value) { + late final T actualMin; + if (min != null) { + actualMin = min; + } else if (dynMin != null) { + actualMin = dynMin(); + } + + return (inclusive ? (value >= actualMin) : (value > actualMin)) + ? null + : minMessage?.call(actualMin) ?? + FormBuilderLocalizations.current.minErrorText(actualMin); + }; +} + +Validator greaterThan(T? n, + [T Function()? dynN, String Function(num)? greaterThanMessage]) { + assert( + n != null && dynN == null || n == null && dynN != null, + 'Exactly one of the inputs must be null', + ); + return (value) { + final T actualN; + if (n != null) { + actualN = n; + } else if (dynN != null) { + actualN = dynN(); + } else { + throw TypeError(); + } + return value > actualN + ? null + : greaterThanMessage?.call(actualN) ?? + 'Value must be greater than $actualN'; + }; +} + +Validator greaterThanOrEqual(T? n, + [T Function()? dynN, String Function(num)? greaterThanOrEqualMessage]) { + assert( + n != null && dynN == null || n == null && dynN != null, + 'Exactly one of the inputs must be null', + ); + return (value) { + final T actualN; + if (n != null) { + actualN = n; + } else if (dynN != null) { + actualN = dynN(); + } else { + throw TypeError(); + } + return value >= actualN + ? null + : greaterThanOrEqualMessage?.call(actualN) ?? + 'Value must be greater than or equal to $actualN'; + }; +} + +Validator lessThan(T? n, + [T Function()? dynN, String Function(num)? lessThanMessage]) { + assert( + n != null && dynN == null || n == null && dynN != null, + 'Exactly one of the inputs must be null', + ); + return (value) { + final T actualN; + if (n != null) { + actualN = n; + } else if (dynN != null) { + actualN = dynN(); + } else { + throw TypeError(); + } + return value < actualN + ? null + : lessThanMessage?.call(actualN) ?? 'Value must be less than $actualN'; + }; +} + +Validator lessThanOrEqual(T? n, + [T Function()? dynN, String Function(num)? lessThanOrEqualMessage]) { + assert( + n != null && dynN == null || n == null && dynN != null, + 'Exactly one of the inputs must be null', + ); + return (value) { + final T actualN; + if (n != null) { + actualN = n; + } else if (dynN != null) { + actualN = dynN(); + } else { + throw TypeError(); + } + return value <= actualN + ? null + : lessThanOrEqualMessage?.call(actualN) ?? + 'Value must be less than or equal to $actualN'; + }; +} + +Validator between(T? min, T? max, + {T Function()? dynMin, + T Function()? dynMax, + bool leftInclusive = true, + bool rightInclusive = true, + String Function(num min, num max)? betweenMessage}) { + assert( + min != null && dynMin == null || min == null && dynMin != null, + 'Exactly one of the min inputs must be null', + ); + assert( + max != null && dynMax == null || max == null && dynMax != null, + 'Exactly one of the max inputs must be null', + ); + return (value) { + final T actualMin; + final T actualMax; + if (min != null) { + actualMin = min; + } else if (dynMin != null) { + actualMin = dynMin(); + } else { + throw TypeError(); + } + if (max != null) { + actualMax = max; + } else if (dynMax != null) { + actualMax = dynMax(); + } else { + throw TypeError(); + } + return (leftInclusive ? value >= actualMin : value > actualMin) && + (rightInclusive ? value <= actualMax : value < actualMax) + ? null + : betweenMessage?.call(actualMin, actualMax) ?? + 'Value must be greater than ${leftInclusive ? 'or equal to ' : ''}$actualMin and less than ${rightInclusive ? 'or equal to ' : ''}$actualMax'; + }; +} + +const gt = greaterThan; +const gtE = greaterThanOrEqual; +const lt = lessThan; +const ltE = lessThanOrEqual; +const bw = between; + +// bool validators +String? isTrue(bool value) => + value ? null : FormBuilderLocalizations.current.mustBeTrueErrorText; + +String? isFalse(bool value) => + value ? FormBuilderLocalizations.current.mustBeFalseErrorText : null; + +// msg wrapper +/// Replaces any inner message with [errorMessage]. It is useful also for changing +/// the message of direct validator implementations, like [isTrue] or [isFalse]. +Validator withMessage( + String errorMessage, + Validator v, +) { + return (value) { + final vErrorMessage = v(value); + if (vErrorMessage != null) { + return errorMessage; + } + return null; + }; +} + +const msg = withMessage; + +//****************************************************************************** +//* Aux functions * +//****************************************************************************** +const int _maxUrlLength = 2083; +final RegExp _ipv4Maybe = + RegExp(r'^(\d?\d?\d)\.(\d?\d?\d)\.(\d?\d?\d)\.(\d?\d?\d)$'); +final RegExp _ipv6 = RegExp( + r'^((?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(?::0{1,4}){0,1}:){0,1}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$', +); + +/// Check if the string [str] is IP [version] 4 or 6. +/// +/// * [version] is a String or an `int`. +bool isIP(String? str, int version, RegExp? regex) { + if (version != 4 && version != 6) { + return isIP(str, 4, regex) || isIP(str, 6, regex); + } else if (version == 4) { + if (regex != null) { + return regex.hasMatch(str!); + } else if (!_ipv4Maybe.hasMatch(str!)) { + return false; + } + final List parts = str.split('.') + ..sort((String a, String b) => int.parse(a) - int.parse(b)); + return int.parse(parts[3]) <= 255; + } else if (regex != null) { + return regex.hasMatch(str!); + } + return version == 6 && _ipv6.hasMatch(str!); +} + +/// Check if the string [value] is a URL. +/// +/// * [protocols] sets the list of allowed protocols +/// * [requireTld] sets if TLD is required +/// * [requireProtocol] is a `bool` that sets if protocol is required for validation +/// * [allowUnderscore] sets if underscores are allowed +/// * [hostWhitelist] sets the list of allowed hosts +/// * [hostBlacklist] sets the list of disallowed hosts +bool isURL( + String? value, { + List protocols = const ['http', 'https', 'ftp'], + bool requireTld = true, + bool requireProtocol = false, + bool allowUnderscore = false, + List hostWhitelist = const [], + List hostBlacklist = const [], + RegExp? regexp, +}) { + if (value == null || + value.isEmpty || + value.length > _maxUrlLength || + value.startsWith('mailto:')) { + return false; + } + final int port; + final String? protocol; + final String? auth; + final String user; + final String host; + final String hostname; + final String portStr; + final String path; + final String query; + final String hash; + + // check protocol + List split = value.split('://'); + if (split.length > 1) { + protocol = shift(split).toLowerCase(); + if (!protocols.contains(protocol)) { + return false; + } + } else if (requireProtocol == true) { + return false; + } + final String str1 = split.join('://'); + + // check hash + split = str1.split('#'); + final String str2 = shift(split); + hash = split.join('#'); + if (hash.isNotEmpty && RegExp(r'\s').hasMatch(hash)) { + return false; + } + + // check query params + split = str2.split('?'); + final String str3 = shift(split); + query = split.join('?'); + if (query.isNotEmpty && RegExp(r'\s').hasMatch(query)) { + return false; + } + + // check path + split = str3.split('/'); + final String str4 = shift(split); + path = split.join('/'); + if (path.isNotEmpty && RegExp(r'\s').hasMatch(path)) { + return false; + } + + // check auth type urls + split = str4.split('@'); + if (split.length > 1) { + auth = shift(split); + if (auth?.contains(':') ?? false) { + user = shift(auth!.split(':')); + if (!RegExp(r'^\S+$').hasMatch(user)) { + return false; + } + if (!RegExp(r'^\S*$').hasMatch(user)) { + return false; + } + } + } + + // check hostname + hostname = split.join('@'); + split = hostname.split(':'); + host = shift(split).toLowerCase(); + if (split.isNotEmpty) { + portStr = split.join(':'); + try { + port = int.parse(portStr, radix: 10); + } catch (e) { + return false; + } + if (!RegExp(r'^[0-9]+$').hasMatch(portStr) || port <= 0 || port > 65535) { + return false; + } + } + + if (!isIP(host, 0, regexp) && + !isFQDN( + host, + requireTld: requireTld, + allowUnderscores: allowUnderscore, + ) && + host != 'localhost') { + return false; + } + + if (hostWhitelist.isNotEmpty && !hostWhitelist.contains(host)) { + return false; + } + + if (hostBlacklist.isNotEmpty && hostBlacklist.contains(host)) { + return false; + } + + return true; +} + +/// Check if the string [str] is a fully qualified domain name (e.g., domain.com). +/// +/// * [requireTld] sets if TLD is required +/// * [allowUnderscores] sets if underscores are allowed +bool isFQDN( + String str, { + bool requireTld = true, + bool allowUnderscores = false, +}) { + final List parts = str.split('.'); + if (requireTld) { + final String tld = parts.removeLast(); + if (parts.isEmpty || !RegExp(r'^[a-z]{2,}$').hasMatch(tld)) { + return false; + } + } + + final String partPattern = allowUnderscores + ? r'^[a-z\u00a1-\uffff0-9-_]+$' + : r'^[a-z\u00a1-\uffff0-9-]+$'; + + for (final String part in parts) { + if (!RegExp(partPattern).hasMatch(part)) { + return false; + } + if (part[0] == '-' || + part[part.length - 1] == '-' || + part.contains('---') || + (allowUnderscores && part.contains('__'))) { + return false; + } + } + return true; +} + +/// Remove and return the first element from a list. +T shift(List l) { + if (l.isNotEmpty) { + final T first = l.first; + l.removeAt(0); + return first; + } + return null as T; +} + +bool isCreditCard(String value, RegExp regex) { + final String sanitized = value.replaceAll(RegExp('[^0-9]+'), ''); + if (!regex.hasMatch(sanitized)) { + return false; + } + + // Luhn algorithm + int sum = 0; + String digit; + bool shouldDouble = false; + + for (int i = sanitized.length - 1; i >= 0; i--) { + digit = sanitized.substring(i, i + 1); + int tmpNum = int.parse(digit); + + if (shouldDouble) { + tmpNum *= 2; + if (tmpNum >= 10) { + sum += (tmpNum % 10) + 1; + } else { + sum += tmpNum; + } + } else { + sum += tmpNum; + } + shouldDouble = !shouldDouble; + } + + return (sum % 10 == 0); +} diff --git a/lib/src/base_elementary_validator.dart b/lib/src/base_elementary_validator.dart index 6d15365e..2ffbba99 100644 --- a/lib/src/base_elementary_validator.dart +++ b/lib/src/base_elementary_validator.dart @@ -121,212 +121,3 @@ abstract base class BaseElementaryValidator>? otherwise; } - -/* -final class RequiredValidator - extends BaseElementaryValidator { - const RequiredValidator({ - super.and, - super.otherwise, - }); - - @override - String get _errorText => 'Value is required.'; - - @override - (bool, T?) transformValueIfValid(T? value) { - if (value != null && - (value is! String || value.trim().isNotEmpty) && - (value is! Iterable || value.isNotEmpty) && - (value is! Map || value.isNotEmpty)) { - return (true, value); - } - return (false, null); - } -} - -final class NotRequiredValidator - extends BaseElementaryValidator { - const NotRequiredValidator({ - super.and, - // in this case, the or is more restricted, thus, we need to restrict its - // type in the constructor. - List>? otherwise, - }) : super(otherwise: otherwise); - - @override - String get _errorText => 'Value must not be provided.'; - - @override - (bool, T?) transformValueIfValid(T? value) { - if (value == null || - (value is String && value.trim().isEmpty) || - (value is Iterable && value.isEmpty) || - (value is Map && value.isEmpty)) { - return (true, value); - } - return (false, null); - } -} - -final class IsBool extends BaseElementaryValidator { - const IsBool({ - super.and, - super.otherwise, - }); - - @override - String get _errorText => 'Value must be true/false'; - - @override - (bool, bool?) transformValueIfValid(T value) { - if (value is String) { - final processedValue = value.trim().toLowerCase(); - - switch (processedValue) { - case 'true': - return (true, true); - case 'false': - return (true, false); - } - } - if (value is bool) { - return (true, value); - } - return (false, null); - } -} - -final class IsInt extends BaseElementaryValidator { - const IsInt({ - super.and, - super.otherwise, - }); - - @override - String get _errorText => 'Value must be int'; - - @override - (bool, int?) transformValueIfValid(T value) { - if (value is String) { - final intCandidateValue = int.tryParse(value); - if (intCandidateValue != null) { - return (true, intCandidateValue); - } - } - if (value is int) { - return (true, value); - } - return (false, null); - } -} - -final class IsLessThan extends BaseElementaryValidator { - const IsLessThan( - this.reference, { - super.and, - super.otherwise, - }); - final T reference; - - @override - String get _errorText => 'Value must be less than $reference'; - - @override - (bool, T?) transformValueIfValid(T value) { - final isValid = value < reference; - return (isValid, isValid ? value : null); - } -} - -final class IsGreaterThan extends BaseElementaryValidator { - const IsGreaterThan( - this.reference, { - super.and, - super.otherwise, - }); - final T reference; - - @override - String get _errorText => 'Value must be greater than $reference'; - - @override - (bool, T?) transformValueIfValid(T value) { - final isValid = value > reference; - return (isValid, isValid ? value : null); - } -} - -final class StringLengthLessThan - extends BaseElementaryValidator { - const StringLengthLessThan({required this.referenceValue}) - : assert(referenceValue > 0); - final int referenceValue; - - @override - String get _errorText => 'Length must be less than $referenceValue'; - - @override - (bool, String?) transformValueIfValid(String value) { - final isValid = value.length < referenceValue; - return (isValid, isValid ? value : null); - } -} -*/ - -/* -void main() { - print('-------------New validation-------------------------'); - print('Enter the value: '); - final value = stdin.readLineSync(); - - const requiredIntLessThan10Validator = RequiredValidator( - and: [ - IsInt( - and: [IsLessThan(10)], - ), - ], - ); - - const requiredIntLessThan10OrGreaterThan13OrBool = RequiredValidator( - and: [ - IsInt( - and: [ - IsGreaterThan(13, otherwise: [IsLessThan(10)]) - ], - ), - IsBool(), - ], - ); - - const optionalDescriptionText = NotRequiredValidator( - otherwise: [ - StringLengthLessThan(referenceValue: 10), - ], - ); - - // this validator does not compile, because it does not make sense to compare - // a bool with an integer - /* - const validator = RequiredValidator( - and: [ - IsInt( - and: [ - IsBool( - and: [IsGreaterThan(13)], - ) - ], - ) - ], - otherwise: [ - IsInt(), - ], - ); - - */ - final validation = optionalDescriptionText.validate(value); - - print(validation ?? 'Valid value!'); -} - - */ diff --git a/lib/src/identity/password_validator.dart b/lib/src/identity/password_validator.dart index 5065e109..41899488 100644 --- a/lib/src/identity/password_validator.dart +++ b/lib/src/identity/password_validator.dart @@ -79,6 +79,6 @@ class PasswordValidator extends BaseValidator { ), ], ).call(valueCandidate); - return result != null && errorText != '' ? errorText : result; + return result != null && errorText != null ? errorText : result; } }