Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: custom validators #61

Merged
merged 3 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
pubspec.lock

.fvm/
.fvmrc
.fvmrc

.nvim.lua
6 changes: 1 addition & 5 deletions packages/luthor/lib/src/validations/any_validation.dart
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
35 changes: 35 additions & 0 deletions packages/luthor/lib/src/validations/custom_validation.dart
Original file line number Diff line number Diff line change
@@ -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<String, List<String>>? get errors => null;
}
11 changes: 9 additions & 2 deletions packages/luthor/lib/src/validator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,8 +26,14 @@ class Validator {
final List<Validation> 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;
}

Expand Down
43 changes: 43 additions & 0 deletions packages/luthor/test/validations/custom_validation_test.dart
Original file line number Diff line number Diff line change
@@ -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']);
}
});
}
3 changes: 2 additions & 1 deletion packages/luthor_annotation/lib/luthor_annotation.dart
Original file line number Diff line number Diff line change
@@ -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';
6 changes: 6 additions & 0 deletions packages/luthor_annotation/lib/src/validators/custom.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class WithCustomValidator {
final String? message;
final bool Function(Object? value) customValidator;

const WithCustomValidator(this.customValidator, {this.message});
}
7 changes: 7 additions & 0 deletions packages/luthor_generator/example/lib/sample.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<Sample> validate(Map<String, dynamic> json) =>
Expand All @@ -61,6 +66,7 @@ void main() {
"minAndMaxInt": 1,
"minAndMaxDouble": 1.0,
"minAndMaxNumber": 1,
"custom": 'custom',
});
switch (result2) {
case SchemaValidationError(errors: final errors):
Expand All @@ -78,6 +84,7 @@ void main() {
"minAndMaxInt": 5,
"minAndMaxDouble": 5.0,
"minAndMaxNumber": 5,
"custom": 1,
});
switch (result3) {
case SchemaValidationError(errors: final errors):
Expand Down
39 changes: 32 additions & 7 deletions packages/luthor_generator/example/lib/sample.freezed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
));
}
}
Expand Down Expand Up @@ -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<String, dynamic> json) =>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -514,7 +533,8 @@ class _$SampleImpl implements _Sample {
httpsLink,
luthorPath,
anotherSample,
foo
foo,
custom
]);

@JsonKey(ignore: true)
Expand Down Expand Up @@ -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<String, dynamic> json) = _$SampleImpl.fromJson;

Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions packages/luthor_generator/example/lib/sample.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/luthor_generator/lib/checkers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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(')');
}
}
Loading