diff --git a/source/index.ts b/source/index.ts index e38d412..c03e223 100644 --- a/source/index.ts +++ b/source/index.ts @@ -156,9 +156,11 @@ export interface Ow { const main = (value: T, predicate: Predicate) => { for (const {validator, message} of predicate[validatorSymbol]) { - if (!validator(value)) { + const result = validator(value); + + if (typeof result !== 'boolean' || !result) { // TODO: Modify the stack output to show the original `ow()` call instead of this `throw` statement - throw new ArgumentError(message(value), main); + throw new ArgumentError(message(value, result), main); } } }; diff --git a/source/lib/predicates/map.ts b/source/lib/predicates/map.ts index 0360cfd..c87c2d8 100644 --- a/source/lib/predicates/map.ts +++ b/source/lib/predicates/map.ts @@ -1,6 +1,7 @@ import * as isEqual from 'lodash.isequal'; -import ow from '../..'; import {Predicate, Context} from './predicate'; +import hasItems from '../utils/has-items'; +import ofType from '../utils/of-type'; export class MapPredicate extends Predicate> { constructor(context?: Context) { @@ -49,25 +50,9 @@ export class MapPredicate extends Predicate> { * @param keys The keys that should be a key in the Map. */ hasKeys(...keys: any[]) { - const missingKeys: any[] = []; - return this.addValidator({ - message: () => `Expected Map to have keys \`${JSON.stringify(missingKeys)}\``, - validator: map => { - for (const key of keys) { - if (map.has(key)) { - continue; - } - - missingKeys.push(key); - - if (missingKeys.length === 5) { - return false; - } - } - - return missingKeys.length === 0; - } + message: (_, missingKeys) => `Expected Map to have keys \`${JSON.stringify(missingKeys)}\``, + validator: map => hasItems(map, keys) }); } @@ -89,27 +74,9 @@ export class MapPredicate extends Predicate> { * @param values The values that should be a value in the Map. */ hasValues(...values: any[]) { - const missingValues: any[] = []; - return this.addValidator({ - message: () => `Expected Map to have values \`${JSON.stringify(missingValues)}\``, - validator: map => { - const valueSet = new Set(map.values()); - - for (const value of values) { - if (valueSet.has(value)) { - continue; - } - - missingValues.push(value); - - if (missingValues.length === 5) { - return false; - } - } - - return missingValues.length === 0; - } + message: (_, missingValues) => `Expected Map to have values \`${JSON.stringify(missingValues)}\``, + validator: map => hasItems(new Set(map.values()), values) }); } @@ -135,23 +102,9 @@ export class MapPredicate extends Predicate> { * @param predicate The predicate that should be applied against every key in the Map. */ keysOfType(predicate: Predicate) { - let error: string; - return this.addValidator({ - message: () => error, - validator: map => { - try { - for (const item of map.keys()) { - ow(item, predicate); - } - - return true; - } catch (err) { - error = err.message; - - return false; - } - } + message: (_, error) => error, + validator: map => ofType(map.keys(), predicate) }); } @@ -161,23 +114,9 @@ export class MapPredicate extends Predicate> { * @param predicate The predicate that should be applied against every value in the Map. */ valuesOfType(predicate: Predicate) { - let error: string; - return this.addValidator({ - message: () => error, - validator: map => { - try { - for (const item of map.values()) { - ow(item, predicate); - } - - return true; - } catch (err) { - error = err.message; - - return false; - } - } + message: (_, error) => error, + validator: map => ofType(map.values(), predicate) }); } diff --git a/source/lib/predicates/predicate.ts b/source/lib/predicates/predicate.ts index 420e2a7..d748a6d 100644 --- a/source/lib/predicates/predicate.ts +++ b/source/lib/predicates/predicate.ts @@ -5,8 +5,8 @@ import {not} from '../operators/not'; * @hidden */ export interface Validator { - message(value: T): string; - validator(value: T): boolean; + message(value: T, result?: any): string; + validator(value: T): any; } /** diff --git a/source/lib/predicates/set.ts b/source/lib/predicates/set.ts index a3f765f..0ca8962 100644 --- a/source/lib/predicates/set.ts +++ b/source/lib/predicates/set.ts @@ -1,6 +1,7 @@ import * as isEqual from 'lodash.isequal'; -import ow from '../..'; import {Predicate, Context} from './predicate'; +import hasItems from '../utils/has-items'; +import ofType from '../utils/of-type'; export class SetPredicate extends Predicate> { constructor(context?: Context) { @@ -49,25 +50,9 @@ export class SetPredicate extends Predicate> { * @param items The items that should be a item in the Set. */ has(...items: any[]) { - const missingItems: any[] = []; - return this.addValidator({ - message: () => `Expected Set to have items \`${JSON.stringify(missingItems)}\``, - validator: set => { - for (const item of items) { - if (set.has(item)) { - continue; - } - - missingItems.push(item); - - if (missingItems.length === 5) { - return false; - } - } - - return missingItems.length === 0; - } + message: (_, missingItems) => `Expected Set to have items \`${JSON.stringify(missingItems)}\``, + validator: set => hasItems(set, items) }); } @@ -89,23 +74,9 @@ export class SetPredicate extends Predicate> { * @param predicate The predicate that should be applied against every item in the Set. */ ofType(predicate: Predicate) { - let error: string; - return this.addValidator({ - message: () => error, - validator: set => { - try { - for (const item of set.keys()) { - ow(item, predicate); - } - - return true; - } catch (err) { - error = err.message; - - return false; - } - } + message: (_, error) => error, + validator: set => ofType(set, predicate) }); } diff --git a/source/lib/predicates/weak-map.ts b/source/lib/predicates/weak-map.ts index 4eac696..07e9626 100644 --- a/source/lib/predicates/weak-map.ts +++ b/source/lib/predicates/weak-map.ts @@ -1,4 +1,5 @@ import {Predicate, Context} from './predicate'; +import hasItems from '../utils/has-items'; export class WeakMapPredicate extends Predicate> { constructor(context?: Context) { @@ -11,25 +12,9 @@ export class WeakMapPredicate extends Predicate> { * @param keys The keys that should be a key in the WeakMap. */ hasKeys(...keys: any[]) { - const missingKeys: any[] = []; - return this.addValidator({ - message: () => `Expected WeakMap to have keys \`${JSON.stringify(missingKeys)}\``, - validator: map => { - for (const key of keys) { - if (map.has(key)) { - continue; - } - - missingKeys.push(key); - - if (missingKeys.length === 5) { - return false; - } - } - - return missingKeys.length === 0; - } + message: (_, missingKeys) => `Expected WeakMap to have keys \`${JSON.stringify(missingKeys)}\``, + validator: map => hasItems(map, keys) }); } diff --git a/source/lib/predicates/weak-set.ts b/source/lib/predicates/weak-set.ts index 0f9cbaa..ba50606 100644 --- a/source/lib/predicates/weak-set.ts +++ b/source/lib/predicates/weak-set.ts @@ -1,4 +1,5 @@ import {Predicate, Context} from './predicate'; +import hasItems from '../utils/has-items'; export class WeakSetPredicate extends Predicate> { constructor(context?: Context) { @@ -11,25 +12,9 @@ export class WeakSetPredicate extends Predicate> { * @param items The items that should be a item in the WeakSet. */ has(...items: any[]) { - const missingItems: any[] = []; - return this.addValidator({ - message: () => `Expected WeakSet to have items \`${JSON.stringify(missingItems)}\``, - validator: set => { - for (const key of items) { - if (set.has(key)) { - continue; - } - - missingItems.push(key); - - if (missingItems.length === 5) { - return false; - } - } - - return missingItems.length === 0; - } + message: (_, missingItems) => `Expected WeakSet to have items \`${JSON.stringify(missingItems)}\``, + validator: set => hasItems(set, items) }); } diff --git a/source/lib/utils/has-items.ts b/source/lib/utils/has-items.ts new file mode 100644 index 0000000..b7c1320 --- /dev/null +++ b/source/lib/utils/has-items.ts @@ -0,0 +1,27 @@ +type Collection = Set | Map | WeakSet | WeakMap; + +/** + * Retrieve the missing values in a collection based on an array of items. + * + * @hidden + * @param source Source collection to search through. + * @param items Items to search for. + * @param maxValues Maximum number of values after the search process is stopped. (Default: 5) + */ +export default (source: Collection, items: any[], maxValues = 5) => { + const missingValues: any[] = []; + + for (const value of items) { + if (source.has(value)) { + continue; + } + + missingValues.push(value); + + if (missingValues.length === maxValues) { + return missingValues; + } + } + + return missingValues.length === 0 ? true : missingValues; +}; diff --git a/source/lib/utils/of-type.ts b/source/lib/utils/of-type.ts new file mode 100644 index 0000000..624cd26 --- /dev/null +++ b/source/lib/utils/of-type.ts @@ -0,0 +1,21 @@ +import ow from '../..'; +import {Predicate} from '../predicates/predicate'; + +/** + * Test all the values in the collection against a provided predicate. + * + * @hidden + * @param source Source collection to test. + * @param predicate Predicate to test every item in the source collection against. + */ +export default (source: IterableIterator | Set, predicate: Predicate): boolean | string => { + try { + for (const item of source) { + ow(item, predicate); + } + + return true; + } catch (err) { + return err.message; + } +};