Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds MultiMethodDecoratorFactory #4417

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions packages/metadata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,92 @@ class MyController {
}
```

### To create a decorator that can be used multiple times on a single method

Instead of a single immutable object to be merged, the
`MethodMultiDecoratorFactory` reduced parameters into a flat array of items.
When fetching the metadata later, you will receive it as an array.

```ts
import {MethodMultiDecoratorFactory} from '@loopback/metadata';

function myMultiMethodDecorator(spec: object): MethodDecorator {
return MethodMultiDecoratorFactory.createDecorator<object>(
'metadata-key-for-my-method-multi-decorator',
spec,
);
}
```

Now, you can use it multiple times on a method:

```ts
class MyController {
@myMultiMethodDecorator({x: 1})
@myMultiMethodDecorator({y: 2})
@myMultiMethodDecorator({z: 3})
public point() {}
}

class MyOtherController {
@myMultiMethodDecorator([{x: 1}, {y: 2}, {z: 3}])
public point() {}
}
```

And when you access this data:

```ts
const arrayOfSpecs = MetadataInspector.getMethodMetadata<object>(
'metadata-key-for-my-method-multi-decorator',
constructor.prototype,
op,
);

// [{x:1}, {y:2}, {z: 3}]
```

Note that the order of items is **not** guaranteed and should be treated as
unsorted.
mschnee marked this conversation as resolved.
Show resolved Hide resolved

You can also create a decorator that takes an object that can contain an array:

```ts
interface Point {
x?: number;
y?: number;
z?: number;
}
interface GeometryMetadata {
points: Point[];
}
function geometry(points: Point | Point[]): MethodDecorator {
mschnee marked this conversation as resolved.
Show resolved Hide resolved
return MethodMultiDecoratorFactory.createDecorator<GeometryMetadata>(
'metadata-key-for-my-method-multi-decorator',
points: Array.isArray(points) ? points : [points],
);
}

class MyGeoController {
@geometry({x: 1})
@geometry([{x:2}, {y:3}])
@geometry({z: 5})
public abstract() {}
}

const arrayOfSpecs = MetadataInspector.getMethodMetadata<GeometryMetadata>(
'metadata-key-for-my-method-multi-decorator',
constructor.prototype,
op,
);

// [
// { points: [{x: 1}]},
// { points: [{x:2}, {y:3}]},
// { points: [{z: 5}]},
// ]
```

### To create a property decorator

```ts
Expand Down
188 changes: 188 additions & 0 deletions packages/metadata/src/__tests__/unit/decorator-factory.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ClassDecoratorFactory,
DecoratorFactory,
MethodDecoratorFactory,
MethodMultiDecoratorFactory,
MethodParameterDecoratorFactory,
ParameterDecoratorFactory,
PropertyDecoratorFactory,
Expand Down Expand Up @@ -523,6 +524,193 @@ describe('MethodDecoratorFactory for static methods', () => {
});
});

describe('MethodMultiDecoratorFactory', () => {
function methodMultiArrayDecorator(spec: object | object[]): MethodDecorator {
if (Array.isArray(spec)) {
return MethodMultiDecoratorFactory.createDecorator('test', spec);
} else {
return MethodMultiDecoratorFactory.createDecorator('test', [spec]);
}
}

function methodMultiDecorator(spec: object): MethodDecorator {
return MethodMultiDecoratorFactory.createDecorator('test', spec);
}

class BaseController {
@methodMultiArrayDecorator({x: 1})
public myMethod() {}

@methodMultiArrayDecorator({foo: 1})
@methodMultiArrayDecorator({foo: 2})
@methodMultiArrayDecorator([{foo: 3}, {foo: 4}])
public multiMethod() {}

@methodMultiDecorator({a: 'a'})
@methodMultiDecorator({b: 'b'})
public checkDecorator() {}
}

class SubController extends BaseController {
@methodMultiArrayDecorator({y: 2})
public myMethod() {}

@methodMultiArrayDecorator({bar: 1})
@methodMultiArrayDecorator([{bar: 2}, {bar: 3}])
public multiMethod() {}
}

describe('single-decorator methods', () => {
it('applies metadata to a method', () => {
const meta = Reflector.getOwnMetadata('test', BaseController.prototype);
expect(meta.myMethod).to.eql([{x: 1}]);
});

it('merges with base method metadata', () => {
const meta = Reflector.getOwnMetadata('test', SubController.prototype);
expect(meta.myMethod).to.eql([{x: 1}, {y: 2}]);
});

it('does not mutate base method metadata', () => {
const meta = Reflector.getOwnMetadata('test', BaseController.prototype);
expect(meta.myMethod).to.eql([{x: 1}]);
});
});

describe('multi-decorator methods', () => {
it('applies to non-array decorator creation', () => {
const meta = Reflector.getOwnMetadata('test', BaseController.prototype);
expect(meta.checkDecorator).to.containDeep([{a: 'a'}, {b: 'b'}]);
});

it('applies metadata to a method', () => {
const meta = Reflector.getOwnMetadata('test', BaseController.prototype);
expect(meta.multiMethod).to.containDeep([
{foo: 4},
{foo: 3},
{foo: 2},
{foo: 1},
]);
});

it('merges with base method metadata', () => {
const meta = Reflector.getOwnMetadata('test', SubController.prototype);
expect(meta.multiMethod).to.containDeep([
{foo: 4},
{foo: 3},
{foo: 2},
{foo: 1},
{bar: 3},
{bar: 2},
{bar: 1},
]);
});

it('does not mutate base method metadata', () => {
const meta = Reflector.getOwnMetadata('test', BaseController.prototype);
expect(meta.multiMethod).to.containDeep([
{foo: 1},
{foo: 2},
{foo: 3},
{foo: 4},
]);
});
});
});
describe('MethodMultiDecoratorFactory for static methods', () => {
function methodMultiArrayDecorator(spec: object | object[]): MethodDecorator {
if (Array.isArray(spec)) {
return MethodMultiDecoratorFactory.createDecorator('test', spec);
} else {
return MethodMultiDecoratorFactory.createDecorator('test', [spec]);
}
}

function methodMultiDecorator(spec: object): MethodDecorator {
return MethodMultiDecoratorFactory.createDecorator('test', spec);
}

class BaseController {
@methodMultiArrayDecorator({x: 1})
static myMethod() {}

@methodMultiArrayDecorator({foo: 1})
@methodMultiArrayDecorator({foo: 2})
@methodMultiArrayDecorator([{foo: 3}, {foo: 4}])
static multiMethod() {}

@methodMultiDecorator({a: 'a'})
@methodMultiDecorator({b: 'b'})
static checkDecorator() {}
}

class SubController extends BaseController {
@methodMultiArrayDecorator({y: 2})
static myMethod() {}

@methodMultiArrayDecorator({bar: 1})
@methodMultiArrayDecorator([{bar: 2}, {bar: 3}])
static multiMethod() {}
}

describe('single-decorator methods', () => {
it('applies metadata to a method', () => {
const meta = Reflector.getOwnMetadata('test', BaseController);
expect(meta.myMethod).to.eql([{x: 1}]);
});

it('merges with base method metadata', () => {
const meta = Reflector.getOwnMetadata('test', SubController);
expect(meta.myMethod).to.eql([{x: 1}, {y: 2}]);
});

it('does not mutate base method metadata', () => {
const meta = Reflector.getOwnMetadata('test', BaseController);
expect(meta.myMethod).to.eql([{x: 1}]);
});
});

describe('multi-decorator methods', () => {
it('applies metadata to a method', () => {
const meta = Reflector.getOwnMetadata('test', BaseController);
expect(meta.multiMethod).to.containDeep([
{foo: 4},
{foo: 3},
{foo: 2},
{foo: 1},
]);
});

it('applies to non-array decorator creation', () => {
const meta = Reflector.getOwnMetadata('test', BaseController);
expect(meta.checkDecorator).to.containDeep([{a: 'a'}, {b: 'b'}]);
});

it('merges with base method metadata', () => {
const meta = Reflector.getOwnMetadata('test', SubController);
expect(meta.multiMethod).to.containDeep([
{foo: 4},
{foo: 3},
{foo: 2},
{foo: 1},
{bar: 3},
{bar: 2},
{bar: 1},
]);
});

it('does not mutate base method metadata', () => {
const meta = Reflector.getOwnMetadata('test', BaseController);
expect(meta.multiMethod).to.containDeep([
{foo: 1},
{foo: 2},
{foo: 3},
{foo: 4},
]);
});
});
});

describe('ParameterDecoratorFactory', () => {
/**
* Define `@parameterDecorator(spec)`
Expand Down
75 changes: 75 additions & 0 deletions packages/metadata/src/decorator-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,3 +789,78 @@ export class MethodParameterDecoratorFactory<T> extends DecoratorFactory<
);
}
}

/**
* Factory for an append-array of method-level decorators
* The @response metadata for a method is an array.
* Each item in the array should be a single value, containing
* a response code and a single spec or Model. This should allow:
* ```ts
* @response(200, MyFirstModel)
* @response(403, [NotAuthorizedReasonOne, NotAuthorizedReasonTwo])
* @response(404, NotFoundOne)
* @response(404, NotFoundTwo)
* @response(409, {schema: {}})
* public async myMethod() {}
* ```
*
* In the case that a ResponseObject is passed, it becomes the
* default for description/content, and if possible, further Models are
* incorporated as a `oneOf: []` array.
*
* In the case that a ReferenceObject is passed, it and it alone is used, since
* references can be external and we cannot `oneOf` their content.
*
* The factory creates and updates an array of items T[], and the getter
* provides the values as that array.
*/
export class MethodMultiDecoratorFactory<T> extends MethodDecoratorFactory<
T[]
> {
protected mergeWithInherited(
inheritedMetadata: MetadataMap<T[]>,
target: Object,
methodName?: string,
) {
inheritedMetadata = inheritedMetadata || {};

inheritedMetadata[methodName!] = this._mergeArray(
inheritedMetadata[methodName!],
this.withTarget(this.spec, target),
);

return inheritedMetadata;
}

protected mergeWithOwn(
ownMetadata: MetadataMap<T[]>,
target: Object,
methodName?: string,
methodDescriptor?: TypedPropertyDescriptor<any> | number,
) {
ownMetadata = ownMetadata || {};
ownMetadata[methodName!] = this._mergeArray(
ownMetadata[methodName!],
this.withTarget(this.spec, target),
);
return ownMetadata;
}

private _mergeArray(result: T[], methodMeta: T | T[]) {
if (!result) {
if (Array.isArray(methodMeta)) {
result = methodMeta;
} else {
result = [methodMeta];
}
} else {
if (Array.isArray(methodMeta)) {
result.push(...methodMeta);
} else {
result.push(methodMeta);
}
}

return result;
}
}