Creating a DSL is one part Engineering and many parts Art. With simple Dev UX as the primary goal, we developed a fluent DSL that lets you hook the validators, along with the built-in plugin-n-play validators. This is powerful coz it lets you validate any Bean with any level or nesting, be it Single or Batch. It’s easy to fit this model in our heads, as validation configuration aligns with Bean hierarchical-structure.
-
A config object resides outside your Validatable is 1-1 mapped with its Data-Structure.
-
Config being decoupled from the Validatable gives flexibility over annotation-based frameworks. You can define configuration for classes that are not part of your code-base/module
-
It holds all the information/requirements/specifications required to validate that Data-structure.
Different flavors of Config DSLs are here to co-pilot with you, to prepare the config instance as per your validation requirements. These DSL methods follow the Builder Pattern, where you instantiate the Builder like this:
*ValidationConfig.<ValidatableT, FailureT>toValidate() // (1)(2)
-
ValidatableT — Represents the data-type under validation.
-
FailureT — Represents the consumer data-type that represents a failure.
ℹ️
|
As you notice, these API methods are generic and Vador is agnostic of the consumer’s ValidatableT or FailureT .
|
Single (Non-Batch) |
|
---|---|
Collection (Batch) |
You may have a requirement to validate a Data-structure which HAS-A Nested-Data-structure that needs to be validated too. Such scenarios are complex as they involve various combinations of Container-Member state(Batch or Non-Batch) + Execution Strategy (Fail Fast for Each or Fail Fast for Any). There is no one-solution-fits all.
This table should help you make the right choice.
*ValidationConfig
talks about the Data structure itself, whereas ContainerValidationConfig*
talks about what-it-contains.
Configuration fields like shouldHaveMinBatchSizeOrFailWith, shouldHaveMaxBatchSizeOrFailWith
won’t make sense when a *ValidationConfig
is describing a Bean (or BeanBatch).
So these config parameters are separated-out into a different config under the umbrella of ContainerValidationConfig*
.
Container with 1 level deep scope |
|
---|---|
Container with 2 levels deep scope |
However, there can be confusing in scenarios like this:
class ContainerWithMultiBatch {
List<Bean1> batch1;
List<Bean2> batch2;
}
In a data-structure you may have a validation like batch1
should not be empty.
You can achieve this using both ContainerValidationConfig and BatchValidationConfig, with configs as below:
ContainerValidationConfig.<ContainerWithMultiBatch, ValidationFailure>toValidate()
.withBatchMember(ContainerWithMultiBatch::getBatch1)
.shouldHaveMinBatchSizeOrFailWith(Tuple.of(1, INVALID_BATCH_SIZE)).prepare();
ValidationConfig.<ContainerWithMultiBatch, ValidationFailure>toValidate()
.shouldHaveFieldOrFailWith(ContainerWithMultiBatch::getBatch1, FIELD_MISSING).prepare();
This similarity may cause confusion as to which one to use.
The answer is — "It depends on your Intent".
If you look at the list being empty/null
as INVALID_BATCH_SIZE
, go with ContainerValidationConfig.
If you look at it as any other mandatory field, go with ValidationConfig.
ℹ️
|
These examples don’t exhaustively cover all the DSL methods and use-cases. You may refer the Javadoc (TBD) of each validation config to find-out more. Also, the existing unit tests should help with the use-cases. As usual, file a GitHub issue if you have any new or unique use-cases. |
BatchValidationConfig.<Bean, ValidationFailure>toValidate()
.withValidators(Tuple.of( // 👈🏼 Hook your validators // (1)
List.of(Validators::validator1, validator2, validator2,...),
ValidationFailure.NONE))
.shouldHaveFieldsOrFailWithFn(…) // Declare Mandatory fields (2)
.withIdConfig(…) // Declare fields for Strict SF ID validation // (3)
.findAndFilterDuplicatesConfigs(…) // Multi-filter criteria to knock-out duplicates // (4)
.specify(…) // 👈🏼 Low-code validations go here // (5)
.…
.prepare();
This is used to wire Validator
type lambdas into the config. This accepts a Tuple (Pair) of:
-
java.util.Collection<Validator>
— Collections of Validators. -
Failure
— Consumer defined value representing no-failure (or success). Vador recognizes that a validation passed, only if a validator returns this value.
💡
|
If you need an order of execution (say, ascending order of validation cost),
all you need is chain your validators in an Ordered List (like java.util.List ) to maintain the sequence of validations.
|
final Validator<Bean, ValidationFailure> validator1 = bean -> NONE;
final Validator<Bean, ValidationFailure> validator2 = bean -> NONE;
final Validator<Bean, ValidationFailure> validator3 = bean -> UNKNOWN_EXCEPTION;
final List<Validator<Bean, ValidationFailure>> validatorChain =
List.of(validator1, validator2, validator3);
final var validationConfig =
ValidationConfig.<Bean, ValidationFailure>toValidate()
.withValidators(Tuple.of(validatorChain, NONE))
.prepare();