diff --git a/package.json b/package.json index cae2d19..a4c4d81 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "codecov": "^3.0.0", "del-cli": "^1.1.0", "license-webpack-plugin": "^1.1.1", + "lodash.isequal": "^4.5.0", "nyc": "^11.2.1", "tslint": "^5.8.0", "tslint-xo": "^0.2.0", diff --git a/source/lib/predicates/array.ts b/source/lib/predicates/array.ts new file mode 100644 index 0000000..1daa516 --- /dev/null +++ b/source/lib/predicates/array.ts @@ -0,0 +1,151 @@ +import * as isEqual from 'lodash.isequal'; +import {ow} from '../../ow'; +import {Predicate, Context} from './predicate'; + +export class ArrayPredicate extends Predicate { + constructor(context?: Context) { + super('array', context); + } + + /** + * Test an array to have a specific length. + * + * @param length The length of the array. + */ + length(length: number) { + return this.addValidator({ + message: value => `Expected array to have length \`${length}\`, got \`${value.length}\``, + validator: value => value.length === length + }); + } + + /** + * Test an array to have a minimum length. + * + * @param length The minimum length of the array. + */ + minLength(length: number) { + return this.addValidator({ + message: value => `Expected array to have a minimum length of \`${length}\`, got \`${value.length}\``, + validator: value => value.length >= length + }); + } + + /** + * Test an array to have a maximum length. + * + * @param length The maximum length of the array. + */ + maxLength(length: number) { + return this.addValidator({ + message: value => `Expected array to have a maximum length of \`${length}\`, got \`${value.length}\``, + validator: value => value.length <= length + }); + } + + /** + * Test an array to start with a specific value. The value is tested by identity, not structure. + * + * @param searchElement The value that should be the start of the array. + */ + startsWith(searchElement: any) { + return this.addValidator({ + message: value => `Expected array to start with \`${searchElement}\`, got \`${value[0]}\``, + validator: value => value[0] === searchElement + }); + } + + /** + * Test an array to end with a specific value. The value is tested by identity, not structure. + * + * @param searchElement The value that should be the end of the array. + */ + endsWith(searchElement: any) { + return this.addValidator({ + message: value => `Expected array to end with \`${searchElement}\`, got \`${value[value.length - 1]}\``, + validator: value => value[value.length - 1] === searchElement + }); + } + + /** + * Test an array to include all the provided elements. The values are tested by identity, not structure. + * + * @param searchElements The values that should be included in the array. + */ + includes(...searchElements: any[]) { + return this.addValidator({ + message: value => `Expected array to include all elements of \`${JSON.stringify(searchElements)}\`, got \`${JSON.stringify(value)}\``, + validator: value => searchElements.every(el => value.indexOf(el) !== -1) + }); + } + + /** + * Test an array to include any of the provided elements. The values are tested by identity, not structure. + * + * @param searchElements The values that should be included in the array. + */ + includesAny(...searchElements: any[]) { + return this.addValidator({ + message: value => `Expected array to include any element of \`${JSON.stringify(searchElements)}\`, got \`${JSON.stringify(value)}\``, + validator: value => searchElements.some(el => value.indexOf(el) !== -1) + }); + } + + /** + * Test an array to be empty. + */ + get empty() { + return this.addValidator({ + message: value => `Expected array to be empty, got \`${JSON.stringify(value)}\``, + validator: value => value.length === 0 + }); + } + + /** + * Test an array to be not empty. + */ + get nonEmpty() { + return this.addValidator({ + message: () => 'Expected array to not be empty', + validator: value => value.length > 0 + }); + } + + /** + * Test an array to be deeply equal to the provided array. + * + * @param expected Expected value to match. + */ + deepEqual(expected: any[]) { + return this.addValidator({ + message: value => `Expected array to be deeply equal to \`${JSON.stringify(expected)}\`, got \`${JSON.stringify(value)}\``, + validator: value => isEqual(value, expected) + }); + } + + /** + * Test all elements in the array to match to provided predicate. + * + * @param predicate The predicate that should be applied against every individual item. + */ + ofType(predicate: Predicate) { + let error; + + return this.addValidator({ + message: () => error, + validator: value => { + try { + for (const item of value) { + ow(item, predicate); + } + + return true; + } catch (err) { + error = err.message; + + return false; + } + } + }); + } +} diff --git a/source/ow.ts b/source/ow.ts index 9f3b05a..132e4f0 100644 --- a/source/ow.ts +++ b/source/ow.ts @@ -3,6 +3,7 @@ import {Predicate, validatorSymbol} from './lib/predicates/predicate'; import {StringPredicate} from './lib/predicates/string'; import {NumberPredicate} from './lib/predicates/number'; import {BooleanPredicate} from './lib/predicates/boolean'; +import {ArrayPredicate} from './lib/predicates/array'; export interface Ow { (value: any, predicate: Predicate): void; @@ -18,6 +19,10 @@ export interface Ow { * Test the value to be a boolean. */ boolean: BooleanPredicate; + /** + * Test the value to be an array. + */ + array: ArrayPredicate; } const main = (value: any, predicate: Predicate) => { @@ -38,6 +43,9 @@ Object.defineProperties(main, { }, boolean: { get: () => new BooleanPredicate() + }, + array: { + get: () => new ArrayPredicate() } }); diff --git a/source/test/array.ts b/source/test/array.ts new file mode 100644 index 0000000..a2cfc64 --- /dev/null +++ b/source/test/array.ts @@ -0,0 +1,69 @@ +import test from 'ava'; +import * as m from '..'; + +test('array', t => { + t.notThrows(() => m([], m.array)); + t.throws(() => m('12', m.array), 'Expected argument to be of type `array` but received type `string`'); +}); + +test('array.length', t => { + t.notThrows(() => m(['foo'], m.array.length(1))); + t.notThrows(() => m(['foo', 'bar'], m.array.length(2))); + t.throws(() => m(['foo'], m.array.length(2)), 'Expected array to have length `2`, got `1`'); +}); + +test('array.minLength', t => { + t.notThrows(() => m(['foo'], m.array.minLength(1))); + t.notThrows(() => m(['foo', 'bar'], m.array.minLength(1))); + t.throws(() => m(['foo'], m.array.minLength(2)), 'Expected array to have a minimum length of `2`, got `1`'); +}); + +test('array.maxLength', t => { + t.notThrows(() => m(['foo'], m.array.maxLength(1))); + t.notThrows(() => m(['foo', 'bar'], m.array.maxLength(4))); + t.throws(() => m(['foo', 'bar'], m.array.maxLength(1)), 'Expected array to have a maximum length of `1`, got `2`'); +}); + +test('array.startsWith', t => { + t.notThrows(() => m(['foo', 'bar'], m.array.startsWith('foo'))); + t.throws(() => m(['foo', 'bar'], m.array.startsWith('bar')), 'Expected array to start with `bar`, got `foo`'); +}); + +test('array.endsWith', t => { + t.notThrows(() => m(['foo', 'bar'], m.array.endsWith('bar'))); + t.throws(() => m(['foo', 'bar'], m.array.endsWith('foo')), 'Expected array to end with `foo`, got `bar`'); +}); + +test('array.includes', t => { + t.notThrows(() => m(['foo', 'bar'], m.array.includes('foo'))); + t.notThrows(() => m(['foo', 'bar', 'unicorn'], m.array.includes('foo', 'bar'))); + t.throws(() => m(['foo', 'bar'], m.array.includes('foo', 'unicorn')), 'Expected array to include all elements of `["foo","unicorn"]`, got `["foo","bar"]`'); +}); + +test('array.includesAny', t => { + t.notThrows(() => m(['foo', 'bar'], m.array.includesAny('foo'))); + t.notThrows(() => m(['foo', 'bar', 'unicorn'], m.array.includesAny('unicorn', 'rainbow'))); + t.throws(() => m(['foo', 'bar'], m.array.includesAny('unicorn')), 'Expected array to include any element of `["unicorn"]`, got `["foo","bar"]`'); +}); + +test('array.empty', t => { + t.notThrows(() => m([], m.array.empty)); + t.throws(() => m(['foo'], m.array.empty), 'Expected array to be empty, got `["foo"]`'); +}); + +test('array.nonEmpty', t => { + t.notThrows(() => m(['foo'], m.array.nonEmpty)); + t.throws(() => m([], m.array.nonEmpty), 'Expected array to not be empty'); +}); + +test('array.deepEqual', t => { + t.notThrows(() => m(['foo'], m.array.deepEqual(['foo']))); + t.notThrows(() => m(['foo', {id: 1}], m.array.deepEqual(['foo', {id: 1}]))); + t.throws(() => m(['foo', {id: 1}], m.array.deepEqual(['foo', {id: 2}])), 'Expected array to be deeply equal to `["foo",{"id":2}]`, got `["foo",{"id":1}]`'); +}); + +test('array.ofType', t => { + t.notThrows(() => m(['foo', 'bar'], m.array.ofType(m.string))); + t.notThrows(() => m(['foo', 'bar'], m.array.ofType(m.string.minLength(3)))); + t.throws(() => m(['foo', 'b'], m.array.ofType(m.string.minLength(3))), 'Expected string to have a minimum length of `3`, got `b`'); +}); diff --git a/tsconfig.json b/tsconfig.json index 4b32b1a..f6bdaa8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,6 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "allowSyntheticDefaultImports": true, "strictNullChecks": true, "strictFunctionTypes": true, "alwaysStrict": true