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.
This commit is contained in:
Julien Fontanet 2022-03-10 11:51:57 +01:00 committed by GitHub
parent 8ce1b4bf71
commit b9ff3db9b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 132 additions and 38 deletions

View File

@ -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, {
decorateClass(Foo, {
// getter and/or setter
bar: {
// without arguments
bar: lodash.curry,
get: lodash.memoize,
// with arguments
baz: [lodash.debounce, 150],
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).

View File

@ -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, {
decorateClass(Foo, {
// getter and/or setter
bar: {
// without arguments
bar: lodash.curry,
get: lodash.memoize,
// with arguments
baz: [lodash.debounce, 150],
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

View File

@ -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

View File

@ -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', () => {

View File

@ -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