Skip to content

Commit

Permalink
feature: [n4s] templates (#509)
Browse files Browse the repository at this point in the history
  • Loading branch information
ealush committed Nov 19, 2020
1 parent 10e6e75 commit 54e500f
Show file tree
Hide file tree
Showing 23 changed files with 1,789 additions and 1,674 deletions.
2 changes: 2 additions & 0 deletions jsconfig.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"genJSConfig": "node ./scripts/genJsconfig",
"test": "jest --projects ./packages/*",
"lint": "eslint . --ignore-path .gitignore",
"pretest": "yarn genJSConfig"
"pretest": "yarn genJSConfig",
"docs": "node scripts/release/steps/updateDocs"
},
"husky": {
"hooks": {
Expand Down
1 change: 1 addition & 0 deletions packages/n4s/docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
- [List of Enforce Rules](./rules)
- [Business reated rules](./business_rules)
- [Schema validations](./coumpound)
- [Templates and Composites](./template)
- [Custom Enforce Rules](./custom)
18 changes: 10 additions & 8 deletions packages/n4s/docs/compound.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Compound Rules - Rules that validate using other rules
# Shape and schema validation

Alongside the list of rules that only accept data provided by the user, enforce also supports compound rules - these are rules that accept other rules as their arguments. These rules let you validate more complex scenarios with the ergonomics of enforce.

Expand All @@ -8,7 +8,7 @@ Alongside the list of rules that only accept data provided by the user, enforce
- [enforec.loose() - loose shape matching](#loose)
- [enforce.isArrayOf() - array shape matching](#isarrayof)

## <a id="anyof"></a>enforce.anyOf() - either/or validations
## enforce.anyOf() - either/or validations :id=anyof

Sometimes a value has more than one valid possibilities, `any` lets us validate that a value passes _at least_ one of the supplied rules.

Expand All @@ -17,7 +17,7 @@ enforce(value).anyOf(enforce.isString(), enforce.isArray()).isNotEmpty();
// A valid value would either an array or a string.
```

## <a id="shape"></a>enforce.shape() - Lean schema validation.
## enforce.shape() - Lean schema validation. :id=shape

`enforce.shape()` validates the structure of an object.

Expand Down Expand Up @@ -63,7 +63,7 @@ enforce({
});
```

### <a id="optional"></a>enforce.optional() - nullable keys
### enforce.optional() - nullable keys :id=optional

-- Optional can only be used within enforce.shape().

Expand Down Expand Up @@ -92,19 +92,21 @@ enforce({
});
```

## <a id="loose"></a>enforec.loose() - loose shape matching
## enforec.loose() - loose shape matching :id=loose

By default, shape will treat excess keys in your data object as validation errors. If you wish to allow support for excess keys in your object's shape, you can use `enforce.loose()` which is a shorthand to `enforce.shape(data, shape, { loose: true })`.

```js
enforce({ name: 'Laura', code: 'x23' }).shape({ name: enforce.isString() }); // 🚨 This will throw an error because `code` is not defined in the shape
enforce({ name: 'Laura', code: 'x23' }).shape({ name: enforce.isString() });
// 🚨 This will throw an error because `code` is not defined in the shape
```

```js
enforce({ name: 'Laura', code: 'x23' }).loose({ name: enforce.isString() }); // ✅ This will pass with `code` not being validated
enforce({ name: 'Laura', code: 'x23' }).loose({ name: enforce.isString() });
// ✅ This will pass with `code` not being validated
```

## <a id="isarrayof"></a>enforce.isArrayOf() - array shape matching
## enforce.isArrayOf() - array shape matching :id=isarrayof

enforce.isArrayOf can be used to determine the allowed types and values within an array. It will run against each element in the array, and will only pass if all items meet at least one of the validation rules.

Expand Down
2 changes: 1 addition & 1 deletion packages/n4s/docs/custom.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Custom enforce rules
# Creating Custom Rules

To make it easier to reuse logic across your application, sometimes you would want to encapsulate bits of logic in rules that you can use later on, for example, "what's considered a valid email".

Expand Down
53 changes: 53 additions & 0 deletions packages/n4s/docs/template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Enforce templates

When you have common patterns you need to repeat in multiple places, it might be simpler to store them as templates.

For example, let's assume that all across our systems, a username must be a non-numeric string that's longer than 3 characters.

```js
enforce(username).isString().isNotEmpty().isNotNumeric().longerThan(3);
```

This is quite simple to understand, but if you have to keep it up-to-date in every place you validate a username, you may eventually have inconsistent or out-of-date validations.

It can be beneficial in that case to keep this enforcement as a template for later use:

```js
const Username = enforce.template(
enforce.isString().isNotEmpty().isNotNumeric().longerThan(3)
);
```

And then, anywhere else you can use your new `Username` template to validate usernames all across your application:

```js
Username('myUsername'); // passes
Username('1234'); // throws
Username('ab'); // throws
```

You can also use templates inside other compound rules, such as `shape`, `isArrayOf` or `anyOf`.

```js
enforce({
username: 'someusername',
}).shape({ username: Username });

enforce(['user1', 'user2']).isArrayOf(Username);
```

Templates can also be nested and composited:

```js
const RequiredField = enforce.template(enforce.isNotEmpty());
const NumericString = enforce.template(enforce.isNumeric().isString());

const EvenNumeric = enforce.template(
RequiredField,
NumericString,
enforce.isEven()
);

EvenNumeric('10'); // pases
EvenNumeric('1'); // throws
```
91 changes: 91 additions & 0 deletions packages/n4s/src/enforce/__tests__/enforce.template.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import enforce from 'enforce';
import runtimeRules from 'runtimeRules';

describe('enforce.template', () => {
test('enforce has `template` static function', () => {
expect(typeof enforce.template).toBe('function');
});

describe('Return value', () => {
it('Should return a function', () => {
const x = enforce.template(enforce.isArray(), enforce.longerThan(5));

expect(typeof x).toBe('function');
});

it('Should have a static function called `test`', () => {
const x = enforce.template(enforce.isArray(), enforce.longerThan(5));

expect(typeof x.test).toBe('function');
});
});

describe('When the returned function gets called', () => {
it('Should throw an error when invalid', () => {
const x = enforce.template(enforce.isArray(), enforce.longerThan(2));
const y = enforce.template(enforce.isString(), enforce.isNumeric());
expect(() => x([])).toThrow();
expect(() => y('')).toThrow();
expect(() => x([1])).toThrow();
expect(() => x('hello')).toThrow();
expect(() => y('hello')).toThrow();
});

it('Should return silently when valid', () => {
const x = enforce.template(enforce.isArray(), enforce.longerThan(2));
const y = enforce.template(enforce.isString(), enforce.isNumeric());

x([1, 2, 3]);
y('123');
});

it('Should return rules proxy', () => {
const x = enforce.template(enforce.isArray());
expect(x([])).toEqual(enforce(1));
Object.keys(runtimeRules).forEach(k => {
expect(typeof x([])[k]).toBe('function');
});
});
});

describe('When the `test` function gets called', () => {
it('Should return `false` when invalid', () => {
const x = enforce.template(enforce.isNumber().greaterThan(50));
const y = enforce.template(
enforce.isString().isNumeric(),
enforce.isOdd()
);

expect(x.test(10)).toBe(false);
expect(x.test('90')).toBe(false);
expect(y.test(true)).toBe(false);
expect(x.test('90')).toBe(false);
});
it('Should return `true` when valid', () => {
const x = enforce.template(enforce.isNumber().greaterThan(50));
const y = enforce.template(
enforce.isString().isNumeric(),
enforce.isOdd()
);

expect(x.test(51)).toBe(true);
expect(y.test('11')).toBe(true);
});
});

describe('Nested templates', () => {
it('Should validate all joined templates', () => {
const X = enforce.template(enforce.isString(), enforce.longerThan(2));
const Y = enforce.template(enforce.isNumeric());
const Z = enforce.template(enforce.isEven());

expect(() => enforce.template(X, Y, Z)('a')).toThrow();
expect(() => enforce.template(X, Y, Z)('abc')).toThrow();
expect(() => enforce.template(X, Y, Z)('111')).toThrow();
expect(() => enforce.template(X, Y, Z)(112)).toThrow();
enforce.template(X, Y, Z)('112');
enforce.template(X, Y)('111');
enforce.template(X)('abc');
});
});
});
16 changes: 16 additions & 0 deletions packages/n4s/src/enforce/enforce.extend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import bindLazyRule from 'bindLazyRule';
import genRuleProxy from 'genRuleProxy';
import proxySupported from 'proxySupported';
import runtimeRules from 'runtimeRules';

export default function bindExtend(enforce, Enforce) {
enforce.extend = customRules => {
Object.assign(runtimeRules, customRules);

if (!proxySupported()) {
genRuleProxy(Enforce, bindLazyRule);
}

return enforce;
};
}
14 changes: 4 additions & 10 deletions packages/n4s/src/enforce/enforce.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import bindLazyRule from 'bindLazyRule';
import bindExtend from 'enforce.extend';
import bindTemplate from 'enforce.template';
import runner from 'enforceRunner';
import genRuleProxy from 'genRuleProxy';
import proxySupported from 'proxySupported';
import runtimeRules from 'runtimeRules';

const Enforce = value => {
Expand All @@ -14,14 +15,7 @@ const Enforce = value => {

const enforce = genRuleProxy(Enforce, bindLazyRule);

enforce.extend = customRules => {
Object.assign(runtimeRules, customRules);

if (!proxySupported()) {
genRuleProxy(Enforce, bindLazyRule);
}

return enforce;
};
bindExtend(enforce, Enforce);
bindTemplate(enforce);

export default enforce;
23 changes: 23 additions & 0 deletions packages/n4s/src/enforce/enforce.template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import runner from 'enforceRunner';
import genRuleProxy from 'genRuleProxy';
import isFunction from 'isFunction';
import runLazyRules from 'runLazyRules';
import runtimeRules from 'runtimeRules';

export default function bindTemplate(enforce) {
enforce.template = (...rule) => {
const template = value => {
runner(runLazyRules.bind(null, rule), value);
const proxy = genRuleProxy({}, ruleName => (...args) => {
runner(runtimeRules[ruleName], value, args);
return proxy;
});
return proxy;
};

template.test = getValue =>
runLazyRules(rule, isFunction(getValue) ? getValue() : getValue);

return template;
};
}
4 changes: 4 additions & 0 deletions packages/vest/docs/_sidebar.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
- [Getting Started](./getting_started)
- [enforce](./enforce)
- [List of Enforce rules](./n4s/rules)
- [Creating Custom Rules](./n4s/custom)
- [Shape and schema validation](./n4s/compound)
- [Enforce templates](./n4s/template)
- [The result object](./result)
- [test](./test)
- [How to fail a test](./test#failing_a_test)
Expand Down
13 changes: 13 additions & 0 deletions packages/vest/docs/_sidebar.md.bak
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- [Getting Started](./getting_started)
- [enforce](./enforce){{ENFORCE_DOCS}}
- [The result object](./result)
- [test](./test)
- [How to fail a test](./test#failing_a_test)
- [Warn only tests](./warn)
- [Grouping tests](./group)
- [Understanding Vest's state](./state)
- Advanced cases
- [Cross Field Validations](./cross_field_validations)
- [Excluding or including tests](./exclusion)
- [Using with node](./node)
- [Utilities](./utilities)
Loading

0 comments on commit 54e500f

Please sign in to comment.