Skip to content

Latest commit

 

History

History
184 lines (144 loc) · 7.81 KB

config-driven-validation.adoc

File metadata and controls

184 lines (144 loc) · 7.81 KB

Config driven Validation

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.

hierarchical-validation

What’s Configuration?

  • 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.

config-1-1-validatable

How to construct a Config Object 🔮?

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
*ValidationConfig.<ValidatableT, FailureT>toValidate() // (1)(2)
  1. ValidatableT — Represents the data-type under validation.

  2. 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.

Data-structure under validation

Single (Non-Batch)

ValidationConfig

Collection (Batch)

BatchValidationConfig

Data-structure (Container) HAS-A Nested-Data-structure (Member)

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.

Container Scope

*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

ContainerValidationConfig

Container with 2 levels deep scope

ContainerValidationConfigWith2Levels

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.

Glimpse of the DSL

ℹ️
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();

withValidator(s)

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();