Skip to content

Commit

Permalink
feat: simplification of the API and rename of props to objectProps
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

The `getValidatorProps` approach was causing some usability troubles:

1. Constructing validators became unpredictable
    * The built-in validators were quite powerful, yes, but they were simply _too_ flexible
    * The overabundance of flexibility made it hard to grok what was happening because there was too much magic
2. Creating validators became too difficult
    * It raised the bar too high for validator authors to adopt the same level of magic flexibility
    * And it was unclear what would happen if some validators did not adhere

To address these issues:

1. Validators no longer take ordinal params in the flexible way -- instead, there's just a single props object supplied
2. That props param can be a function that returns the props object
3. Context is passed to said function, but there's just a single props object/function now instead of a magic chain of them
4. The `validate` function reliably puts `value` on context and the result props -- no validators are responsible for doing that

Even though using a props object parameter is more verbose for basic scenarios, it makes the API more predictable and therefore approachable.

Additionally, the `props` validator was badly named.  The "props" concept is used throughout Strickland and the name collision between concept and validator was hard to keep clear.  It is now named `objectProps`.
  • Loading branch information
jeffhandley committed Mar 12, 2018
1 parent 425dd30 commit 4129242
Show file tree
Hide file tree
Showing 68 changed files with 1,043 additions and 2,096 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ const result = validate(letterA, 'B');
* [Validation Context](http://strickland.io/docs/Extensibility/ValidationContext.html)
* [Validation Result Props](http://strickland.io/docs/Extensibility/ValidationResults.html)
* [Extensibility Pattern](http://strickland.io/docs/Extensibility/Pattern.html)
* [getValidatorProps](http://strickland.io/docs/Extensibility/getValidatorProps.html)
* [Built-In Validators](http://strickland.io/docs/Validators/index.html)
* [required](http://strickland.io/docs/Validators/required.html)
* [compare](http://strickland.io/docs/Validators/compare.html)
Expand Down
3 changes: 1 addition & 2 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
* [Validation Context](/docs/Extensibility/ValidationContext.md)
* [Validation Result Props](/docs/Extensibility/ValidationResultProps.md)
* [Extensibility Pattern](/docs/Extensibility/Pattern.md)
* [getValidatorProps](/docs/Extensibility/getValidatorProps.md)
* [Built-In Validators](/docs/Validators/README.md)
* [required](/docs/Validators/required.md)
* [compare](/docs/Validators/compare.md)
Expand All @@ -26,7 +25,7 @@
* [each](/docs/Composition/each.md)
* [some](/docs/Composition/some.md)
* [Validating Objects](/docs/Composition/ValidatingObjects.md)
* [props](/docs/Composition/props.md)
* [objectProps](/docs/Composition/objectProps.md)
* [Advanced Object Validation](/docs/Composition/AdvancedObjectValidation.md)
* [Nested Objects](/docs/Composition/NestedObjects.md)
* [Array and Object Conventions](/docs/Composition/Conventions.md)
Expand Down
12 changes: 6 additions & 6 deletions demo/src/formValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,28 @@ export default form({
firstName: required({message: 'Required'}),
lastName: [
required({message: 'Required'}),
minLength(2, {message: 'Must have at least 2 characters'})
minLength({minLength: 2, message: 'Must have at least 2 characters'})
],
username: [
required({message: 'Required'}),
minLength(4, {message: 'Must have at least 4 characters'}),
minLength({minLength: 4, message: 'Must have at least 4 characters'}),
usernameIsAvailable
],
password: every(
[required(), minLength(8)],
[required(), minLength({minLength: 8})],
{message: 'Must have at least 8 characters'}
),
confirmPassword: every(
[required(), compare(({form}) => form.values.password)],
[required(), compare(({form: {values: {password}}}) => ({compare: password}))],
{message: 'Must match password'}
)
});

export function getValidationClassName(form, validation, fieldName) {
export function getValidationClassName(formValues, validation, fieldName) {
const fieldValidation = validation && validation.form && validation.form.validationResults[fieldName];

return classnames({
'validation-value': !!form[fieldName],
'validation-value': !!formValues[fieldName],
'validation-valid': fieldValidation && fieldValidation.isValid,
'validation-async': fieldValidation && fieldValidation.validateAsync,
'validation-invalid': fieldValidation && !fieldValidation.isValid && !fieldValidation.validateAsync
Expand Down
6 changes: 5 additions & 1 deletion docs/Async/RaceConditions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ A common pitfall with async validation is to ensure the value hasn't changed dur
Let's take a look at handling this race condition in application code:

``` jsx
const validateUsername = [required(), length(2, 20), usernameIsAvailableTwoStage];
const validateUsername = [
required(),
length({minLength: 2, maxLength: 20}),
usernameIsAvailableTwoStage
];

let username = 'marty';
let usernameResult = validate(validateUsername, username);
Expand Down
11 changes: 9 additions & 2 deletions docs/Async/TwoStageValidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,15 @@ In the following example, the application will render the partial, synchronous v
import validate, {required, length} from 'strickland';

const validateUser = {
name: [required(), length(2, 20)],
username: [required(), length(2, 20), usernameIsAvailableTwoStage]
name: [
required(),
length({minLength: 2, maxLength: 20})
],
username: [
required(),
length({minLength: 2, maxLength: 20}),
usernameIsAvailableTwoStage
]
};

const user = {
Expand Down
18 changes: 10 additions & 8 deletions docs/Async/ValidatorArraysAndObjects.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Async Validator Arrays and Objects

The `every`, `each`, `some`, and `props` validators support async validators too. You can compose async validators together with any other validators. Here is an example showing sync and async validators mixed together with nested objects and arrays.
The `every`, `each`, `some`, and `objectProps` validators support async validators too. You can compose async validators together with any other validators. Here is an example showing sync and async validators mixed together with nested objects and arrays.

``` jsx
import {validateAsync, required, length} from 'strickland';
Expand All @@ -27,21 +27,23 @@ function validateCity(address) {
const validatePerson = {
name: [
required(),
length(2, 20, {
length({
minLength: 2,
maxLength: 20,
message: 'Name must be 2-20 characters'
})
],
username: [
required(),
length(2, 20),
length({minLength: 2, maxLength: 20}),
usernameIsAvailable
],
address: [
required({message: 'Address is required'}),
{
street: [required(), length(2, 40)],
city: [required(), length(2, 40)],
state: [required(), length(2, 2)]
street: [required(), length({minLength: 2, maxLength: 40})],
city: [required(), length({minLength: 2, maxLength: 40})],
state: [required(), length({minLength: 2, maxLength: 2})]
},
validateCity
]
Expand Down Expand Up @@ -105,6 +107,6 @@ The `each` validator resolves all async prop validators in parallel. Because `ea

The `some` validator is similar to `every`; it short-circuits and therefore cannot run async validators in parallel. The `some` validator will short-circuit and return a *valid* result as soon as it encounters the first valid result. Async validators will therefore get chained together and run in series until a valid result is found.

## props
## objectProps

The `props` validator resolves all async prop validators in parallel. This is possible because one prop being invalid does not prevent other props from being validated. The `props` validator result will not be resolved until all props have been validated, but the async validators will be executed in parallel using `Promise.all()`. In the example above, `usernameIsAvailable` and `validateCity` run in parallel.
The `objectProps` validator resolves all async prop validators in parallel. This is possible because one prop being invalid does not prevent other props from being validated. The `objectProps` validator result will not be resolved until all props have been validated, but the async validators will be executed in parallel using `Promise.all()`. In the example above, `usernameIsAvailable` and `validateCity` run in parallel.
18 changes: 12 additions & 6 deletions docs/Composition/AdvancedObjectValidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ With the composable nature of Strickland, it is very easy to perform advanced ob

``` jsx
import validate, {
props, required, length, range, every
objectProps, required, length, range, every
} from 'strickland';

// Define the rules for first name, last name, and birthYear
const validatePersonProps = props({
firstName: every([required(), length(2, 20)]),
lastName: every([required(), length(2, 20)]),
birthYear: range(1900, 2018)
const validatePersonProps = objectProps({
firstName: every([
required(),
length({minLength: 2, maxLength: 20})
]),
lastName: every([
required(),
length({minLength: 2, maxLength: 20})
]),
birthYear: range({min: 1900, max: 2018})
});

function stanfordStricklandBornIn1925(person) {
Expand Down Expand Up @@ -59,7 +65,7 @@ In this example, the following will be validated (in this order):

Here are some notes should anything have been invalid:

1. If the `person` was empty, neither the props nor `stanfordStricklandBornIn1925` would be validated
1. If the `person` was empty, neither the object props nor `stanfordStricklandBornIn1925` would be validated
1. If the `firstName` prop was empty, its length would not be validated
1. If the `lastName` prop was empty, its length would not be validated
1. If the `firstName`, `lastName`, or `birthYear` props were invalid, `stanfordStricklandBornIn1925` would not be validated
6 changes: 5 additions & 1 deletion docs/Composition/ArraysOfValidators.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ Here is how the `every` validator can be used.
``` jsx
import validate, {every, required, minLength} from 'strickland';

const mustExistWithLength5 = every([required(), minLength(5)]);
const mustExistWithLength5 = every([
required(),
minLength({minLength: 5})
]);

const result = validate(mustExistWithLength5, '1234', {
message: 'Must have a length of at least 5'
});
Expand Down
14 changes: 7 additions & 7 deletions docs/Composition/Conventions.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Array and Object Conventions

We defined early on that all validators must be functions in Strickland. This is technically true, but because `every` and `props` are used so frequently to validate arrays of validators and object properties, conventions are built into Strickland's `validate` function to automatically use `every` and `props`.
We defined early on that all validators must be functions in Strickland. This is technically true, but because `every` and `objectProps` are used so frequently to validate arrays of validators and object properties, conventions are built into Strickland's `validate` function to automatically use `every` and `objectProps`.

If a validator is not a function, but it is instead an array, it is assumed to be an array of validator functions. This array will be wrapped with `every`.

If a validator is an object, it is assumed to be an object defining validators for object props. This object will be wrapped with `props`.
If a validator is an object, it is assumed to be an object defining validators for object props. This object will be wrapped with `objectProps`.

We can rewrite the example for validating a person's name and address more naturally.

Expand All @@ -14,19 +14,19 @@ import validate, {required, length, range} from 'strickland';
const validatePerson = [
required(),
{
name: [required(), length(5, 40)],
name: [required(), length({minLength: 5, maxLength: 40})],
address: [
required(),
{
street: [
required(),
{
number: [required(), range(1, 99999)],
name: [required(), length(2, 40)]
number: [required(), range({min: 1, max: 99999})],
name: [required(), length({minLength: 2, maxLength: 40})]
}
],
city: required(),
state: [required(), length(2, 2)]
state: [required(), length({minLength: 2, maxLength: 2})]
}
]
}
Expand All @@ -46,4 +46,4 @@ const result = validate(validatePerson, person);
// address does not have a street
```

There may be times when you do need to explicitly use `every` and `props` though. With the object and array conventions, there is no way to pass validator props in that would apply at the object-level or to all validators within the array. But it is quite easy to reintroduce the `props` or `every` wrapper and pass props in after the object or array as seen previously.
There may be times when you do need to explicitly use `every` and `objectProps` though. With the object and array conventions, there is no way to pass validator props in that would apply at the object-level or to all validators within the array. But it is quite easy to reintroduce the `objectProps` or `every` wrapper and pass props in after the object or array as seen previously.
16 changes: 8 additions & 8 deletions docs/Composition/NestedObjects.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ The composition ability for combining validators together on props and objects o

``` jsx
import validate, {
props, required, length, range, every
objectProps, required, length, range, every
} from 'strickland';

const validatePerson = props({
name: every([required(), length(5, 40)]),
address: props({
street: props({
number: every([required(), range(1, 99999)]),
name: every([required(), length(2, 40)])
const validatePerson = objectProps({
name: every([required(), length({minLength: 5, maxLength: 40})]),
address: objectProps({
street: objectProps({
number: every([required(), range({min: 1, max: 99999})]),
name: every([required(), length({minLength: 2, maxLength: 40})])
}),
city: required(),
state: every([required(), length(2, 2)])
state: every([required(), length({minLength: 2, maxLength: 2})])
})
});

Expand Down
8 changes: 4 additions & 4 deletions docs/Composition/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
The examples we've seen so far only validate single values against single validators. But Strickland gains the bulk of its power through composition of validators. Because every validator is simply a function, it is easy to create a function that executes multiple validators. Validators themselves can invoke the `validate` function to collect results of multiple validators and combine them together into top-level validation results.

* [Arrays of Validators](ArraysOfValidators.md)
* [every](../Composition/every.md)
* [each](../Composition/each.md)
* [some](../Composition/some.md)
* [every](./every.md)
* [each](./each.md)
* [some](./some.md)
* [Validating Objects](ValidatingObjects.md)
* [With the props Validator](../Validators/props.md)
* [With the objectProps Validator](./objectProps.md)
* [Advanced Object Validation](AdvancedObjectValidation.md)
* [Nested Objects](NestedObjects.md)
* [Array and Object Conventions](Conventions.md)
34 changes: 18 additions & 16 deletions docs/Composition/ValidatingObjects.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ import validate, {

// Define the rules for first name, last name, and birthYear
const validateProps = {
firstName: every([required(), length(2, 20)]),
lastName: every([required(), length(2, 20)]),
birthYear: range(1900, 2018)
firstName: every([
required(),
length({minLength: 2, maxLength: 20})
]),
lastName: every([
required(),
length({minLength: 2, maxLength: 20})
]),
birthYear: range({min: 1900, max: 2018})
};

// Create a person
Expand All @@ -24,39 +30,35 @@ const person = {
};

// Validate the person's properties
const props = {
const personProps = {
firstName: validate(validateProps.firstName, person.firstName),
lastName: validate(validateProps.lastName, person.lastName),
birthYear: validate(validateProps.birthYear, person.birthYear)
};
```

With this example, we have very primitive object property validation. The `props` output includes the validation results for each property, but there isn't anything providing a top-level `isValid` prop on the results. Let's add that in.
With this example, we have very primitive object property validation. The `personProps` output includes the validation results for each property, but there isn't anything providing a top-level `isValid` prop on the results. Let's add that in.

``` jsx
// Validate the person's properties
const props = {
const personProps = {
firstName: validate(rules.firstName, person.firstName),
lastName: validate(rules.lastName, person.lastName),
birthYear: validate(rules.birthYear, person.birthYear)
};

// Create a top-level result including the results from the props
// Create a top-level result including the results from personProps
const result = {
props,
personProps,
isValid: (
props.firstName.isValid &&
props.lastName.isValid &&
props.birthYear.isValid
personProps.firstName.isValid &&
personProps.lastName.isValid &&
personProps.birthYear.isValid
),
value: person
};
```

The top-level result also includes the `value` to be consistent with the output of other validators.

At this point, we can see a pattern where we would want a validator to iterate over the properties that have validators, validate each of those properties, and compose a final validation result for all props. Indeed, Strickland has such a validator built-in called `props`.

## Built-In Object Validation

* [props](props.md)
At this point, we can see a pattern where we would want a validator to iterate over the properties that have validators, validate each of those properties, and compose a final validation result for all props. Indeed, Strickland has such a validator built-in called `objectProps`.
8 changes: 4 additions & 4 deletions docs/Composition/each.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The first parameter to the `each` validator factory is the array of validators.
const atLeast5Chars = each(
[
required(),
minLength(5)
minLength({minLength: 5})
],
{message: 'Must have at least 5 characters'}
);
Expand All @@ -20,7 +20,7 @@ const result = validate(atLeast5Chars, '1234');
const requiredWithMinLength = each(
[
required(),
minLength((context) => context.minLength)
minLength((context) => ({minLength: context.minLength}))
],
(context) => ({message: `Must have at least ${context.minLength} characters`})
);
Expand All @@ -41,8 +41,8 @@ import validate, {

const mustExistWithLength5to10 = each([
required({message: 'Required'}),
minLength(5, {message: 'Must have at least 5 characters'}),
maxLength(10, {message: 'Must have at most 10 characters'})
minLength({minLength: 5, message: 'Must have at least 5 characters'}),
maxLength({maxLength: 10, message: 'Must have at most 10 characters'})
]);
const result = validate(mustExistWithLength5to10, '1234');

Expand Down
8 changes: 4 additions & 4 deletions docs/Composition/every.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The first parameter to the `every` validator factory is the array of validators.
const atLeast5Chars = every(
[
required(),
minLength(5)
minLength({minLength: 5})
],
{message: 'Must have at least 5 characters'}
);
Expand All @@ -20,7 +20,7 @@ const result = validate(atLeast5Chars, '1234');
const requiredWithMinLength = every(
[
required(),
minLength((context) => context.minLength)
minLength((context) => ({minLength: context.minLength}))
],
(context) => ({message: `Must have at least ${context.minLength} characters`})
);
Expand All @@ -37,8 +37,8 @@ import validate, {every, required, minLength, maxLength} from 'strickland';

const mustExistWithLength5to10 = every([
required({message: 'Required'}),
minLength(5, {message: 'Must have at least 5 characters'}),
maxLength(10, {message: 'Must have at most 10 characters'})
minLength({minLength: 5, message: 'Must have at least 5 characters'}),
maxLength({maxLength: 10, message: 'Must have at most 10 characters'})
]);
const result = validate(mustExistWithLength5to10, '1234');

Expand Down
Loading

0 comments on commit 4129242

Please sign in to comment.