From 952d09b3236371d2228ee78024c99fbd73384fb7 Mon Sep 17 00:00:00 2001 From: Kazi Waseef Date: Sun, 18 Feb 2024 22:12:54 +0600 Subject: [PATCH] Feat: custom validators (#61) * feat(luthor): added custom validator * feat(luthor_annotation): added custom validator annotation fixup * feat(luthor_generator): added custom validator for generator --- .gitignore | 4 +- .../lib/src/validations/any_validation.dart | 6 +-- .../src/validations/custom_validation.dart | 35 +++++++++++++++ packages/luthor/lib/src/validator.dart | 11 ++++- .../validations/custom_validation_test.dart | 43 +++++++++++++++++++ .../lib/luthor_annotation.dart | 3 +- .../lib/src/validators/custom.dart | 6 +++ .../luthor_generator/example/lib/sample.dart | 7 +++ .../example/lib/sample.freezed.dart | 39 ++++++++++++++--- .../example/lib/sample.g.dart | 3 ++ packages/luthor_generator/lib/checkers.dart | 1 + .../helpers/validations/base_validations.dart | 3 ++ .../validations/custom_validations.dart | 24 +++++++++++ 13 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 packages/luthor/lib/src/validations/custom_validation.dart create mode 100644 packages/luthor/test/validations/custom_validation_test.dart create mode 100644 packages/luthor_annotation/lib/src/validators/custom.dart create mode 100644 packages/luthor_generator/lib/helpers/validations/custom_validations.dart diff --git a/.gitignore b/.gitignore index 48dcc728..f84d9a90 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ pubspec.lock .fvm/ -.fvmrc \ No newline at end of file +.fvmrc + +.nvim.lua diff --git a/packages/luthor/lib/src/validations/any_validation.dart b/packages/luthor/lib/src/validations/any_validation.dart index 3c045dbe..fa7e6e5f 100644 --- a/packages/luthor/lib/src/validations/any_validation.dart +++ b/packages/luthor/lib/src/validations/any_validation.dart @@ -1,11 +1,7 @@ import 'package:luthor/src/validation.dart'; class AnyValidation extends Validation { - String? customMessage; - - AnyValidation({ - String? message, - }) : customMessage = message; + AnyValidation(); @override bool call(String? fieldName, Object? value) { diff --git a/packages/luthor/lib/src/validations/custom_validation.dart b/packages/luthor/lib/src/validations/custom_validation.dart new file mode 100644 index 00000000..b811d46a --- /dev/null +++ b/packages/luthor/lib/src/validations/custom_validation.dart @@ -0,0 +1,35 @@ +import 'package:luthor/src/validation.dart'; + +typedef CustomValidator = bool Function(Object? value); + +class CustomValidation extends Validation { + String? customMessage; + final CustomValidator customValidator; + + CustomValidation( + this.customValidator, { + String? message, + }) : customMessage = message; + + @override + bool call(String? fieldName, Object? value) { + super.call(fieldName, value); + try { + return customValidator(value); + } catch (e, s) { + // ignore: avoid_print + print(e); + // ignore: avoid_print + print(s); + return false; + } + } + + @override + String get message => + customMessage ?? + '${fieldName ?? 'value'} does not pass custom validation'; + + @override + Map>? get errors => null; +} diff --git a/packages/luthor/lib/src/validator.dart b/packages/luthor/lib/src/validator.dart index 50f6a65d..1ddb668d 100644 --- a/packages/luthor/lib/src/validator.dart +++ b/packages/luthor/lib/src/validator.dart @@ -2,6 +2,7 @@ import 'package:luthor/src/validation.dart'; import 'package:luthor/src/validation_result.dart'; import 'package:luthor/src/validations/any_validation.dart'; import 'package:luthor/src/validations/bool_validation.dart'; +import 'package:luthor/src/validations/custom_validation.dart'; import 'package:luthor/src/validations/double_validation.dart'; import 'package:luthor/src/validations/int_validation.dart'; import 'package:luthor/src/validations/list_validation.dart'; @@ -25,8 +26,14 @@ class Validator { final List validations; /// Validates a value as dynamic. Always returns true. - Validator any({String? message}) { - validations.add(AnyValidation(message: message)); + Validator custom(CustomValidator customValidator, {String? message}) { + validations.add(CustomValidation(customValidator, message: message)); + return this; + } + + /// Validates a value as dynamic. Always returns true. + Validator any() { + validations.add(AnyValidation()); return this; } diff --git a/packages/luthor/test/validations/custom_validation_test.dart b/packages/luthor/test/validations/custom_validation_test.dart new file mode 100644 index 00000000..c1156be5 --- /dev/null +++ b/packages/luthor/test/validations/custom_validation_test.dart @@ -0,0 +1,43 @@ +import 'package:luthor/luthor.dart'; +import 'package:test/test.dart'; + +void main() { + test('should return true when custom validator passes', () { + final result = l.custom((value) { + return value! as bool; + }).validateValue(true); + + switch (result) { + case SingleValidationSuccess(data: _): + expect(result.data, true); + case SingleValidationError(data: _, errors: _): + fail('should not have errors'); + } + }); + + test('should return false when custom validator fails', () { + final result = l.custom((value) { + return !(value! as bool); + }).validateValue(true); + + switch (result) { + case SingleValidationSuccess(data: _): + fail('should not be a success'); + case SingleValidationError(data: _, errors: final errors): + expect(errors, ['value does not pass custom validation']); + } + }); + + test('should return false if the value is null with required()', () { + final result = l.required().custom((value) { + return true; + }).validateValue(null); + + switch (result) { + case SingleValidationSuccess(data: _): + fail('should not be a success'); + case SingleValidationError(data: _, errors: final errors): + expect(errors, ['value is required']); + } + }); +} diff --git a/packages/luthor_annotation/lib/luthor_annotation.dart b/packages/luthor_annotation/lib/luthor_annotation.dart index d02b382b..b91bcb77 100644 --- a/packages/luthor_annotation/lib/luthor_annotation.dart +++ b/packages/luthor_annotation/lib/luthor_annotation.dart @@ -1,8 +1,9 @@ export './src/luthor.dart'; +export './src/validators/custom.dart'; export './src/validators/date_time.dart'; export './src/validators/email.dart'; export './src/validators/length.dart'; export './src/validators/max.dart'; export './src/validators/min.dart'; -export './src/validators/uri.dart'; export './src/validators/regex.dart'; +export './src/validators/uri.dart'; diff --git a/packages/luthor_annotation/lib/src/validators/custom.dart b/packages/luthor_annotation/lib/src/validators/custom.dart new file mode 100644 index 00000000..143963e2 --- /dev/null +++ b/packages/luthor_annotation/lib/src/validators/custom.dart @@ -0,0 +1,6 @@ +class WithCustomValidator { + final String? message; + final bool Function(Object? value) customValidator; + + const WithCustomValidator(this.customValidator, {this.message}); +} diff --git a/packages/luthor_generator/example/lib/sample.dart b/packages/luthor_generator/example/lib/sample.dart index fbccd661..b4b41e4d 100644 --- a/packages/luthor_generator/example/lib/sample.dart +++ b/packages/luthor_generator/example/lib/sample.dart @@ -9,6 +9,10 @@ part 'sample.freezed.dart'; part 'sample.g.dart'; +bool customValidatorFn(Object? value) { + return value == 'custom'; +} + @luthor @freezed class Sample with _$Sample { @@ -35,6 +39,7 @@ class Sample with _$Sample { required String luthorPath, required AnotherSample anotherSample, @JsonKey(name: 'jsonKeyName') required String foo, + @WithCustomValidator(customValidatorFn) required String custom, }) = _Sample; static SchemaValidationResult validate(Map json) => @@ -61,6 +66,7 @@ void main() { "minAndMaxInt": 1, "minAndMaxDouble": 1.0, "minAndMaxNumber": 1, + "custom": 'custom', }); switch (result2) { case SchemaValidationError(errors: final errors): @@ -78,6 +84,7 @@ void main() { "minAndMaxInt": 5, "minAndMaxDouble": 5.0, "minAndMaxNumber": 5, + "custom": 1, }); switch (result3) { case SchemaValidationError(errors: final errors): diff --git a/packages/luthor_generator/example/lib/sample.freezed.dart b/packages/luthor_generator/example/lib/sample.freezed.dart index 0ab09f60..cd9ff356 100644 --- a/packages/luthor_generator/example/lib/sample.freezed.dart +++ b/packages/luthor_generator/example/lib/sample.freezed.dart @@ -55,6 +55,8 @@ mixin _$Sample { AnotherSample get anotherSample => throw _privateConstructorUsedError; @JsonKey(name: 'jsonKeyName') String get foo => throw _privateConstructorUsedError; + @WithCustomValidator(customValidatorFn) + String get custom => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -85,7 +87,8 @@ abstract class $SampleCopyWith<$Res> { @IsUri(allowedSchemes: ['https']) String? httpsLink, @MatchRegex(r'^https:\/\/pub\.dev\/packages\/luthor') String luthorPath, AnotherSample anotherSample, - @JsonKey(name: 'jsonKeyName') String foo}); + @JsonKey(name: 'jsonKeyName') String foo, + @WithCustomValidator(customValidatorFn) String custom}); $AnotherSampleCopyWith<$Res> get anotherSample; } @@ -122,6 +125,7 @@ class _$SampleCopyWithImpl<$Res, $Val extends Sample> Object? luthorPath = null, Object? anotherSample = null, Object? foo = null, + Object? custom = null, }) { return _then(_value.copyWith( anyValue: freezed == anyValue @@ -200,6 +204,10 @@ class _$SampleCopyWithImpl<$Res, $Val extends Sample> ? _value.foo : foo // ignore: cast_nullable_to_non_nullable as String, + custom: null == custom + ? _value.custom + : custom // ignore: cast_nullable_to_non_nullable + as String, ) as $Val); } @@ -238,7 +246,8 @@ abstract class _$$SampleImplCopyWith<$Res> implements $SampleCopyWith<$Res> { @IsUri(allowedSchemes: ['https']) String? httpsLink, @MatchRegex(r'^https:\/\/pub\.dev\/packages\/luthor') String luthorPath, AnotherSample anotherSample, - @JsonKey(name: 'jsonKeyName') String foo}); + @JsonKey(name: 'jsonKeyName') String foo, + @WithCustomValidator(customValidatorFn) String custom}); @override $AnotherSampleCopyWith<$Res> get anotherSample; @@ -274,6 +283,7 @@ class __$$SampleImplCopyWithImpl<$Res> Object? luthorPath = null, Object? anotherSample = null, Object? foo = null, + Object? custom = null, }) { return _then(_$SampleImpl( anyValue: freezed == anyValue @@ -352,6 +362,10 @@ class __$$SampleImplCopyWithImpl<$Res> ? _value.foo : foo // ignore: cast_nullable_to_non_nullable as String, + custom: null == custom + ? _value.custom + : custom // ignore: cast_nullable_to_non_nullable + as String, )); } } @@ -379,7 +393,8 @@ class _$SampleImpl implements _Sample { @MatchRegex(r'^https:\/\/pub\.dev\/packages\/luthor') required this.luthorPath, required this.anotherSample, - @JsonKey(name: 'jsonKeyName') required this.foo}) + @JsonKey(name: 'jsonKeyName') required this.foo, + @WithCustomValidator(customValidatorFn) required this.custom}) : _listValue = listValue; factory _$SampleImpl.fromJson(Map json) => @@ -445,10 +460,13 @@ class _$SampleImpl implements _Sample { @override @JsonKey(name: 'jsonKeyName') final String foo; + @override + @WithCustomValidator(customValidatorFn) + final String custom; @override String toString() { - return 'Sample(anyValue: $anyValue, boolValue: $boolValue, doubleValue: $doubleValue, intValue: $intValue, listValue: $listValue, numValue: $numValue, stringValue: $stringValue, email: $email, date: $date, dateTime: $dateTime, exactly10Characters: $exactly10Characters, minAndMaxString: $minAndMaxString, minAndMaxInt: $minAndMaxInt, minAndMaxDouble: $minAndMaxDouble, minAndMaxNumber: $minAndMaxNumber, httpsLink: $httpsLink, luthorPath: $luthorPath, anotherSample: $anotherSample, foo: $foo)'; + return 'Sample(anyValue: $anyValue, boolValue: $boolValue, doubleValue: $doubleValue, intValue: $intValue, listValue: $listValue, numValue: $numValue, stringValue: $stringValue, email: $email, date: $date, dateTime: $dateTime, exactly10Characters: $exactly10Characters, minAndMaxString: $minAndMaxString, minAndMaxInt: $minAndMaxInt, minAndMaxDouble: $minAndMaxDouble, minAndMaxNumber: $minAndMaxNumber, httpsLink: $httpsLink, luthorPath: $luthorPath, anotherSample: $anotherSample, foo: $foo, custom: $custom)'; } @override @@ -489,7 +507,8 @@ class _$SampleImpl implements _Sample { other.luthorPath == luthorPath) && (identical(other.anotherSample, anotherSample) || other.anotherSample == anotherSample) && - (identical(other.foo, foo) || other.foo == foo)); + (identical(other.foo, foo) || other.foo == foo) && + (identical(other.custom, custom) || other.custom == custom)); } @JsonKey(ignore: true) @@ -514,7 +533,8 @@ class _$SampleImpl implements _Sample { httpsLink, luthorPath, anotherSample, - foo + foo, + custom ]); @JsonKey(ignore: true) @@ -554,7 +574,9 @@ abstract class _Sample implements Sample { @MatchRegex(r'^https:\/\/pub\.dev\/packages\/luthor') required final String luthorPath, required final AnotherSample anotherSample, - @JsonKey(name: 'jsonKeyName') required final String foo}) = _$SampleImpl; + @JsonKey(name: 'jsonKeyName') required final String foo, + @WithCustomValidator(customValidatorFn) + required final String custom}) = _$SampleImpl; factory _Sample.fromJson(Map json) = _$SampleImpl.fromJson; @@ -612,6 +634,9 @@ abstract class _Sample implements Sample { @JsonKey(name: 'jsonKeyName') String get foo; @override + @WithCustomValidator(customValidatorFn) + String get custom; + @override @JsonKey(ignore: true) _$$SampleImplCopyWith<_$SampleImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/packages/luthor_generator/example/lib/sample.g.dart b/packages/luthor_generator/example/lib/sample.g.dart index f4b3aa4e..9548a4c4 100644 --- a/packages/luthor_generator/example/lib/sample.g.dart +++ b/packages/luthor_generator/example/lib/sample.g.dart @@ -28,6 +28,7 @@ _$SampleImpl _$$SampleImplFromJson(Map json) => _$SampleImpl( anotherSample: AnotherSample.fromJson(json['anotherSample'] as Map), foo: json['jsonKeyName'] as String, + custom: json['custom'] as String, ); Map _$$SampleImplToJson(_$SampleImpl instance) => @@ -51,6 +52,7 @@ Map _$$SampleImplToJson(_$SampleImpl instance) => 'luthorPath': instance.luthorPath, 'anotherSample': instance.anotherSample, 'jsonKeyName': instance.foo, + 'custom': instance.custom, }; // ************************************************************************** @@ -78,6 +80,7 @@ Validator $SampleSchema = l.schema({ l.string().regex(r"^https:\/\/pub\.dev\/packages\/luthor").required(), 'anotherSample': $AnotherSampleSchema.required(), 'jsonKeyName': l.string().required(), + 'custom': l.string().custom(customValidatorFn).required(), }); SchemaValidationResult _$SampleValidate(Map json) => diff --git a/packages/luthor_generator/lib/checkers.dart b/packages/luthor_generator/lib/checkers.dart index 923c7db4..583283fb 100644 --- a/packages/luthor_generator/lib/checkers.dart +++ b/packages/luthor_generator/lib/checkers.dart @@ -16,3 +16,4 @@ const hasMaxNumberChecker = TypeChecker.fromRuntime(HasMaxNumber); const hasMinNumberChecker = TypeChecker.fromRuntime(HasMinNumber); const isUriChecker = TypeChecker.fromRuntime(IsUri); const matchRegexChecker = TypeChecker.fromRuntime(MatchRegex); +const customValidatorChecker = TypeChecker.fromRuntime(WithCustomValidator); diff --git a/packages/luthor_generator/lib/helpers/validations/base_validations.dart b/packages/luthor_generator/lib/helpers/validations/base_validations.dart index b6375c72..bc210826 100644 --- a/packages/luthor_generator/lib/helpers/validations/base_validations.dart +++ b/packages/luthor_generator/lib/helpers/validations/base_validations.dart @@ -4,6 +4,7 @@ import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:luthor_generator/checkers.dart'; import 'package:luthor_generator/errors/unsupported_type_error.dart'; +import 'package:luthor_generator/helpers/validations/custom_validations.dart'; import 'package:luthor_generator/helpers/validations/double_validations.dart'; import 'package:luthor_generator/helpers/validations/int_validations.dart'; import 'package:luthor_generator/helpers/validations/number_validations.dart'; @@ -56,6 +57,8 @@ String getValidations(ParameterElement param) { _checkAndAddCustomSchema(buffer, param); } + getCustomValidations(param, buffer); + if (param.type is! DynamicType && !isNullable) buffer.write('.required()'); return buffer.toString(); diff --git a/packages/luthor_generator/lib/helpers/validations/custom_validations.dart b/packages/luthor_generator/lib/helpers/validations/custom_validations.dart new file mode 100644 index 00000000..e906c26d --- /dev/null +++ b/packages/luthor_generator/lib/helpers/validations/custom_validations.dart @@ -0,0 +1,24 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:luthor_generator/checkers.dart'; +import 'package:luthor_generator/helpers/validations/base_validations.dart'; + +void getCustomValidations(ParameterElement param, StringBuffer buffer) { + _checkAndWriteCustomValidation(buffer, param); +} + +void _checkAndWriteCustomValidation( + StringBuffer buffer, + ParameterElement param, +) { + final customAnnotation = getAnnotation(customValidatorChecker, param); + if (customAnnotation != null) { + buffer.write('.custom('); + final message = customAnnotation.getField('message')?.toStringValue(); + final customFuntion = + customAnnotation.getField('customValidator')!.toFunctionValue()!.name; + + buffer.write(customFuntion); + if (message != null) buffer.write(", message: '$message'"); + buffer.write(')'); + } +}