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 @deprecated convenience decorator #4415

Merged
merged 1 commit into from
Jan 31, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
38 changes: 38 additions & 0 deletions docs/site/decorators/Decorators_openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,44 @@ This namespace contains decorators that are specific to the OpenAPI
specification, but are also similar to other well-known decorators available,
such as `@deprecated()`

### @oas.deprecated

[API document](https://loopback.io/doc/en/lb4/apidocs.openapi-v3.deprecated.html),
[OpenAPI Operation Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operation-object)

This decorator can currently be applied to class and a class method. It will set
the `deprecated` boolean property of the Operation Object. When applied to a
class, it will mark all operation methods of that class as deprecated, unless a
method overloads with `@oas.deprecated(false)`.

This decorator does not currently support marking
(parameters)[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameter-object]
as deprecated.

```ts
@oas.deprecated()
class MyController {
@oas.get('/greet')
public async function greet() {
return 'Hello, World!'
}

@oas.get('/greet-v2')
@oas.deprecated(false)
public async function greetV2() {
return 'Hello, World!'
}
}

class MyOtherController {
@oas.get('/echo')
@oas.deprecated()
public async function echo() {
return 'Echo!'
}
}
```

### @oas.tags

[API document](https://loopback.io/doc/en/lb4/apidocs.openapi-v3.tags.html),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/openapi-v3
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {anOpenApiSpec} from '@loopback/openapi-spec-builder';
import {expect} from '@loopback/testlab';
import {api, get, getControllerSpec, oas} from '../../..';

describe('deprecation decorator', () => {
it('Returns a spec with all the items decorated from the class level', () => {
const expectedSpec = anOpenApiSpec()
.withOperationReturningString('get', '/greet', 'greet')
.withOperationReturningString('get', '/echo', 'echo')
.build();

@api(expectedSpec)
@oas.deprecated()
class MyController {
greet() {
return 'Hello world!';
}
echo() {
return 'Hello world!';
}
}

const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get.deprecated).to.eql(true);
expect(actualSpec.paths['/echo'].get.deprecated).to.eql(true);
});

it('Returns a spec where only one method is deprecated', () => {
class MyController {
@get('/greet')
greet() {
return 'Hello world!';
}

@get('/echo')
@oas.deprecated()
echo() {
return 'Hello world!';
}
}

const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get.deprecated).to.be.undefined();
expect(actualSpec.paths['/echo'].get.deprecated).to.eql(true);
});

it('Allows a method to override the deprecation of a class', () => {
@oas.deprecated()
class MyController {
@get('/greet')
greet() {
return 'Hello world!';
}

@get('/echo')
echo() {
return 'Hello world!';
}

@get('/yell')
@oas.deprecated(false)
yell() {
return 'HELLO WORLD!';
}
}
const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get.deprecated).to.eql(true);
expect(actualSpec.paths['/echo'].get.deprecated).to.eql(true);
expect(actualSpec.paths['/yell'].get.deprecated).to.be.undefined();
});

it('Allows a class to not be decorated with @oas.deprecated at all', () => {
class MyController {
@get('/greet')
greet() {
return 'Hello world!';
}

@get('/echo')
echo() {
return 'Hello world!';
}
}

const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get.deprecated).to.be.undefined();
expect(actualSpec.paths['/echo'].get.deprecated).to.be.undefined();
});

it('Does not allow a member variable to be decorated', () => {
const shouldThrow = () => {
class MyController {
@oas.deprecated()
public foo: string;

@get('/greet')
greet() {}
}

return getControllerSpec(MyController);
};

expect(shouldThrow).to.throw(
/^\@oas.deprecated cannot be used on a property:/,
);
});
});
34 changes: 33 additions & 1 deletion packages/openapi-v3/src/controller-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
spec = {paths: {}};
}

const isClassDeprecated = MetadataInspector.getClassMetadata<boolean>(
OAI3Keys.DEPRECATED_CLASS_KEY,
constructor,
);

if (isClassDeprecated) {
debug(' using class-level @deprecated()');
}
const classTags = MetadataInspector.getClassMetadata<TagsDecoratorMetadata>(
OAI3Keys.TAGS_CLASS_KEY,
constructor,
Expand All @@ -87,9 +95,13 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
debug(' using class-level @oas.tags()');
}

if (classTags) {
if (classTags || isClassDeprecated) {
for (const path of Object.keys(spec.paths)) {
for (const method of Object.keys(spec.paths[path])) {
/* istanbul ignore else */
if (isClassDeprecated) {
mschnee marked this conversation as resolved.
Show resolved Hide resolved
spec.paths[path][method].deprecated = true;
}
/* istanbul ignore else */
if (classTags) {
if (
Expand Down Expand Up @@ -121,6 +133,15 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
const verb = endpoint.verb!;
const path = endpoint.path!;

const isMethodDeprecated = MetadataInspector.getMethodMetadata<boolean>(
OAI3Keys.DEPRECATED_METHOD_KEY,
constructor.prototype,
op,
);
if (isMethodDeprecated) {
debug(' using method-level deprecation via @deprecated()');
}

const methodTags = MetadataInspector.getMethodMetadata<
TagsDecoratorMetadata
>(OAI3Keys.TAGS_METHOD_KEY, constructor.prototype, op);
Expand Down Expand Up @@ -168,6 +189,17 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {

debug(' spec responses for method %s: %o', op, operationSpec.responses);

// Prescedence: method decorator > class decorator > operationSpec > undefined
const deprecationSpec =
isMethodDeprecated ??
isClassDeprecated ??
operationSpec.deprecated ??
false;

if (deprecationSpec) {
operationSpec.deprecated = true;
}

for (const code in operationSpec.responses) {
const responseObject: ResponseObject | ReferenceObject =
operationSpec.responses[code];
Expand Down
87 changes: 87 additions & 0 deletions packages/openapi-v3/src/decorators/deprecated.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/openapi-v3
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {
ClassDecoratorFactory,
DecoratorFactory,
MethodDecoratorFactory,
} from '@loopback/core';
import {OAI3Keys} from '../keys';

const debug = require('debug')(
'loopback:openapi3:metadata:controller-spec:deprecated',
);

/**
* Marks an api path as deprecated. When applied to a class, this decorator
* marks all paths as deprecated.
*
* You can optionally mark all controllers in a class as deprecated, but use
* `@deprecated(false)` on a specific method to ensure it is not marked
* as deprecated in the specification.
*
* @param isDeprecated - whether or not the path should be marked as deprecated.
* This is useful for marking a class as deprecated, but a method as
* not deprecated.
*
* @example
* ```ts
* @oas.deprecated()
* class MyController {
* @get('/greet')
* public async function greet() {
* return 'Hello, World!'
* }
*
* @get('/greet-v2')
* @oas.deprecated(false)
* public async function greetV2() {
* return 'Hello, World!'
* }
* }
*
* class MyOtherController {
* @get('/echo')
* public async function echo() {
* return 'Echo!'
* }
* }
* ```
*/
export function deprecated(isDeprecated = true) {
mschnee marked this conversation as resolved.
Show resolved Hide resolved
return function deprecatedDecoratorForClassOrMethod(
// Class or a prototype
// eslint-disable-next-line @typescript-eslint/no-explicit-any
target: any,
method?: string,
// Use `any` to for `TypedPropertyDescriptor`
// See https://github.com/strongloop/loopback-next/pull/2704
// eslint-disable-next-line @typescript-eslint/no-explicit-any
methodDescriptor?: TypedPropertyDescriptor<any>,
) {
debug(target, method, methodDescriptor);

if (method && methodDescriptor) {
// Method
return MethodDecoratorFactory.createDecorator<boolean>(
OAI3Keys.DEPRECATED_METHOD_KEY,
isDeprecated,
{decoratorName: '@oas.deprecated'},
)(target, method, methodDescriptor);
} else if (typeof target === 'function' && !method && !methodDescriptor) {
// Class
return ClassDecoratorFactory.createDecorator<boolean>(
OAI3Keys.DEPRECATED_CLASS_KEY,
isDeprecated,
{decoratorName: '@oas.deprecated'},
)(target);
} else {
throw new Error(
'@oas.deprecated cannot be used on a property: ' +
DecoratorFactory.getTargetName(target, method, methodDescriptor),
);
}
};
}
12 changes: 11 additions & 1 deletion packages/openapi-v3/src/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
// License text available at https://opensource.org/licenses/MIT

export * from './api.decorator';
export * from './deprecated.decorator';
export * from './operation.decorator';
export * from './parameter.decorator';
export * from './request-body.decorator';
export * from './tags.decorator';

import {api} from './api.decorator';
import {deprecated} from './deprecated.decorator';
import {del, get, operation, patch, post, put} from './operation.decorator';
import {param} from './parameter.decorator';
import {requestBody} from './request-body.decorator';
Expand All @@ -18,12 +19,21 @@ import {tags} from './tags.decorator';
export const oas = {
api,
operation,

// methods
get,
post,
del,
patch,
put,

//param
param,

// request body
requestBody,

// oas convenience decorators
deprecated,
tags,
};
16 changes: 16 additions & 0 deletions packages/openapi-v3/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ export namespace OAI3Keys {
MethodDecorator
>('openapi-v3:methods');

/**
* Metadata key used to set or retrieve `@deprecated` metadata on a method.
*/
export const DEPRECATED_METHOD_KEY = MetadataAccessor.create<
boolean,
MethodDecorator
>('openapi-v3:methods:deprecated');

/**
* Metadata key used to set or retrieve `@deprecated` metadata on a class
*/
export const DEPRECATED_CLASS_KEY = MetadataAccessor.create<
boolean,
ClassDecorator
>('openapi-v3:class:deprecated');

/**
* Metadata key used to set or retrieve `param` decorator metadata
*/
Expand Down