Skip to content

Commit

Permalink
Merge 05369d0 into ee6a74e
Browse files Browse the repository at this point in the history
  • Loading branch information
mschnee committed Jan 14, 2020
2 parents ee6a74e + 05369d0 commit c341789
Show file tree
Hide file tree
Showing 3 changed files with 349 additions and 0 deletions.
86 changes: 86 additions & 0 deletions packages/metadata/README.md
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.

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 {
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
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
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;
}
}

0 comments on commit c341789

Please sign in to comment.