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 1 commit
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
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@
"0"
]
},
{
mschnee marked this conversation as resolved.
Show resolved Hide resolved
"type": "node",
"request": "launch",
"name": "Debug Current Test File",
"program": "${workspaceRoot}/packages/build/node_modules/.bin/_mocha",
"cwd": "${workspaceRoot}",
"autoAttachChildProcesses": true,
"args": [
"--config",
"${workspaceRoot}/packages/build/config/.mocharc.json",
"-t",
"0",
"$(node ${workspaceRoot}/packages/build/bin/get-dist-file ${file})"
],
"disableOptimisticBPs": true
},
{
"type": "node",
"request": "attach",
Expand Down
75 changes: 75 additions & 0 deletions packages/build/bin/get-dist-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env node
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/build
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

/*
========
This is used in the launch.json to enable you to debug a test file written in
typescript. This function attempts to convert the passed typescript file to
the best-gust output javascript file.

It walks up the filesystem from the current file, stops at package.json, and
looks in `dist`

Ideally, we could use the typescript compiler and tsconfig.json to get the
explicit output file instead of trying to guess it.

Ex:
```jsonc
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Current Test File",
"program": "${workspaceRoot}/packages/build/node_modules/.bin/_mocha",
"cwd": "${workspaceRoot}",
"autoAttachChildProcesses": true,
"args": [
"--config",
"${workspaceRoot}/packages/build/config/.mocharc.json",
"-t",
"0",
"$(node ${workspaceRoot}/packages/build/bin/get-dist-file ${file})"
],
"disableOptimisticBPs": true
}
]
}
```

For your personal projects, you can sub directlry from loopback:
```
"$(node ${workspaceRoot}/node_modules/@loopback/build/bin/get-dist-file ${file})"
```
You still have to compile the package/project first.
========
*/

'use strict';
const path = require('path');
const fs = require('fs');

function findDistFile(filename) {
const absolutePath = path.resolve(filename);
let currentDir = path.dirname(absolutePath);
let isPackageRoot = fs.existsSync(path.resolve(currentDir, 'package.json'));
while (!isPackageRoot) {
currentDir = path.join(currentDir, '..');
isPackageRoot = fs.existsSync(path.resolve(currentDir, 'package.json'));
}
const base = path.resolve(currentDir);
const relative = path.relative(currentDir, absolutePath);
const resultPath = relative
.replace(/^src\//, 'dist/')
.replace(/\.ts$/, '.js');
return path.resolve(base, resultPath);
}

module.exports = findDistFile;
if (require.main === module) {
process.stdout.write(findDistFile(process.argv.splice(-1)[0]));
}
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
Loading