From 7d0eeedc6c4ca786a984e783963bde611af745b1 Mon Sep 17 00:00:00 2001 From: Jacob Moura Date: Tue, 27 Aug 2024 09:59:43 -0300 Subject: [PATCH] start localization --- .../lib/domain/validations/extensions.dart | 2 +- lib/lucid_validation.dart | 24 ++++++++ lib/src/localization/language.dart | 59 +++++++++++++++++++ lib/src/localization/language_manager.dart | 27 +++++++++ .../languages/english_language.dart | 5 ++ lib/src/localization/localization.dart | 3 + lib/src/lucid_validation_builder.dart | 43 ++++++++------ lib/src/validations/equal_validation.dart | 26 ++++++-- .../validations/greater_than_validation.dart | 30 +++++++--- lib/src/validations/is_empty_validation.dart | 23 +++++--- .../validations/is_not_null_validation.dart | 25 +++++--- ....dart => must_have_number_validation.dart} | 2 +- lib/src/validations/validations.dart | 2 +- test/lucid_validation_test.dart | 55 +++++++---------- test/mocks/mocks.dart | 22 +++---- .../validations/equal_validation_test.dart | 24 ++++++++ .../greater_than_validation_test.dart | 21 +++++++ .../validations/is_empty_validation_test.dart | 23 ++++++++ 18 files changed, 322 insertions(+), 94 deletions(-) create mode 100644 lib/src/localization/language.dart create mode 100644 lib/src/localization/language_manager.dart create mode 100644 lib/src/localization/languages/english_language.dart create mode 100644 lib/src/localization/localization.dart rename lib/src/validations/{must_have_numbers_validation.dart => must_have_number_validation.dart} (86%) create mode 100644 test/src/validations/equal_validation_test.dart create mode 100644 test/src/validations/greater_than_validation_test.dart create mode 100644 test/src/validations/is_empty_validation_test.dart diff --git a/example/lib/domain/validations/extensions.dart b/example/lib/domain/validations/extensions.dart index a2fb1c8..39651ba 100644 --- a/example/lib/domain/validations/extensions.dart +++ b/example/lib/domain/validations/extensions.dart @@ -6,7 +6,7 @@ extension CustomValidPasswordValidator on SimpleValidationBuilder { .minLength(5) .mustHaveLowercase() .mustHaveUppercase() - .mustHaveNumbers() + .mustHaveNumber() .mustHaveSpecialCharacter(); } } diff --git a/lib/lucid_validation.dart b/lib/lucid_validation.dart index d5b9ab6..eebe024 100644 --- a/lib/lucid_validation.dart +++ b/lib/lucid_validation.dart @@ -54,7 +54,31 @@ /// library lucid_validation; +import 'lucid_validation.dart'; + +export 'src/localization/language_manager.dart'; +export 'src/localization/localization.dart'; export 'src/lucid_validation_builder.dart'; export 'src/lucid_validator.dart'; export 'src/types/types.dart'; export 'src/validations/validations.dart'; + +sealed class LucidValidation { + static final global = _GlobalConfig( + language: EnglishLanguage(), + languageManager: DefaultLanguageManager(), + cascadeMode: CascadeMode.continueExecution, + ); +} + +class _GlobalConfig { + LanguageManager languageManager; + CascadeMode cascadeMode; + Language language; + + _GlobalConfig({ + required this.languageManager, + required this.cascadeMode, + required this.language, + }); +} diff --git a/lib/src/localization/language.dart b/lib/src/localization/language.dart new file mode 100644 index 0000000..e9e159f --- /dev/null +++ b/lib/src/localization/language.dart @@ -0,0 +1,59 @@ +abstract class Language { + final String culture; + + static const code = ( + equalTo: 'equalTo', + greaterThan: 'greaterThan', + isEmpty: 'isEmpty', + isNotNull: 'isNotNull', + isNull: 'isNull', + lessThan: 'lessThan', + matchesPattern: 'matchesPattern', + max: 'max', + maxLength: 'maxLength', + min: 'min', + minLength: 'minLength', + mustHaveLowercase: 'mustHaveLowercase', + mustHaveNumber: 'mustHaveNumber', + mustHaveSpecialCharacter: 'mustHaveSpecialCharacter', + mustHaveUppercase: 'mustHaveUppercase', + notEmpty: 'notEmpty', + notEqualTo: 'notEqualTo', + range: 'range', + validCEP: 'validCEP', + validCPF: 'validCPF', + validCNPJ: 'validCNPJ', + validCreditCard: 'validCreditCard', + validEmail: 'validEmail', + ); + + Language(this.culture); + + final _translations = { + code.equalTo: "'{PropertyName}' must be equal to '{ComparisonValue}'.", + code.greaterThan: "'{PropertyName}' must be greater than '{ComparisonValue}'.", + code.isEmpty: "'{PropertyName}' must be empty.", + code.isNotNull: "'{PropertyName}' must not be empty.", + code.isNull: "'{PropertyName}' must be empty.", + code.lessThan: "'{PropertyName}' must be less than '{ComparisonValue}'.", + code.matchesPattern: "'{PropertyName}' is not in the correct format.", + code.max: "'{PropertyName}' must be less than or equal to {MaxValue}. You entered {PropertyValue}.", + code.maxLength: "The length of '{PropertyName}' must be {MaxLength} characters or fewer. You entered {TotalLength} characters.", + code.min: "'{PropertyName}' must be greater than or equal to {MinValue}. You entered {PropertyValue}.", + code.minLength: "The length of '{PropertyName}' must be at least {MinLength} characters. You entered {TotalLength} characters.", + code.mustHaveLowercase: "'{PropertyName}' must have at least one lowercase letter.", + code.mustHaveNumber: "'{PropertyName}' must have at least one digit ('0'-'9').", + code.mustHaveSpecialCharacter: "'{PropertyName}' must have at least one non-alphanumeric character.", + code.mustHaveUppercase: "'{PropertyName}' must have at least one uppercase letter.", + code.notEmpty: "'{PropertyName}' must not be empty.", + code.notEqualTo: "'{PropertyName}' must not be equal to '{ComparisonValue}'.", + code.range: "'{PropertyName}' must be between {From} and {To}. You entered {PropertyValue}.", + code.validCEP: "'{PropertyName}' is not a valid CEP.", + code.validCPF: "'{PropertyName}' is not a valid CPF.", + code.validCNPJ: "'{PropertyName}' is not a valid CNPJ.", + code.validCreditCard: "'{PropertyName}' is not a valid credit card number.", + code.validEmail: "'{PropertyName}' is not a valid email address.", + }; + + String? getTranslation(String key) => _translations[key]; +} diff --git a/lib/src/localization/language_manager.dart b/lib/src/localization/language_manager.dart new file mode 100644 index 0000000..393225a --- /dev/null +++ b/lib/src/localization/language_manager.dart @@ -0,0 +1,27 @@ +import '../../lucid_validation.dart'; + +abstract class LanguageManager { + final _globalTranslations = >{}; + + Language get currentLanguage => LucidValidation.global.language; + + void addTranslation(String culture, String code, String value) { + if (!_globalTranslations.containsKey(culture)) { + _globalTranslations[culture] = {}; + } + _globalTranslations[culture]![code] = value; + } + + String translate(String key, [Map parameters = const {}]) { + final culture = currentLanguage.culture; + final translations = _globalTranslations[culture] ?? {}; + var message = translations[key] ?? currentLanguage.getTranslation(key) ?? key; + for (var key in parameters.keys) { + final value = parameters[key]!; + message = message.replaceAll('{$key}', value); + } + return message; + } +} + +class DefaultLanguageManager extends LanguageManager {} diff --git a/lib/src/localization/languages/english_language.dart b/lib/src/localization/languages/english_language.dart new file mode 100644 index 0000000..a3a8329 --- /dev/null +++ b/lib/src/localization/languages/english_language.dart @@ -0,0 +1,5 @@ +import '../language.dart'; + +class EnglishLanguage extends Language { + EnglishLanguage() : super('en'); +} diff --git a/lib/src/localization/localization.dart b/lib/src/localization/localization.dart new file mode 100644 index 0000000..427c48c --- /dev/null +++ b/lib/src/localization/localization.dart @@ -0,0 +1,3 @@ +export 'language.dart'; +export 'language_manager.dart'; +export 'languages/english_language.dart'; diff --git a/lib/src/lucid_validation_builder.dart b/lib/src/lucid_validation_builder.dart index 163c45d..066df91 100644 --- a/lib/src/lucid_validation_builder.dart +++ b/lib/src/lucid_validation_builder.dart @@ -76,8 +76,7 @@ abstract class LucidValidationBuilder { /// builder.must((username) => username.isNotEmpty, 'Username cannot be empty'); /// ``` LucidValidationBuilder must(bool Function(TProp value) validator, String message, String code) { - ValidationError? callback(entity) { - final value = _selector(entity); + ValidationError? callback(value, entity) { if (validator(value)) { return null; } @@ -88,9 +87,7 @@ abstract class LucidValidationBuilder { ); } - _rules.add(callback); - - return this; + return use(callback); } /// Adds a validation rule that checks if the [TProp] value satisfies the [validator] condition, @@ -119,21 +116,31 @@ abstract class LucidValidationBuilder { String message, String code, ) { - ValidationError? callback(entity) { - final value = _selector(entity); - if (validator(value, entity)) { - return null; - } - - return ValidationError( - message: message, - key: key, - code: code, - ); - } + return use( + (value, entity) { + if (validator(value, entity)) { + return null; + } - _rules.add(callback); + return ValidationError( + message: message, + key: key, + code: code, + ); + }, + ); + } + /// Adds a validation rule to the LucidValidationBuilder. + /// + /// The [rule] parameter is a function that takes an [Entity] object as input and returns a [ValidationError] object. + /// This method adds the [rule] to the list of validation rules in the LucidValidationBuilder. + /// + /// Returns the current instance of the LucidValidationBuilder. + LucidValidationBuilder use( + ValidationError? Function(TProp value, Entity entity) rule, + ) { + _rules.add((entity) => rule(_selector(entity), entity)); return this; } diff --git a/lib/src/validations/equal_validation.dart b/lib/src/validations/equal_validation.dart index 5c3f82b..c59dcdc 100644 --- a/lib/src/validations/equal_validation.dart +++ b/lib/src/validations/equal_validation.dart @@ -20,14 +20,30 @@ extension EqualValidation on LucidValidationBuilder { /// .equalTo((user) => user.password); /// /// ``` - LucidValidationBuilder equalTo(T Function(E entity) predicate, {String message = r'Must be equal', String code = 'equal_error'}) { - return mustWith( + /// + /// String format args: + /// - **{PropertyName}**: The name of the property. + /// - **{ComparisonValue}**: The value to compare against. + /// + LucidValidationBuilder equalTo( + T Function(E entity) predicate, { + String? message, + String? code, + }) { + return use( (value, entity) { final comparison = predicate(entity); - return value == comparison; + if (value == comparison) return null; + + final currentCode = code ?? Language.code.equalTo; + final currentMessage = message ?? + LucidValidation.global.languageManager.translate(currentCode, { + 'PropertyName': key, + 'ComparisonValue': '$comparison', + }); + + return ValidationError(message: currentMessage, code: currentCode); }, - message, - code, ); } } diff --git a/lib/src/validations/greater_than_validation.dart b/lib/src/validations/greater_than_validation.dart index 922084f..5243cd8 100644 --- a/lib/src/validations/greater_than_validation.dart +++ b/lib/src/validations/greater_than_validation.dart @@ -8,7 +8,7 @@ extension GreaterThanValidation on SimpleValidationBuilder { /// Adds a validation rule that checks if the [num] is greater than [minValue]. /// /// [minValue] is the value that the number must be greater than. - /// [message] is the error message returned if the validation fails. Defaults to "Must be greater than $minValue". + /// [message] is the error message returned if the validation fails. /// [code] is an optional error code for translation purposes. /// /// Returns the [LucidValidationBuilder] to allow for method chaining. @@ -19,11 +19,27 @@ extension GreaterThanValidation on SimpleValidationBuilder { /// ruleFor((user) => user.age, key: 'age') /// .greaterThan(18); /// ``` - SimpleValidationBuilder greaterThan(num minValue, {String message = r'Must be greater than $minValue', String code = 'greater_than'}) { - return must( - (value) => value > minValue, - message.replaceAll('$minValue', minValue.toString()), - code, - ); + /// + /// String format args: + /// - **{PropertyName}**: The name of the property. + /// - **{ComparisonValue}**: The value to compare against. + /// + SimpleValidationBuilder greaterThan( + num minValue, { + String? message, + String? code, + }) { + return use((value, entity) { + if (value > minValue) return null; + + final currentCode = code ?? Language.code.greaterThan; + final currentMessage = message ?? + LucidValidation.global.languageManager.translate(currentCode, { + 'PropertyName': key, + 'ComparisonValue': '$minValue', + }); + + return ValidationError(message: currentMessage, code: currentCode); + }); } } diff --git a/lib/src/validations/is_empty_validation.dart b/lib/src/validations/is_empty_validation.dart index 92e4229..cbdcbd0 100644 --- a/lib/src/validations/is_empty_validation.dart +++ b/lib/src/validations/is_empty_validation.dart @@ -7,7 +7,7 @@ part of 'validations.dart'; extension IsEmptyValidation on SimpleValidationBuilder { /// Adds a validation rule that checks if the [String] is empty. /// - /// [message] is the error message returned if the validation fails. Defaults to "Must be empty". + /// [message] is the error message returned if the validation fails. /// [code] is an optional error code for translation purposes. /// /// Returns the [LucidValidationBuilder] to allow for method chaining. @@ -18,11 +18,20 @@ extension IsEmptyValidation on SimpleValidationBuilder { /// ruleFor((user) => user.name, key: 'name') /// .isEmpty(); /// ``` - SimpleValidationBuilder isEmpty({String message = 'Must be empty', String code = 'must_be_empty'}) { - return must( - (value) => value.isEmpty, - message, - code, - ); + /// String format args: + /// - **{PropertyName}**: The name of the property. + /// + SimpleValidationBuilder isEmpty({String? message, String? code}) { + return use((value, entity) { + if (value.isEmpty) return null; + + final currentCode = code ?? Language.code.isEmpty; + final currentMessage = message ?? + LucidValidation.global.languageManager.translate(currentCode, { + 'PropertyName': key, + }); + + return ValidationError(message: currentMessage, code: currentCode); + }); } } diff --git a/lib/src/validations/is_not_null_validation.dart b/lib/src/validations/is_not_null_validation.dart index c9df9a1..c10a41e 100644 --- a/lib/src/validations/is_not_null_validation.dart +++ b/lib/src/validations/is_not_null_validation.dart @@ -4,10 +4,10 @@ part of 'validations.dart'; /// /// This extension adds an `isNotNull` method that can be used to ensure that a value /// is not null. -extension IsNotNullValidation on SimpleValidationBuilder { +extension IsNotNullValidation on SimpleValidationBuilder { /// Adds a validation rule that checks if the value is not null. /// - /// [message] is the error message returned if the validation fails. Defaults to "Cannot be null". + /// [message] is the error message returned if the validation fails. /// [code] is an optional error code for translation purposes. /// /// Returns the [LucidValidationBuilder] to allow for method chaining. @@ -18,11 +18,22 @@ extension IsNotNullValidation on SimpleValidationBuilder { /// ruleFor((user) => user.name, key: 'name') // required field /// .isNotNull(); /// ``` - SimpleValidationBuilder isNotNull({String message = 'Cannot be null', String code = 'cannot_be_null'}) { - return must( - (value) => value != null, - message, - code, + /// String format args: + /// - **{PropertyName}**: The name of the property. + /// + SimpleValidationBuilder isNotNull({String? message, String? code}) { + return use( + (value, entity) { + if (value != null) return null; + + final currentCode = code ?? Language.code.isNotNull; + final currentMessage = message ?? + LucidValidation.global.languageManager.translate(currentCode, { + 'PropertyName': key, + }); + + return ValidationError(message: currentMessage, code: currentCode); + }, ); } } diff --git a/lib/src/validations/must_have_numbers_validation.dart b/lib/src/validations/must_have_number_validation.dart similarity index 86% rename from lib/src/validations/must_have_numbers_validation.dart rename to lib/src/validations/must_have_number_validation.dart index 7aad95b..d6cedc9 100644 --- a/lib/src/validations/must_have_numbers_validation.dart +++ b/lib/src/validations/must_have_number_validation.dart @@ -18,7 +18,7 @@ extension MustHaveNumbersValidation on SimpleValidationBuilder { /// ruleFor((user) => user.password, key: 'password') /// .mustHaveNumbers(); /// ``` - SimpleValidationBuilder mustHaveNumbers({String message = 'Must contain at least one numeric digit', String code = 'must_have_numbers'}) { + SimpleValidationBuilder mustHaveNumber({String message = 'Must contain at least one numeric digit', String code = 'must_have_numbers'}) { return must( (value) => RegExp(r'[0-9]').hasMatch(value), message, diff --git a/lib/src/validations/validations.dart b/lib/src/validations/validations.dart index 00d062d..15c8a4d 100644 --- a/lib/src/validations/validations.dart +++ b/lib/src/validations/validations.dart @@ -12,7 +12,7 @@ part 'max_validation.dart'; part 'min_length_validation.dart'; part 'min_validation.dart'; part 'must_have_lowercase_validation.dart'; -part 'must_have_numbers_validation.dart'; +part 'must_have_number_validation.dart'; part 'must_have_special_character_validation.dart'; part 'must_have_uppercase_validation.dart'; part 'not_empty_validation.dart'; diff --git a/test/lucid_validation_test.dart b/test/lucid_validation_test.dart index 5b93277..3184baa 100644 --- a/test/lucid_validation_test.dart +++ b/test/lucid_validation_test.dart @@ -5,12 +5,11 @@ import 'mocks/mocks.dart'; void main() { test('when validating [UserEntityMock] should return a list of error messages for the email field', () { final validator = UserValidator(); - final userEntity = UserModel( - email: '', - password: 'Teste@1234', - age: 18, - phone: '(11) 99999-9999', - ); + final userEntity = UserModel() + ..age = 18 + ..email = '' + ..phone = '(11) 99999-9999' + ..password = 'Teste@1234'; final result = validator.validate(userEntity); final errors = result.errors; @@ -23,12 +22,11 @@ void main() { test('when validating [UserModel] should return a list of error messages for the password field', () { final validator = UserValidator(); - final userEntity = UserModel( - email: 'teste@gmail.com', - password: '', - age: 18, - phone: '(11) 99999-9999', - ); + final userEntity = UserModel() + ..age = 18 + ..email = 'teste@gmail.com' + ..phone = '(11) 99999-9999' + ..password = ''; final result = validator.validate(userEntity); final errors = result.errors; @@ -45,12 +43,11 @@ void main() { test('when validating [UserModel] should return a list of error messages for the age field', () { final validator = UserValidator(); - final userEntity = UserModel( - email: 'teste@gmail.com', - password: 'Teste@1234', - age: 15, - phone: '(11) 99999-9999', - ); + final userEntity = UserModel() + ..age = 15 + ..email = 'teste@gmail.com' + ..phone = '(11) 99999-9999' + ..password = 'Teste@1234'; final result = validator.validate(userEntity); final errors = result.errors; @@ -62,12 +59,12 @@ void main() { test('when validating [UserModel] should return a list of error messages for the phone field', () { final validator = UserValidator(); - final userEntity = UserModel( - email: 'teste@gmail.com', - password: 'Teste@1234', - age: 18, - phone: '', - ); + + final userEntity = UserModel() + ..age = 18 + ..email = 'teste@gmail.com' + ..phone = '' + ..password = 'Teste@1234'; final result = validator.validate(userEntity); final errors = result.errors; @@ -79,12 +76,7 @@ void main() { test('when validating [UserModel] should return a list of error messages for all fields', () { final validator = UserValidator(); - final userEntity = UserModel( - email: '', - password: '', - age: 15, - phone: '', - ); + final userEntity = UserModel()..age = 15; final result = validator.validate(userEntity); final errors = result.errors; @@ -113,9 +105,6 @@ void main() { result = validator.validate(credentials); errors = result.errors; expect(errors.length, 2); - - final stringError = validator.byField(credentials, 'confirmPassword')(); - expect(stringError, 'Must be equal to password'); }); test('setValidator', () { diff --git a/test/mocks/mocks.dart b/test/mocks/mocks.dart index 8d6f494..00ad340 100644 --- a/test/mocks/mocks.dart +++ b/test/mocks/mocks.dart @@ -2,19 +2,11 @@ import 'package:lucid_validation/lucid_validation.dart'; class UserModel { - String email; - String password; - int age; - String phone; - - UserModel({ - required this.email, - required this.password, - required this.age, - required this.phone, - }); - - factory UserModel.empty() => UserModel(email: '', password: '', age: 18, phone: ''); + String email = ''; + String password = ''; + String confirmPassword = ''; + int age = 0; + String phone = ''; } class UserValidator extends LucidValidator { @@ -50,7 +42,7 @@ extension CustomValidPasswordValidator on LucidValidationBuilder { .setValidator(addressValidator); } } + +class TestLucidValidator extends LucidValidator {} diff --git a/test/src/validations/equal_validation_test.dart b/test/src/validations/equal_validation_test.dart new file mode 100644 index 0000000..10e084c --- /dev/null +++ b/test/src/validations/equal_validation_test.dart @@ -0,0 +1,24 @@ +import 'package:lucid_validation/lucid_validation.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mocks.dart'; + +void main() { + test('equal validation', () { + final validator = TestLucidValidator(); + validator + .ruleFor((user) => user.confirmPassword, key: 'confirmPassword') // + .equalTo((user) => user.password); + + final user = UserModel() + ..password = 'password' + ..confirmPassword = 'password2'; + final result = validator.validate(user); + + expect(result.isValid, false); + expect(result.errors.length, 1); + + final error = result.errors.first; + expect(error.message, r"'confirmPassword' must be equal to 'password'."); + }); +} diff --git a/test/src/validations/greater_than_validation_test.dart b/test/src/validations/greater_than_validation_test.dart new file mode 100644 index 0000000..ca1c84c --- /dev/null +++ b/test/src/validations/greater_than_validation_test.dart @@ -0,0 +1,21 @@ +import 'package:lucid_validation/src/validations/validations.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mocks.dart'; + +void main() { + test('greater than validation ...', () { + final validator = TestLucidValidator(); + validator + .ruleFor((user) => user.age, key: 'age') // + .greaterThan(17); + + final user = UserModel()..age = 15; + + final result = validator.validate(user); + + expect(result.isValid, false); + expect(result.errors.length, 1); + expect(result.errors.first.message, "'age' must be greater than '17'."); + }); +} diff --git a/test/src/validations/is_empty_validation_test.dart b/test/src/validations/is_empty_validation_test.dart new file mode 100644 index 0000000..358265b --- /dev/null +++ b/test/src/validations/is_empty_validation_test.dart @@ -0,0 +1,23 @@ +import 'package:lucid_validation/src/validations/validations.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mocks.dart'; + +void main() { + test('is empty validation ...', () { + final validator = TestLucidValidator(); + validator + .ruleFor((user) => user.email, key: 'email') // + .isEmpty(); + + final user = UserModel()..email = 'ss'; + + final result = validator.validate(user); + + expect(result.isValid, false); + + expect(result.errors.length, 1); + + expect(result.errors.first.message, "'email' must be empty."); + }); +}