From b9ff3db9b0e023aef36961d0127a266ae2b621a6 Mon Sep 17 00:00:00 2001 From: Julien Fontanet Date: Thu, 10 Mar 2022 11:51:57 +0100 Subject: [PATCH] feat(decorate-with): decorateClass() (#6136) Generalization of `decorateMethodsWith` which also works for accessors. The suffix `With` is not part of the name because it's not fluent (unlike for `@decorateWith(decorator)`). `decorateMethodsWith` is now a deprecated alias for this new implementation. --- @vates/decorate-with/.USAGE.md | 40 ++++++++++++------ @vates/decorate-with/README.md | 40 ++++++++++++------ @vates/decorate-with/index.js | 23 ++++++++--- @vates/decorate-with/index.spec.js | 66 ++++++++++++++++++++++++++---- CHANGELOG.unreleased.md | 1 + 5 files changed, 132 insertions(+), 38 deletions(-) diff --git a/@vates/decorate-with/.USAGE.md b/@vates/decorate-with/.USAGE.md index 583b81d0c..ea2b9aa6c 100644 --- a/@vates/decorate-with/.USAGE.md +++ b/@vates/decorate-with/.USAGE.md @@ -13,15 +13,19 @@ class Foo { } ``` -### `decorateMethodsWith(class, map)` +### `decorateClass(class, map)` -Decorates a number of methods directly, without using the decorator syntax: +Decorates a number of accessors and methods directly, without using the decorator syntax: ```js -import { decorateMethodsWith } from '@vates/decorate-with' +import { decorateClass } from '@vates/decorate-with' class Foo { - bar() { + get bar() { + // body + } + + set bar(value) { // body } @@ -30,22 +34,28 @@ class Foo { } } -decorateMethodsWith(Foo, { - // without arguments - bar: lodash.curry, +decorateClass(Foo, { + // getter and/or setter + bar: { + // without arguments + get: lodash.memoize, - // with arguments - baz: [lodash.debounce, 150], + // with arguments + set: [lodash.debounce, 150], + }, + + // method (with or without arguments) + baz: lodash.curry, }) ``` The decorated class is returned, so you can export it directly. -To apply multiple transforms to a method, you can either call `decorateMethodsWith` multiple times or use [`@vates/compose`](https://www.npmjs.com/package/@vates/compose): +To apply multiple transforms to an accessor/method, you can either call `decorateClass` multiple times or use [`@vates/compose`](https://www.npmjs.com/package/@vates/compose): ```js -decorateMethodsWith(Foo, { - bar: compose([ +decorateClass(Foo, { + baz: compose([ [lodash.debounce, 150] lodash.curry, ]) @@ -69,4 +79,8 @@ class Foo { } ``` -Because it's a normal function, it can also be used with `decorateMethodsWith`, with `compose` or even by itself. +Because it's a normal function, it can also be used with `decorateClass`, with `compose` or even by itself. + +### `decorateMethodsWith(class, map)` + +> Deprecated alias for [`decorateClass(class, map)`](#decorateclassclass-map). diff --git a/@vates/decorate-with/README.md b/@vates/decorate-with/README.md index 0a52f6783..13e2bc1b3 100644 --- a/@vates/decorate-with/README.md +++ b/@vates/decorate-with/README.md @@ -31,15 +31,19 @@ class Foo { } ``` -### `decorateMethodsWith(class, map)` +### `decorateClass(class, map)` -Decorates a number of methods directly, without using the decorator syntax: +Decorates a number of accessors and methods directly, without using the decorator syntax: ```js -import { decorateMethodsWith } from '@vates/decorate-with' +import { decorateClass } from '@vates/decorate-with' class Foo { - bar() { + get bar() { + // body + } + + set bar(value) { // body } @@ -48,22 +52,28 @@ class Foo { } } -decorateMethodsWith(Foo, { - // without arguments - bar: lodash.curry, +decorateClass(Foo, { + // getter and/or setter + bar: { + // without arguments + get: lodash.memoize, - // with arguments - baz: [lodash.debounce, 150], + // with arguments + set: [lodash.debounce, 150], + }, + + // method (with or without arguments) + baz: lodash.curry, }) ``` The decorated class is returned, so you can export it directly. -To apply multiple transforms to a method, you can either call `decorateMethodsWith` multiple times or use [`@vates/compose`](https://www.npmjs.com/package/@vates/compose): +To apply multiple transforms to an accessor/method, you can either call `decorateClass` multiple times or use [`@vates/compose`](https://www.npmjs.com/package/@vates/compose): ```js -decorateMethodsWith(Foo, { - bar: compose([ +decorateClass(Foo, { + baz: compose([ [lodash.debounce, 150] lodash.curry, ]) @@ -87,7 +97,11 @@ class Foo { } ``` -Because it's a normal function, it can also be used with `decorateMethodsWith`, with `compose` or even by itself. +Because it's a normal function, it can also be used with `decorateClass`, with `compose` or even by itself. + +### `decorateMethodsWith(class, map)` + +> Deprecated alias for [`decorateClass(class, map)`](#decorateclassclass-map). ## Contributions diff --git a/@vates/decorate-with/index.js b/@vates/decorate-with/index.js index 2b7d1153b..3874d43d3 100644 --- a/@vates/decorate-with/index.js +++ b/@vates/decorate-with/index.js @@ -9,14 +9,27 @@ exports.decorateWith = function decorateWith(fn, ...args) { const { getOwnPropertyDescriptor, defineProperty } = Object -exports.decorateMethodsWith = function decorateMethodsWith(klass, map) { +function applyDecorator(decorator, value) { + return typeof decorator === 'function' ? decorator(value) : decorator[0](value, ...decorator.slice(1)) +} + +exports.decorateClass = exports.decorateMethodsWith = function decorateClass(klass, map) { const { prototype } = klass for (const name of Object.keys(map)) { - const descriptor = getOwnPropertyDescriptor(prototype, name) - const { value } = descriptor - const decorator = map[name] - descriptor.value = typeof decorator === 'function' ? decorator(value) : decorator[0](value, ...decorator.slice(1)) + const descriptor = getOwnPropertyDescriptor(prototype, name) + if (typeof decorator === 'function' || Array.isArray(decorator)) { + descriptor.value = applyDecorator(decorator, descriptor.value) + } else { + const { get, set } = decorator + if (get !== undefined) { + descriptor.get = applyDecorator(get, descriptor.get) + } + if (set !== undefined) { + descriptor.set = applyDecorator(set, descriptor.set) + } + } + defineProperty(prototype, name, descriptor) } return klass diff --git a/@vates/decorate-with/index.spec.js b/@vates/decorate-with/index.spec.js index a048dc270..3ab1968b2 100644 --- a/@vates/decorate-with/index.spec.js +++ b/@vates/decorate-with/index.spec.js @@ -3,7 +3,9 @@ const assert = require('assert') const { describe, it } = require('tap').mocha -const { decorateWith, decorateMethodsWith, perInstance } = require('./') +const { decorateClass, decorateWith, decorateMethodsWith, perInstance } = require('./') + +const identity = _ => _ describe('decorateWith', () => { it('works', () => { @@ -31,11 +33,14 @@ describe('decorateWith', () => { }) }) -describe('decorateMethodsWith', () => { +describe('decorateClass', () => { it('works', () => { class C { foo() {} bar() {} + get baz() {} + // eslint-disable-next-line accessor-pairs + set qux(_) {} } const expectedArgs = [Math.random(), Math.random()] @@ -45,27 +50,74 @@ describe('decorateMethodsWith', () => { const newFoo = () => {} const newBar = () => {} + const newGetBaz = () => {} + const newSetQux = _ => {} - decorateMethodsWith(C, { - foo(method) { + decorateClass(C, { + foo(fn) { assert.strictEqual(arguments.length, 1) - assert.strictEqual(method, P.foo) + assert.strictEqual(fn, P.foo) return newFoo }, bar: [ - function (method, ...args) { - assert.strictEqual(method, P.bar) + function (fn, ...args) { + assert.strictEqual(fn, P.bar) assert.deepStrictEqual(args, expectedArgs) return newBar }, ...expectedArgs, ], + baz: { + get(fn) { + assert.strictEqual(arguments.length, 1) + assert.strictEqual(fn, descriptors.baz.get) + return newGetBaz + }, + }, + qux: { + set: [ + function (fn, ...args) { + assert.strictEqual(fn, descriptors.qux.set) + assert.deepStrictEqual(args, expectedArgs) + return newSetQux + }, + ...expectedArgs, + ], + }, }) const newDescriptors = Object.getOwnPropertyDescriptors(P) assert.deepStrictEqual(newDescriptors.foo, { ...descriptors.foo, value: newFoo }) assert.deepStrictEqual(newDescriptors.bar, { ...descriptors.bar, value: newBar }) + assert.deepStrictEqual(newDescriptors.baz, { ...descriptors.baz, get: newGetBaz }) + assert.deepStrictEqual(newDescriptors.qux, { ...descriptors.qux, set: newSetQux }) }) + + it('throws if using an accessor decorator for a method', function () { + assert.throws(() => + decorateClass( + class { + foo() {} + }, + { foo: { get: identity, set: identity } } + ) + ) + }) + + it('throws if using a method decorator for an accessor', function () { + assert.throws(() => + decorateClass( + class { + get foo() {} + }, + { foo: identity } + ) + ) + }) +}) + +it('decorateMethodsWith is an alias of decorateClass', function () { + assert.strictEqual(decorateMethodsWith, decorateClass) }) describe('perInstance', () => { diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 76a22bed4..df5dd5c13 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -32,6 +32,7 @@ > > In case of conflict, the highest (lowest in previous list) `$version` wins. +- @vates/decorate-with major - xen-api major - @xen-orchestra/xapi minor - @xen-orchestra/fs major