flutter-form-builder-ecosystem / flutter-form-builder-ecosystem/form_builder_validators

[API change suggestion]: Adding elementary validators

Open
#128 0 comments 1 reaction 0 assignees View on GitHub
enhancement
Dominant language
Dart
Stars
67
Forks
106
Avg merge
2d 21h
Merged PRs (30d)
1

Description

### Is there an existing issue for this?

- [X] I have searched the existing issues

### Package/Plugin version

11.0.0

### What you'd like to happen

```Markdown
Given the current API state, there are some important aspects:
1. Composing validators may generate redundant computation
2. It is necessary to add a flag: checkNullOrEmpty for every validator
3. Composition error message may not be expressive

To address those points, it is possible to use a set of very specialized validators that will be composed to generate more general and robust ones. Basically, the composition will be made using and/otherwise class attributes for each elementary validator.
The dart compiler can be used to help adding expressiveness in such a way that some validators can generate restriction information that can be passed to the sucessive ones of the composition.
For example, it is possible to concatenate a validator that checks if the input is int with another that checks if the input is less than 15, without checking for int again. But it is not possible to have a composition like IsInt and IsBool.

With the implementation of this API change, we may not need checkNullOrEmpty flag anymore, because it would be only necessary to prefix any validator with the more generic one: IsRequired. The redundant computation may be avoided by composing disjoint validators. And the error message also may be improved for compositions, even though this could be done with the current API.
```

### Alternatives you've considered

```Markdown
The implementation of a prototype idea implementation is in the section `Aditional information`
```

### Aditional information

Related issues: #116 #119
Example of redundant computation:
Ex. Composing numeric validators `between` and `even` will result in the program checking the type and potentially parsing to numeric value. The following two images show the code that would be executed twice if the two validators were composed.
![image](https://github.com/user-attachments/assets/f5a9d589-2596-4359-bd48-5985768dce8c)
![image](https://github.com/user-attachments/assets/17a17835-ce3b-495e-a25d-8ef71273634e)

Example of code:
```dart
import 'dart:io';

// ignore_for_file: public_member_api_docs, always_specify_types

/// ## Generic types
/// - T: type of the input value, before any possible transformation. It should be more
/// generic or equal to W.
/// - W: type of the transformed value. It should be more specific or equal to T.
abstract base class ElementaryValidatorInterface {
const ElementaryValidatorInterface({
this.and,
this.otherwise,
});
String get errorMsg;

/// Checks if [value] is valid and returns its transformed value if so.
/// ## Return: a record of the form (isValid, transformedValue)
(bool, W?) transformValueIfValid(T value);

String? validate(T value) {
final (isValid, transformedValue) = transformValueIfValid(value);
if (isValid) {
final completeErrorMsg = [];
for (final validator
in and ?? >[]) {
final validation = validator.validate(transformedValue as W);
if (validation == null) {
return null;
}
completeErrorMsg.add(validation);
}
if (completeErrorMsg.isEmpty) {
return null;
}
return completeErrorMsg.join(' or ');
}

final completeErrorMsg = [errorMsg];
for (final validator
in otherwise ?? >[]) {
final validation = validator.validate(value);
if (validation == null) {
return null;
}
completeErrorMsg.add(validation);
}
return completeErrorMsg.join(' or ');
}

// Here we make the restrictions weaker. But we will get them strong when
// overriding those getters.
final List>? and;
final List>? otherwise;
}

final class RequiredValidator
extends ElementaryValidatorInterface {
const RequiredValidator({
super.and,
super.otherwise,
});

@override
String get errorMsg => '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 ElementaryValidatorInterface {
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 errorMsg => '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 ElementaryValidatorInterface {
const IsBool({
super.and,
super.otherwise,
});

@override
String get errorMsg => '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 ElementaryValidatorInterface {
const IsInt({
super.and,
super.otherwise,
});

@override
String get errorMsg => '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 ElementaryValidatorInterface {
const IsLessThan(
this.reference, {
super.and,
super.otherwise,
});
final T reference;

@override
String get errorMsg => '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 ElementaryValidatorInterface {
const IsGreaterThan(
this.reference, {
super.and,
super.otherwise,
});
final T reference;

@override
String get errorMsg => '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 ElementaryValidatorInterface {
const StringLengthLessThan({required this.referenceValue})
: assert(referenceValue > 0);
final int referenceValue;

@override
String get errorMsg => '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!');
}
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.