From 59a71d7d3db94f2e72e387b96fee15919378789b Mon Sep 17 00:00:00 2001 From: Steven Sojka Date: Sat, 17 Mar 2018 08:01:13 -0500 Subject: [PATCH] feat(all): decorators with optional arguments do not require invocation BREAKING CHANGE: This may cause issue with tools that rely on static analysis of the decorators. Since the deocorators are typed with intersections they must be removed from a static function wrapper. --- src/applicators/InvokeApplicator.ts | 2 +- src/attempt.ts | 4 +-- src/bind.ts | 4 +-- src/curry.ts | 4 +-- src/curryAll.spec.ts | 15 ++++++++ src/curryAll.ts | 14 +++----- src/curryRight.spec.ts | 14 ++++++++ src/curryRight.ts | 14 +++----- src/curryRightAll.spec.ts | 14 ++++++++ src/curryRightAll.ts | 14 +++----- src/defer.spec.ts | 51 ++++++++++++++++++++++++++ src/defer.ts | 14 +++----- src/factory/DecoratorFactory.ts | 33 ++++++++++++----- src/factory/common.ts | 17 +++++---- src/memoize.spec.ts | 31 ++++++++++++++++ src/memoize.ts | 16 ++++----- src/memoizeAll.spec.ts | 26 ++++++++++++++ src/memoizeAll.ts | 14 +++----- src/mixin.ts | 4 +-- src/negate.spec.ts | 13 +++++++ src/negate.ts | 34 ++++++++++++------ src/once.ts | 32 ++++++++++++----- src/onceAll.ts | 33 ++++++++++++----- src/overArgs.ts | 18 ++++++++++ src/partial.spec.ts | 8 ++--- src/partial.ts | 21 +++++++++++ src/rest.spec.ts | 16 ++++++++- src/rest.ts | 14 +++----- src/spread.spec.ts | 16 ++++++++- src/spread.ts | 14 +++----- src/tap.spec.ts | 15 +++++++- src/tap.ts | 14 +++----- src/throttle.spec.ts | 21 +++++++++++ src/throttle.ts | 34 ++++++------------ src/unary.spec.ts | 15 +++++++- src/unary.ts | 14 +++----- src/utils.ts | 1 + src/utils/isDecoratorArgs.spec.ts | 56 +++++++++++++++++++++++++++++ src/utils/isDecoratorArgs.ts | 11 ++++++ 39 files changed, 530 insertions(+), 175 deletions(-) create mode 100644 src/utils/isDecoratorArgs.spec.ts create mode 100644 src/utils/isDecoratorArgs.ts diff --git a/src/applicators/InvokeApplicator.ts b/src/applicators/InvokeApplicator.ts index 39e953e..d6345d8 100644 --- a/src/applicators/InvokeApplicator.ts +++ b/src/applicators/InvokeApplicator.ts @@ -6,4 +6,4 @@ export class InvokeApplicator extends Applicator { return execute(value.bind(this), ...invokeArgs, ...args); } } -} \ No newline at end of file +} diff --git a/src/attempt.ts b/src/attempt.ts index a9f2d35..2a7c2b1 100644 --- a/src/attempt.ts +++ b/src/attempt.ts @@ -1,7 +1,7 @@ import attempt = require('lodash/attempt'); import partial = require('lodash/partial'); -import { DecoratorConfig, DecoratorFactory, TypedMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedMethodDecorator } from './factory'; import { PreValueApplicator } from './applicators'; const attemptFn = (fn: () => void) => partial(attempt, fn); @@ -31,6 +31,6 @@ export const Attempt = DecoratorFactory.createDecorator( new DecoratorConfig(attemptFn, new PreValueApplicator(), { optionalParams: true }) -) as TypedMethodDecorator; +) as BiTypedMethodDecorator; export { Attempt as attempt }; export default Attempt; diff --git a/src/bind.ts b/src/bind.ts index 6e3cf88..7ac7b9b 100644 --- a/src/bind.ts +++ b/src/bind.ts @@ -1,6 +1,6 @@ import bind = require('lodash/bind'); -import { DecoratorConfig, DecoratorFactory, TypedMethodDecorator1 } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedMethodDecorator1 } from './factory'; import { BindApplicator } from './applicators'; /** @@ -32,6 +32,6 @@ export const Bind = DecoratorFactory.createInstanceDecorator( new DecoratorConfig(bind, new BindApplicator(), { optionalParams: true }) -) as TypedMethodDecorator1; +) as BiTypedMethodDecorator1; export { Bind as bind, }; export default Bind; diff --git a/src/curry.ts b/src/curry.ts index fec0cc0..08d8c39 100644 --- a/src/curry.ts +++ b/src/curry.ts @@ -1,6 +1,6 @@ import curry = require('lodash/curry'); -import { DecoratorConfig, DecoratorFactory, TypedMethodDecorator1 } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedMethodDecorator1 } from './factory'; import { PreValueApplicator } from './applicators'; /** @@ -31,6 +31,6 @@ import { PreValueApplicator } from './applicators'; */ export const Curry = DecoratorFactory.createInstanceDecorator( new DecoratorConfig(curry, new PreValueApplicator(), { bound: true, optionalParams: true }) -) as TypedMethodDecorator1; +) as BiTypedMethodDecorator1; export { Curry as curry }; export default Curry; diff --git a/src/curryAll.spec.ts b/src/curryAll.spec.ts index 12e36a6..8881d57 100644 --- a/src/curryAll.spec.ts +++ b/src/curryAll.spec.ts @@ -18,6 +18,21 @@ describe('curryAll', () => { expect(add5(10)).to.equal(15); }); + it('should curry the method with default arity (paramless)', () => { + class MyClass { + @CurryAll + add(a: any, b?: any) { + return a + b; + } + } + + const myClass = new MyClass(); + const add5 = myClass.add(5); + + expect(add5).to.be.an.instanceOf(Function); + expect(add5(10)).to.equal(15); + }); + it('should curry the method with fixed arity', () => { class MyClass { @CurryAll(2) diff --git a/src/curryAll.ts b/src/curryAll.ts index 00f25b4..06e1284 100644 --- a/src/curryAll.ts +++ b/src/curryAll.ts @@ -1,12 +1,8 @@ import curry = require('lodash/curry'); -import { DecoratorConfig, DecoratorFactory, LodashMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedDecorator1 } from './factory'; import { PreValueApplicator } from './applicators'; -const decorator = DecoratorFactory.createDecorator( - new DecoratorConfig(curry, new PreValueApplicator()) -); - /** * Creates a function that accepts arguments of func and either invokes func returning its result, if at least arity number of arguments have been provided, or returns a function that accepts the remaining func arguments, and so on. * The arity of func may be specified if func.length is not sufficient. @@ -31,8 +27,8 @@ const decorator = DecoratorFactory.createDecorator( * * add5AndMultiply(10); // => 15 */ -export function CurryAll(arity?: number): LodashMethodDecorator { - return decorator(arity); -} +export const CurryAll = DecoratorFactory.createDecorator( + new DecoratorConfig(curry, new PreValueApplicator(), { optionalParams: true }) +) as BiTypedDecorator1; export { CurryAll as curryAll }; -export default decorator; +export default CurryAll; diff --git a/src/curryRight.spec.ts b/src/curryRight.spec.ts index 0e469db..52058ab 100644 --- a/src/curryRight.spec.ts +++ b/src/curryRight.spec.ts @@ -17,6 +17,20 @@ describe('curryRight', () => { expect(set5(10)).to.eql([ 10, 5 ]); }); + it('should curry the method with default arity (paramless)', () => { + class MyClass { + @CurryRight + add(a: any, b?: any) { + return [ a, b ]; + } + } + + const myClass = new MyClass(); + const set5 = myClass.add(5) as any; + + expect(set5(10)).to.eql([ 10, 5 ]); + }); + it('should retain the class context', () => { class MyClass { value = 'blorg'; diff --git a/src/curryRight.ts b/src/curryRight.ts index 389d3b0..3665c3c 100644 --- a/src/curryRight.ts +++ b/src/curryRight.ts @@ -1,12 +1,8 @@ import curryRight = require('lodash/curryRight'); -import { DecoratorConfig, DecoratorFactory, LodashMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedMethodDecorator1 } from './factory'; import { PreValueApplicator } from './applicators'; -const decorator = DecoratorFactory.createInstanceDecorator( - new DecoratorConfig(curryRight, new PreValueApplicator(), { bound: true }) -); - /** * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight instead of _.partial. * The arity of func may be specified if func.length is not sufficient. @@ -33,8 +29,8 @@ const decorator = DecoratorFactory.createInstanceDecorator( * * add5AndMultiply(10); // => 30 */ -export function CurryRight(arity?: number): LodashMethodDecorator { - return decorator(arity); -} +export const CurryRight = DecoratorFactory.createInstanceDecorator( + new DecoratorConfig(curryRight, new PreValueApplicator(), { bound: true, optionalParams: true }) +) as BiTypedMethodDecorator1; export { CurryRight as curryRight }; -export default decorator; +export default CurryRight; diff --git a/src/curryRightAll.spec.ts b/src/curryRightAll.spec.ts index 0e98302..6a8b304 100644 --- a/src/curryRightAll.spec.ts +++ b/src/curryRightAll.spec.ts @@ -16,4 +16,18 @@ describe('curryRightAll', () => { expect(set5(10)).to.eql([ 10, 5 ]); }); + + it('should curry the method with default arity (paramless)', () => { + class MyClass { + @CurryRightAll + add(a: any, b?: any) { + return [ a, b ]; + } + } + + const myClass = new MyClass(); + const set5 = myClass.add(5) as any; + + expect(set5(10)).to.eql([ 10, 5 ]); + }); }); diff --git a/src/curryRightAll.ts b/src/curryRightAll.ts index 572ab28..8b6c224 100644 --- a/src/curryRightAll.ts +++ b/src/curryRightAll.ts @@ -1,12 +1,8 @@ import curryRight = require('lodash/curryRight'); -import { DecoratorConfig, DecoratorFactory, LodashMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedMethodDecorator1 } from './factory'; import { PreValueApplicator } from './applicators'; -const decorator = DecoratorFactory.createDecorator( - new DecoratorConfig(curryRight, new PreValueApplicator()) -); - /** * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight instead of _.partial. * The arity of func may be specified if func.length is not sufficient. @@ -31,8 +27,8 @@ const decorator = DecoratorFactory.createDecorator( * * add5AndMultiply(10); // => 15 */ -export function CurryRightAll(arity?: number): LodashMethodDecorator { - return decorator(arity); -} +export const CurryRightAll = DecoratorFactory.createDecorator( + new DecoratorConfig(curryRight, new PreValueApplicator(), { optionalParams: true }) +) as BiTypedMethodDecorator1; export { CurryRightAll as curryRightAll }; -export default decorator; +export default CurryRightAll; diff --git a/src/defer.spec.ts b/src/defer.spec.ts index 12b2950..90f0407 100644 --- a/src/defer.spec.ts +++ b/src/defer.spec.ts @@ -27,6 +27,29 @@ describe('defer', () => { }, 0); }); + it('should defer the method (paramless)', (done) => { + const _spy = spy(); + + class MyClass { + @Defer + fn(...args: any[]) { + expect(this, 'context').to.equal(myClass); + _spy(...args); + } + } + + const myClass = new MyClass(); + + myClass.fn(10); + expect(_spy.called).to.be.false; + + setTimeout(() => { + expect(_spy.callCount).to.equal(1); + expect(_spy.getCalls()[0].args).to.eql([ 10 ]); + done(); + }, 0); + }); + it('should debounce the property setter', (done) => { class MyClass { private _value: number = 100; @@ -54,4 +77,32 @@ describe('defer', () => { done(); }, 0); }); + + it('should debounce the property setter (paramless)', (done) => { + class MyClass { + private _value: number = 100; + + @Defer + set value(value: number) { + expect(this, 'context').to.equal(myClass); + this._value = value; + } + + get value(): number { + return this._value; + } + } + + const myClass = new MyClass(); + + myClass.value = 5; + myClass.value = 15; + + expect(myClass.value).to.equal(100); + + setTimeout(() => { + expect(myClass.value).to.equal(15); + done(); + }, 0); + }); }); diff --git a/src/defer.ts b/src/defer.ts index ba68764..499b852 100644 --- a/src/defer.ts +++ b/src/defer.ts @@ -1,12 +1,8 @@ import defer = require('lodash/defer'); -import { DecoratorConfig, DecoratorFactory, LodashMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedDecoratorN } from './factory'; import { InvokeApplicator } from './applicators'; -const decorator = DecoratorFactory.createDecorator( - new DecoratorConfig(defer, new InvokeApplicator(), { setter: true }) -); - /** * Defers invoking the func until the current call stack has cleared. Any additional arguments are provided to func when it's invoked. * @@ -32,8 +28,8 @@ const decorator = DecoratorFactory.createDecorator( * myClass.value; // => 110; * }, 0); */ -export function Defer(...args: any[]): LodashMethodDecorator { - return decorator(...args); -} +export const Defer = DecoratorFactory.createDecorator( + new DecoratorConfig(defer, new InvokeApplicator(), { setter: true, optionalParams: true }) +) as BiTypedDecoratorN; export { Defer as defer }; -export default decorator; +export default Defer; diff --git a/src/factory/DecoratorFactory.ts b/src/factory/DecoratorFactory.ts index dfccbd3..20791cb 100644 --- a/src/factory/DecoratorFactory.ts +++ b/src/factory/DecoratorFactory.ts @@ -6,7 +6,7 @@ import { InstanceChainContext } from './common'; import { DecoratorConfig } from './DecoratorConfig'; -import { copyMetadata, bind } from '../utils'; +import { copyMetadata, bind, isMethodOrPropertyDecoratorArgs } from '../utils'; export type GenericDecorator = (...args: any[]) => LodashDecorator; @@ -15,6 +15,8 @@ export class InternalDecoratorFactory { const { applicator, optionalParams } = config; return (...args: any[]): LodashDecorator => { + let params = args; + const decorator = (target: Object, name: string, _descriptor?: PropertyDescriptor): PropertyDescriptor => { const descriptor = this._resolveDescriptor(target, name, _descriptor); const { value, get, set } = descriptor; @@ -23,18 +25,24 @@ export class InternalDecoratorFactory { // as we can't apply it correctly. if (!InstanceChainMap.has([ target, name ])) { if (isFunction(value)) { - descriptor.value = copyMetadata(applicator.apply({ config, target, value, args }), value); + descriptor.value = copyMetadata(applicator.apply({ config, target, value, args: params }), value); } else if (isFunction(get) && config.getter) { - descriptor.get = copyMetadata(applicator.apply({ config, target, value: get, args }), get); + descriptor.get = copyMetadata(applicator.apply({ config, target, value: get, args: params }), get); } else if (isFunction(set) && config.setter) { - descriptor.set = copyMetadata(applicator.apply({ config, target, value: set, args }), get); + descriptor.set = copyMetadata(applicator.apply({ config, target, value: set, args: params }), set); } } return descriptor; }; - return optionalParams && args.length >= 2 ? decorator(args[0], args[1], args[2]) as any : decorator; + if (optionalParams && isMethodOrPropertyDecoratorArgs(...args)) { + params = []; + + return decorator(args[0], args[1], args[2]) as any; + } + + return decorator; }; } @@ -42,6 +50,7 @@ export class InternalDecoratorFactory { const { applicator, bound, optionalParams } = config; return (...args: any[]): LodashDecorator => { + let params = args; const decorator = (target: Object, name: string, _descriptor?: PropertyDescriptor): PropertyDescriptor => { const descriptor = this._resolveDescriptor(target, name, _descriptor); const { value, writable, enumerable, configurable, get, set } = descriptor; @@ -63,7 +72,7 @@ export class InternalDecoratorFactory { } return copyMetadata( - applicator.apply({ args, target, instance, value: fn, config }), + applicator.apply({ args: params, target, instance, value: fn, config }), fn ); }); @@ -125,7 +134,7 @@ export class InternalDecoratorFactory { descriptor.get = function() { applyDecorator(this); - const descriptor = Object.getOwnPropertyDescriptor(this, name); + const descriptor = Object.getOwnPropertyDescriptor(this, name)!; if (descriptor.get) { return descriptor.get.call(this); @@ -137,7 +146,7 @@ export class InternalDecoratorFactory { descriptor.set = function(value) { applyDecorator(this); - const descriptor = Object.getOwnPropertyDescriptor(this, name); + const descriptor = Object.getOwnPropertyDescriptor(this, name)!; if (descriptor.set) { descriptor.set.call(this, value); @@ -149,7 +158,13 @@ export class InternalDecoratorFactory { return descriptor; }; - return optionalParams && args.length >= 2 ? decorator(args[0], args[1], args[2]) as any: decorator; + if (optionalParams && isMethodOrPropertyDecoratorArgs(...args)) { + params = []; + + return decorator(args[0], args[1], args[2]) as any; + } + + return decorator; }; } diff --git a/src/factory/common.ts b/src/factory/common.ts index f66a1f7..6b5487c 100644 --- a/src/factory/common.ts +++ b/src/factory/common.ts @@ -6,13 +6,16 @@ export type LodashMethodDecorator = MethodDecorator; export type LodashDecorator = MethodDecorator & PropertyDecorator; export type ResolvableFunction = string|Function; -export type TypedMethodDecorator = (() => LodashMethodDecorator) & LodashMethodDecorator; -export type TypedMethodDecorator1 = ((arg?: T) => LodashMethodDecorator) & LodashMethodDecorator; -export type TypedMethodDecorator2 = ((arg1?: T, arg2?: T2) => LodashMethodDecorator) & LodashMethodDecorator; -export type TypedMethodDecorator3 = ((arg1?: T, arg2?: T2, arg3?: T3) => LodashMethodDecorator) & LodashMethodDecorator; -export type TypedDecorator1 = ((arg?: T) => LodashDecorator) & LodashDecorator; -export type TypedDecorator2 = ((arg1?: T, arg2?: T2) => LodashDecorator) & LodashDecorator; -export type TypedDecorator3 = ((arg1?: T, arg2?: T2, arg3?: T3) => LodashDecorator) & LodashDecorator; +export type BiTypedMethodDecorator = (() => LodashMethodDecorator) & LodashMethodDecorator; +export type BiTypedMethodDecorator1 = ((arg?: T) => LodashMethodDecorator) & LodashMethodDecorator; +export type BiTypedMethodDecorator2 = ((arg1?: T, arg2?: T2) => LodashMethodDecorator) & LodashMethodDecorator; +export type BiTypedMethodDecorator3 = ((arg1?: T, arg2?: T2, arg3?: T3) => LodashMethodDecorator) & LodashMethodDecorator; +export type BiTypedMethodDecoratorN = ((...args: any[]) => LodashMethodDecorator) & LodashMethodDecorator; +export type BiTypedDecorator = (() => LodashDecorator) & LodashDecorator; +export type BiTypedDecorator1 = ((arg?: T) => LodashDecorator) & LodashDecorator; +export type BiTypedDecorator2 = ((arg1?: T, arg2?: T2) => LodashDecorator) & LodashDecorator; +export type BiTypedDecorator3 = ((arg1?: T, arg2?: T2, arg3?: T3) => LodashDecorator) & LodashDecorator; +export type BiTypedDecoratorN = ((...args: any[]) => LodashDecorator) & LodashDecorator; export interface InstanceChainContext { getter?: boolean; diff --git a/src/memoize.spec.ts b/src/memoize.spec.ts index cc5c82e..b0dd763 100644 --- a/src/memoize.spec.ts +++ b/src/memoize.spec.ts @@ -35,6 +35,37 @@ describe('memoize', () => { expect(_spy.callCount).to.equal(2); }); + it('should memoize the function (paramless)', () => { + const _spy = spy(); + + class MyClass { + @Memoize + fn(n: number): number { + _spy(n); + expect(this, 'context').to.equal(myClass); + + return n; + } + } + + const myClass = new MyClass(); + + myClass.fn(1); + myClass.fn(1); + myClass.fn(1); + myClass.fn(1); + myClass.fn(1); + + expect(myClass.fn(1)).to.equal(1); + expect(_spy.callCount).to.equal(1); + + myClass.fn(2); + myClass.fn(2); + + expect(myClass.fn(2)).to.equal(2); + expect(_spy.callCount).to.equal(2); + }); + describe('with function resolver', () => { it('should resolve the key', () => { const _spy = spy(); diff --git a/src/memoize.ts b/src/memoize.ts index a06c69a..76756f4 100644 --- a/src/memoize.ts +++ b/src/memoize.ts @@ -3,16 +3,12 @@ import memoize = require('lodash/memoize'); import { DecoratorConfig, DecoratorFactory, - LodashMethodDecorator, - ResolvableFunction + ResolvableFunction, + BiTypedMethodDecorator1 } from './factory'; import { MemoizeApplicator } from './applicators'; import { MemoizeConfig } from './shared'; -const decorator = DecoratorFactory.createInstanceDecorator( - new DecoratorConfig(memoize, new MemoizeApplicator()) -); - /** * Creates a function that memoizes the result of func. If resolver is provided, * it determines the cache key for storing the result based on the arguments provided to the memoized function. @@ -37,8 +33,8 @@ const decorator = DecoratorFactory.createInstanceDecorator( * } * } */ -export function Memoize(resolver?: ResolvableFunction | MemoizeConfig): LodashMethodDecorator { - return decorator(resolver); -} +export const Memoize = DecoratorFactory.createInstanceDecorator( + new DecoratorConfig(memoize, new MemoizeApplicator(), { optionalParams: true }) +) as BiTypedMethodDecorator1>; export { Memoize as memoize }; -export default decorator; +export default Memoize; diff --git a/src/memoizeAll.spec.ts b/src/memoizeAll.spec.ts index 99968c3..c882ce4 100644 --- a/src/memoizeAll.spec.ts +++ b/src/memoizeAll.spec.ts @@ -30,6 +30,32 @@ describe('memoizeAll', () => { expect(_spy.callCount).to.equal(1); }); + it('should memoize the function (paramless)', () => { + const _spy = spy(); + + class MyClass { + @MemoizeAll + fn(n: number): number { + _spy(n); + expect(this, 'context').to.equal(myClass); + + return n; + } + } + + const myClass = new MyClass(); + const myClass2 = new MyClass(); + + myClass.fn(1); + myClass.fn(1); + myClass.fn(1); + myClass2.fn(1); + myClass2.fn(1); + myClass2.fn(1); + + expect(_spy.callCount).to.equal(1); + }); + describe('with function resolver', () => { it('should resolve the key', () => { const _spy = spy(); diff --git a/src/memoizeAll.ts b/src/memoizeAll.ts index 2cf3a7d..cc6841c 100644 --- a/src/memoizeAll.ts +++ b/src/memoizeAll.ts @@ -1,19 +1,15 @@ import memoize = require('lodash/memoize'); -import { DecoratorConfig, DecoratorFactory, LodashMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedMethodDecorator1 } from './factory'; import { MemoizeApplicator } from './applicators'; import { MemoizeConfig } from './shared'; -const decorator = DecoratorFactory.createDecorator( - new DecoratorConfig(memoize, new MemoizeApplicator()) -); - /** * Memoizes a function on the prototype instead of the instance. All instances of the class use the same memoize cache. * @param {Function} [resolver] Optional resolver */ -export function MemoizeAll(resolver?: Function | MemoizeConfig): LodashMethodDecorator { - return decorator(resolver); -} +export const MemoizeAll = DecoratorFactory.createDecorator( + new DecoratorConfig(memoize, new MemoizeApplicator(), { optionalParams: true }) +) as BiTypedMethodDecorator1>; export { MemoizeAll as memoizeAll }; -export default decorator; +export default MemoizeAll; diff --git a/src/mixin.ts b/src/mixin.ts index 3a8b955..be0139c 100644 --- a/src/mixin.ts +++ b/src/mixin.ts @@ -19,11 +19,11 @@ import assign = require('lodash/assign'); * myClass.blorg(); // => 'blorg!' */ export function Mixin(...srcs: Object[]): ClassDecorator { - return (target: Function) => { + return ((target: Function) => { assign(target.prototype, ...srcs); return target; - }; + }) as any; } export { Mixin as mixin }; export default Mixin; diff --git a/src/negate.spec.ts b/src/negate.spec.ts index f262e2a..ae41116 100644 --- a/src/negate.spec.ts +++ b/src/negate.spec.ts @@ -16,6 +16,19 @@ describe('negate', () => { expect(myClass.fn()).to.be.false; }); + it('should inverse the result of the function (paramless)', () => { + class MyClass { + @Negate + fn() { + return true; + } + } + + const myClass = new MyClass(); + + expect(myClass.fn()).to.be.false; + }); + it('should inverse the result of the resolved function', () => { class MyClass { @Negate('fn') diff --git a/src/negate.ts b/src/negate.ts index cbca6a1..7f23d77 100644 --- a/src/negate.ts +++ b/src/negate.ts @@ -3,17 +3,31 @@ import negate = require('lodash/negate'); import { DecoratorConfig, DecoratorFactory, - LodashDecorator, - ResolvableFunction + ResolvableFunction, + BiTypedDecorator1 } from './factory'; import { PartialValueApplicator } from './applicators'; -const decorator = DecoratorFactory.createInstanceDecorator( - new DecoratorConfig(negate, new PartialValueApplicator(), { property: true }) -); - -export function Negate(fn?: ResolvableFunction): LodashDecorator { - return decorator(fn); -} +/** + * Negates a functions result or, when used on a property, creates a function that + * negates the result of a provided function. + * @param {ResolvableFunction} [fn] A resolvable function. + * @example + * class MyClass { + * @Negate('fn') + * fn2: () => boolean; + * + * fn(): boolean { + * return true; + * } + * } + * + * const myClass = new MyClass(); + * + * myClass.fn2(); //=> false + */ +export const Negate = DecoratorFactory.createInstanceDecorator( + new DecoratorConfig(negate, new PartialValueApplicator(), { property: true, optionalParams: true }) +) as BiTypedDecorator1; export { Negate as negate }; -export default decorator; +export default Negate; diff --git a/src/once.ts b/src/once.ts index 00f963b..41fe7a3 100644 --- a/src/once.ts +++ b/src/once.ts @@ -1,14 +1,28 @@ import once = require('lodash/once'); -import { DecoratorConfig, DecoratorFactory, LodashMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedDecorator } from './factory'; import { PreValueApplicator } from './applicators'; -const decorator = DecoratorFactory.createInstanceDecorator( - new DecoratorConfig(once, new PreValueApplicator(), { setter: true }) -); - -export function Once(): LodashMethodDecorator { - return decorator(); -} +/** + * Creates a function that is restricted to invoking func once. Repeat calls to the function return the value of the first invocation. + * @example + * class MyClass { + * value: number = 0; + * + * @Once() + * fn(): number { + * return ++this.value; + * } + * } + * + * const myClass = new MyClass(); + * + * myClass.fn(); //=> 1 + * myClass.fn(); //=> 1 + * myClass.fn(); //=> 1 + */ +export const Once = DecoratorFactory.createInstanceDecorator( + new DecoratorConfig(once, new PreValueApplicator(), { setter: true, optionalParams: true }) +) as BiTypedDecorator; export { Once as once }; -export default decorator; +export default Once; diff --git a/src/onceAll.ts b/src/onceAll.ts index beeb375..35915d5 100644 --- a/src/onceAll.ts +++ b/src/onceAll.ts @@ -1,14 +1,29 @@ import once = require('lodash/once'); -import { DecoratorConfig, DecoratorFactory, LodashMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedDecorator } from './factory'; import { PreValueApplicator } from './applicators'; -const decorator = DecoratorFactory.createDecorator( - new DecoratorConfig(once, new PreValueApplicator(), { setter: true }) -); - -export function OnceAll(): LodashMethodDecorator { - return decorator(); -} +/** + * Creates a function that is restricted to invoking func once. Repeat calls to the function return the value of the first invocation. + * This is shared across all instances of the class. + * @example + * const value = 0; + * + * class MyClass { + * @Once() + * fn(): number { + * return ++value; + * } + * } + * + * const myClass = new MyClass(); + * const myClass2 = new MyClass(); + * + * myClass.fn(); //=> 1 + * myClass2.fn(); //=> 1 + */ +export const OnceAll = DecoratorFactory.createDecorator( + new DecoratorConfig(once, new PreValueApplicator(), { setter: true, optionalParams: true }) +) as BiTypedDecorator; export { OnceAll as onceAll }; -export default decorator; +export default OnceAll; diff --git a/src/overArgs.ts b/src/overArgs.ts index ab9b177..1de688c 100644 --- a/src/overArgs.ts +++ b/src/overArgs.ts @@ -7,6 +7,24 @@ const decorator = DecoratorFactory.createDecorator( new DecoratorConfig(overArgs, new PreValueApplicator(), { setter: true }) ); +/** + * Creates a function that invokes func with its arguments transformed. + * @export + * @param {...Function[]} transforms + * @returns {LodashMethodDecorator} + * @example + * class MyClass { + * @OverArgs(_.castArray) + * fn(list) { + * return list; + * } + * } + * + * const myClass = new MyClass(); + * + * myClass.fn([ 1 ]); //=> [ 1 ]; + * myClass.fn(1); //=> [ 1 ]; + */ export function OverArgs(...transforms: Function[]): LodashMethodDecorator { return decorator(...transforms); } diff --git a/src/partial.spec.ts b/src/partial.spec.ts index 7efb3c5..77c3650 100644 --- a/src/partial.spec.ts +++ b/src/partial.spec.ts @@ -5,9 +5,9 @@ import { Partial } from './partial'; describe('partial', () => { it('should create a partially applied function', () => { class MyClass { - lastName: string = 'Sojka'; + lastName: string = 'Schmo'; - @Partial('fn', 'Avry') + @Partial('fn', 'Joe') fn2: () => string; fn(name: string): string { @@ -18,6 +18,6 @@ describe('partial', () => { const myClass = new MyClass(); expect(myClass.fn2).to.be.a('function'); - expect(myClass.fn2()).to.equal('Avry Sojka'); + expect(myClass.fn2()).to.equal('Joe Schmo'); }); -}); \ No newline at end of file +}); diff --git a/src/partial.ts b/src/partial.ts index 4dff4d1..d34b1a2 100644 --- a/src/partial.ts +++ b/src/partial.ts @@ -7,6 +7,27 @@ const decorator = DecoratorFactory.createInstanceDecorator( new DecoratorConfig(partial, new PartialApplicator(), { property: true, method: false }) ); +/** + * Partially applies arguments to a function. + * @export + * @param {...any[]} partials + * @returns {PropertyDecorator} + * @example + * class MyClass { + * lastName: string = 'Schmo'; + * + * @Partial('fn', 'Joe') + * fn2: () => string; + * + * fn(name: string): string { + * return `${name} ${this.lastName}`; + * } + * } + * + * const myClass = new MyClass(); + * + * myClass.fn2(); //=> 'Joe Schmo' + */ export function Partial(...partials: any[]): PropertyDecorator { return decorator(...partials); } diff --git a/src/rest.spec.ts b/src/rest.spec.ts index d0f316b..df3df7b 100644 --- a/src/rest.spec.ts +++ b/src/rest.spec.ts @@ -17,4 +17,18 @@ describe('rest', () => { myClass.fn(1, 2, 3, 4); }); -}); \ No newline at end of file + + it('should change the order of arguments (paramless)', () => { + class MyClass { + @Rest + fn(...args: any[]) { + expect(args.length).to.equal(1); + expect(args[0]).to.eql([ 1, 2, 3, 4 ]); + } + } + + const myClass = new MyClass(); + + myClass.fn(1, 2, 3, 4); + }); +}); diff --git a/src/rest.ts b/src/rest.ts index 52de241..54495ff 100644 --- a/src/rest.ts +++ b/src/rest.ts @@ -1,14 +1,10 @@ import rest = require('lodash/rest'); -import { DecoratorConfig, DecoratorFactory, LodashMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedMethodDecorator1 } from './factory'; import { PreValueApplicator } from './applicators'; -const decorator = DecoratorFactory.createDecorator( - new DecoratorConfig(rest, new PreValueApplicator()) -); - -export function Rest(start?: number): LodashMethodDecorator { - return decorator(start); -} +export const Rest = DecoratorFactory.createDecorator( + new DecoratorConfig(rest, new PreValueApplicator(), { optionalParams: true }) +) as BiTypedMethodDecorator1; export { Rest as rest }; -export default decorator; +export default Rest; diff --git a/src/spread.spec.ts b/src/spread.spec.ts index 287afd9..f7a4dc9 100644 --- a/src/spread.spec.ts +++ b/src/spread.spec.ts @@ -16,4 +16,18 @@ describe('spread', () => { myClass.fn([ 1, 2, 3, 4 ]); }); -}); \ No newline at end of file + + it('should spread the arguments (paramless)', () => { + class MyClass { + @Spread + fn(...args: any[]) { + expect(args.length).to.equal(4); + expect(args).to.eql([ 1, 2, 3, 4 ]); + } + } + + const myClass = new MyClass(); + + myClass.fn([ 1, 2, 3, 4 ]); + }); +}); diff --git a/src/spread.ts b/src/spread.ts index 8c92d77..889e314 100644 --- a/src/spread.ts +++ b/src/spread.ts @@ -1,14 +1,10 @@ import spread = require('lodash/spread'); -import { DecoratorConfig, DecoratorFactory, LodashMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedMethodDecorator1 } from './factory'; import { PreValueApplicator } from './applicators'; -const decorator = DecoratorFactory.createDecorator( - new DecoratorConfig(spread, new PreValueApplicator()) -); - -export function Spread(start?: number): LodashMethodDecorator { - return decorator(start); -} +export const Spread = DecoratorFactory.createDecorator( + new DecoratorConfig(spread, new PreValueApplicator(), { optionalParams: true }) +) as BiTypedMethodDecorator1; export { Spread as spread }; -export default decorator; +export default Spread; diff --git a/src/tap.spec.ts b/src/tap.spec.ts index 73f61e1..3815cba 100644 --- a/src/tap.spec.ts +++ b/src/tap.spec.ts @@ -15,4 +15,17 @@ describe('tap', () => { expect(myClass.fn(50)).to.equal(50); }); -}); \ No newline at end of file + + it('should return the first argument (paramless)', () => { + class MyClass { + @Tap + fn(n: number): any { + return 10; + } + } + + const myClass = new MyClass(); + + expect(myClass.fn(50)).to.equal(50); + }); +}); diff --git a/src/tap.ts b/src/tap.ts index 18864eb..860b294 100644 --- a/src/tap.ts +++ b/src/tap.ts @@ -1,17 +1,13 @@ -import { DecoratorConfig, DecoratorFactory, LodashMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedMethodDecorator } from './factory'; import { PreValueApplicator } from './applicators'; import { returnAtIndex } from './utils'; -const decorator = DecoratorFactory.createDecorator( - new DecoratorConfig((fn: Function) => returnAtIndex(fn, 0), new PreValueApplicator()) -); - /** * Returns the first argument from the function regardless of * the decorated functions return value. */ -export function Tap(): LodashMethodDecorator { - return decorator(); -} +export const Tap = DecoratorFactory.createDecorator( + new DecoratorConfig((fn: Function) => returnAtIndex(fn, 0), new PreValueApplicator(), { optionalParams: true }) +) as BiTypedMethodDecorator; export { Tap as tap }; -export default decorator; +export default Tap; diff --git a/src/throttle.spec.ts b/src/throttle.spec.ts index 4540734..569f726 100644 --- a/src/throttle.spec.ts +++ b/src/throttle.spec.ts @@ -28,6 +28,27 @@ describe('throttle', () => { }, 20); }); + it('should throttle the method (paramless)', (done) => { + const _spy = spy(); + + class MyClass { + @Throttle + fn(n: number) { + _spy(); + } + } + + const myClass = new MyClass(); + + myClass.fn(1); + myClass.fn(2); + + setTimeout(() => { + expect(_spy.callCount).to.equal(2); + done(); + }, 20); + }); + it('should debounce the property setter', (done) => { class MyClass { private _value: number = 100; diff --git a/src/throttle.ts b/src/throttle.ts index 229e68b..cbc253f 100644 --- a/src/throttle.ts +++ b/src/throttle.ts @@ -1,34 +1,22 @@ import throttle = require('lodash/throttle'); -import { DecoratorConfig, DecoratorFactory, LodashMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedDecorator2 } from './factory'; import { PreValueApplicator } from './applicators'; import { ThrottleOptions } from './shared'; -const decorator = DecoratorFactory.createInstanceDecorator( - new DecoratorConfig(throttle, new PreValueApplicator(), { setter: true, getter: true }) -); +export const Throttle = DecoratorFactory.createInstanceDecorator( + new DecoratorConfig(throttle, new PreValueApplicator(), { setter: true, getter: true, optionalParams: true }) +) as BiTypedDecorator2; -const decoratorGetter = DecoratorFactory.createInstanceDecorator( - new DecoratorConfig(throttle, new PreValueApplicator(), { getter: true }) -); +export const ThrottleGetter = DecoratorFactory.createInstanceDecorator( + new DecoratorConfig(throttle, new PreValueApplicator(), { getter: true, optionalParams: true }) +) as BiTypedDecorator2; -const decoratorSetter = DecoratorFactory.createInstanceDecorator( - new DecoratorConfig(throttle, new PreValueApplicator(), { setter: true }) -); - -export function Throttle(wait?: number, options?: ThrottleOptions): LodashMethodDecorator { - return decorator(wait, options); -} - -export function ThrottleGetter(wait?: number, options?: ThrottleOptions): LodashMethodDecorator { - return decoratorGetter(wait, options); -} - -export function ThrottleSetter(wait?: number, options?: ThrottleOptions): LodashMethodDecorator { - return decoratorSetter(wait, options); -} +export const ThrottleSetter = DecoratorFactory.createInstanceDecorator( + new DecoratorConfig(throttle, new PreValueApplicator(), { setter: true, optionalParams: true }) +) as BiTypedDecorator2; export { Throttle as throttle }; export { ThrottleGetter as throttleGetter }; export { ThrottleSetter as throttleSetter }; -export default decorator; +export default Throttle; diff --git a/src/unary.spec.ts b/src/unary.spec.ts index ba2020d..e03d812 100644 --- a/src/unary.spec.ts +++ b/src/unary.spec.ts @@ -15,4 +15,17 @@ describe('unary', () => { myClass.fn(1, 2, 3, 4); }); -}); \ No newline at end of file + + it('should only invoke with one argument (paramless)', () => { + class MyClass { + @Unary + fn(...args: any[]): any { + expect(args.length).to.equal(1); + } + } + + const myClass = new MyClass(); + + myClass.fn(1, 2, 3, 4); + }); +}); diff --git a/src/unary.ts b/src/unary.ts index 4ee9c7f..9212a8f 100644 --- a/src/unary.ts +++ b/src/unary.ts @@ -1,14 +1,10 @@ import unary = require('lodash/unary'); -import { DecoratorConfig, DecoratorFactory, LodashMethodDecorator } from './factory'; +import { DecoratorConfig, DecoratorFactory, BiTypedMethodDecorator } from './factory'; import { PreValueApplicator } from './applicators'; -const decorator = DecoratorFactory.createDecorator( - new DecoratorConfig(unary, new PreValueApplicator()) -); - -export function Unary(): LodashMethodDecorator { - return decorator(); -} +export const Unary = DecoratorFactory.createDecorator( + new DecoratorConfig(unary, new PreValueApplicator(), { optionalParams: true }) +) as BiTypedMethodDecorator; export { Unary as unary }; -export default decorator; +export default Unary; diff --git a/src/utils.ts b/src/utils.ts index 985f397..e82baba 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,3 +6,4 @@ export * from './utils/copyMetaData'; export * from './utils/bind'; export * from './utils/wrapConstructor'; export * from './utils/assignAll'; +export * from './utils/isDecoratorArgs'; diff --git a/src/utils/isDecoratorArgs.spec.ts b/src/utils/isDecoratorArgs.spec.ts new file mode 100644 index 0000000..aa73d11 --- /dev/null +++ b/src/utils/isDecoratorArgs.spec.ts @@ -0,0 +1,56 @@ +import { expect } from 'chai'; + +import { isMethodOrPropertyDecoratorArgs } from './isDecoratorArgs'; + +describe('when determining if args are from a decorator', () => { + describe('and when the arguments are gte to 2', () => { + describe('and when the first argument is an object', () => { + describe('and when the second argument is a string', () => { + describe('and when the first argument has a constructor property', () => { + describe('and when the constructors prototype is the first argument', () => { + it('should return true', () => { + class Test {} + + expect(isMethodOrPropertyDecoratorArgs(Test.prototype, 'test')).to.be.true; + }); + }); + + describe('and when the constructors prototype is not the first argument', () => { + it('should return false', () => { + class Test {} + + expect(isMethodOrPropertyDecoratorArgs({ constructor: Test }, 'test')).to.be.false; + }); + }); + }); + + describe('and when the first argument does not have a constructor property', () => { + it('should return false', () => { + expect(isMethodOrPropertyDecoratorArgs({}, 'test')).to.be.false; + }); + }); + }); + + describe('and when the second argument is not a string', () => { + it('should return false', () => { + expect(isMethodOrPropertyDecoratorArgs({}, 123)).to.be.false; + }); + }); + }); + + describe('and when the first argument is not an object', () => { + it('should return false', () => { + expect(isMethodOrPropertyDecoratorArgs(true, 123)).to.be.false; + }); + }); + }); + + describe('and when the arguments are gte to 2', () => { + it('should return false', () => { + class Test {} + + expect(isMethodOrPropertyDecoratorArgs(Test.prototype)).to.be.false; + }); + }); +}); + diff --git a/src/utils/isDecoratorArgs.ts b/src/utils/isDecoratorArgs.ts new file mode 100644 index 0000000..c60ec3e --- /dev/null +++ b/src/utils/isDecoratorArgs.ts @@ -0,0 +1,11 @@ +import isObject = require('lodash/isObject'); +import isFunction = require('lodash/isFunction'); +import isString = require('lodash/isString'); + +export function isMethodOrPropertyDecoratorArgs(...args: any[]): boolean { + return args.length >= 2 + && isObject(args[0]) + && isString(args[1]) + && isFunction(args[0].constructor) + && args[0].constructor.prototype === args[0]; +}