Skip to content

Commit

Permalink
feat: adds @oas.deprecated() decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
matt authored and raymondfeng committed Jan 31, 2020
1 parent d9f5741 commit 6b6b5f0
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 2 deletions.
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) {
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) {
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

0 comments on commit 6b6b5f0

Please sign in to comment.