From 396813b7920b8460766fd4c59ecfe2c533073523 Mon Sep 17 00:00:00 2001 From: Sam Verschueren Date: Wed, 16 May 2018 18:06:04 +0200 Subject: [PATCH] Add optional predicate - fixes #58 --- source/index.ts | 235 +++++++++++++++-------------- source/lib/operators/optional.ts | 14 ++ source/lib/predicates/predicate.ts | 19 ++- source/test/optional.ts | 7 + 4 files changed, 159 insertions(+), 116 deletions(-) create mode 100644 source/lib/operators/optional.ts create mode 100644 source/test/optional.ts diff --git a/source/index.ts b/source/index.ts index 5a3cc51..f8fe366 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,4 +1,4 @@ -import {Predicate} from './lib/predicates/predicate'; +import {Predicate, Context} from './lib/predicates/predicate'; import {AnyPredicate} from './lib/predicates/any'; import {testSymbol} from './lib/predicates/base-predicate'; import {StringPredicate} from './lib/predicates/string'; @@ -12,12 +12,20 @@ import {MapPredicate} from './lib/predicates/map'; import {WeakMapPredicate} from './lib/predicates/weak-map'; import {SetPredicate} from './lib/predicates/set'; import {WeakSetPredicate} from './lib/predicates/weak-set'; +import {optional} from './lib/operators/optional'; /** * @hidden */ export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; +export interface Operators { + /** + * Make the value optional. + */ + readonly optional: Ow; +} + export interface Ow { /** * Test if the value matches the predicate. @@ -180,114 +188,121 @@ export interface Ow { readonly iterable: Predicate>; } -const main = (value: T, predicate: Predicate | AnyPredicate) => (predicate as any)[testSymbol](value, main); +const ow = (context?: Context) => { + const main = (value: T, predicate: Predicate | AnyPredicate) => (predicate as any)[testSymbol](value, main); + + Object.defineProperties(main, { + create: { + value: (predicate: Predicate) => (value: T) => main(value, predicate) + }, + any: { + value: (...predicates: Predicate[]) => new AnyPredicate(predicates) + }, + optional: { + get: () => optional(ow, context) + }, + string: { + get: () => new StringPredicate(context) + }, + number: { + get: () => new NumberPredicate(context) + }, + boolean: { + get: () => new BooleanPredicate(context) + }, + undefined: { + get: () => new Predicate('undefined', context) + }, + null: { + get: () => new Predicate('null', context) + }, + nullOrUndefined: { + get: () => new Predicate('nullOrUndefined', context) + }, + nan: { + get: () => new Predicate('nan', context) + }, + symbol: { + get: () => new Predicate('symbol', context) + }, + array: { + get: () => new ArrayPredicate(context) + }, + object: { + get: () => new ObjectPredicate(context) + }, + date: { + get: () => new DatePredicate(context) + }, + error: { + get: () => new ErrorPredicate(context) + }, + map: { + get: () => new MapPredicate(context) + }, + weakMap: { + get: () => new WeakMapPredicate(context) + }, + set: { + get: () => new SetPredicate(context) + }, + weakSet: { + get: () => new WeakSetPredicate(context) + }, + function: { + get: () => new Predicate('function', context) + }, + buffer: { + get: () => new Predicate('buffer', context) + }, + regExp: { + get: () => new Predicate('regExp', context) + }, + promise: { + get: () => new Predicate('promise', context) + }, + typedArray: { + get: () => new Predicate('typedArray', context) + }, + int8Array: { + get: () => new Predicate('int8Array', context) + }, + uint8Array: { + get: () => new Predicate('uint8Array', context) + }, + uint8ClampedArray: { + get: () => new Predicate('uint8ClampedArray', context) + }, + int16Array: { + get: () => new Predicate('int16Array', context) + }, + uint16Array: { + get: () => new Predicate('uint16Array', context) + }, + int32Array: { + get: () => new Predicate('int32Array', context) + }, + uint32Array: { + get: () => new Predicate('uint32Array', context) + }, + float32Array: { + get: () => new Predicate('float32Array', context) + }, + float64Array: { + get: () => new Predicate('float64Array', context) + }, + arrayBuffer: { + get: () => new Predicate('arrayBuffer', context) + }, + dataView: { + get: () => new Predicate('dataView', context) + }, + iterable: { + get: () => new Predicate('iterable', context) + } + }); -Object.defineProperties(main, { - create: { - value: (predicate: Predicate) => (value: T) => main(value, predicate) - }, - any: { - value: (...predicates: Predicate[]) => new AnyPredicate(predicates) - }, - string: { - get: () => new StringPredicate() - }, - number: { - get: () => new NumberPredicate() - }, - boolean: { - get: () => new BooleanPredicate() - }, - undefined: { - get: () => new Predicate('undefined') - }, - null: { - get: () => new Predicate('null') - }, - nullOrUndefined: { - get: () => new Predicate('nullOrUndefined') - }, - nan: { - get: () => new Predicate('nan') - }, - symbol: { - get: () => new Predicate('symbol') - }, - array: { - get: () => new ArrayPredicate() - }, - object: { - get: () => new ObjectPredicate() - }, - date: { - get: () => new DatePredicate() - }, - error: { - get: () => new ErrorPredicate() - }, - map: { - get: () => new MapPredicate() - }, - weakMap: { - get: () => new WeakMapPredicate() - }, - set: { - get: () => new SetPredicate() - }, - weakSet: { - get: () => new WeakSetPredicate() - }, - function: { - get: () => new Predicate('function') - }, - buffer: { - get: () => new Predicate('buffer') - }, - regExp: { - get: () => new Predicate('regExp') - }, - promise: { - get: () => new Predicate('promise') - }, - typedArray: { - get: () => new Predicate('typedArray') - }, - int8Array: { - get: () => new Predicate('int8Array') - }, - uint8Array: { - get: () => new Predicate('uint8Array') - }, - uint8ClampedArray: { - get: () => new Predicate('uint8ClampedArray') - }, - int16Array: { - get: () => new Predicate('int16Array') - }, - uint16Array: { - get: () => new Predicate('uint16Array') - }, - int32Array: { - get: () => new Predicate('int32Array') - }, - uint32Array: { - get: () => new Predicate('uint32Array') - }, - float32Array: { - get: () => new Predicate('float32Array') - }, - float64Array: { - get: () => new Predicate('float64Array') - }, - arrayBuffer: { - get: () => new Predicate('arrayBuffer') - }, - dataView: { - get: () => new Predicate('dataView') - }, - iterable: { - get: () => new Predicate('iterable') - } -}); + return main as Ow & Operators; +}; -export default main as Ow; +export default ow(); diff --git a/source/lib/operators/optional.ts b/source/lib/operators/optional.ts new file mode 100644 index 0000000..caf1a92 --- /dev/null +++ b/source/lib/operators/optional.ts @@ -0,0 +1,14 @@ +import {Ow} from '../..'; +import {Context} from '../predicates/predicate'; + +/** + * Operator which makes the predicate optional. + * + * @hidden + * @param ow Function to construct a new Ow instance. + * @param context Current context object. + */ +export const optional = (ow: (context: Context) => Ow, context?: Context) => ow({ + ...context, + optional: true +}); diff --git a/source/lib/predicates/predicate.ts b/source/lib/predicates/predicate.ts index 906ceb0..ce8cc95 100644 --- a/source/lib/predicates/predicate.ts +++ b/source/lib/predicates/predicate.ts @@ -16,7 +16,8 @@ export interface Validator { * @hidden */ export interface Context { - validators: Validator[]; + validators?: Validator[]; + optional?: boolean; } /** @@ -28,12 +29,14 @@ export const validatorSymbol = Symbol('validators'); * @hidden */ export class Predicate implements BasePredicate { - constructor( - type: string, - private readonly context: Context = { + private readonly context: Context & {validators: Validator[]}; + + constructor(type: string, context?: Context) { + this.context = { + ...context, validators: [] - } - ) { + }; + this.addValidator({ message: value => `Expected argument to be of type \`${type}\` but received type \`${is(value)}\``, validator: value => (is as any)[type](value) @@ -45,6 +48,10 @@ export class Predicate implements BasePredicate { */ // tslint:disable completed-docs [testSymbol](value: T, main: Ow) { + if (this.context.optional && value === undefined) { + return; + } + for (const {validator, message} of this.context.validators) { const result = validator(value); diff --git a/source/test/optional.ts b/source/test/optional.ts new file mode 100644 index 0000000..a944802 --- /dev/null +++ b/source/test/optional.ts @@ -0,0 +1,7 @@ +import test from 'ava'; +import m from '..'; + +test('optional', t => { + t.notThrows(() => m('foo', m.optional.string)); + t.notThrows(() => m(undefined, m.optional.string)); +});