From 1de5e5a16c08064389c133a42ac8dc507f40e773 Mon Sep 17 00:00:00 2001 From: jannyHou Date: Wed, 13 Dec 2017 12:02:21 -0500 Subject: [PATCH 1/3] refactor: Move metadata.ts to openapi-spec --- packages/openapi-spec/package.json | 11 +- .../controller-decorators/metadata.ts | 541 ++++++++++++++++++ packages/openapi-spec/src/index.ts | 1 + packages/openapi-spec/src/openapi-spec-v3.ts | 2 + .../openapi-spec/test/unit/metadata.test.ts | 357 ++++++++++++ .../test/unit/metadata/param-body.test.ts | 29 + .../unit/metadata/param-form-data.test.ts | 89 +++ .../test/unit/metadata/param-header.test.ts | 89 +++ .../test/unit/metadata/param-path.test.ts | 89 +++ .../test/unit/metadata/param-query.test.ts | 89 +++ .../test/unit/metadata/param.test.ts | 157 +++++ 11 files changed, 1453 insertions(+), 1 deletion(-) create mode 100644 packages/openapi-spec/src/decorators/controller-decorators/metadata.ts create mode 100644 packages/openapi-spec/src/openapi-spec-v3.ts create mode 100644 packages/openapi-spec/test/unit/metadata.test.ts create mode 100644 packages/openapi-spec/test/unit/metadata/param-body.test.ts create mode 100644 packages/openapi-spec/test/unit/metadata/param-form-data.test.ts create mode 100644 packages/openapi-spec/test/unit/metadata/param-header.test.ts create mode 100644 packages/openapi-spec/test/unit/metadata/param-path.test.ts create mode 100644 packages/openapi-spec/test/unit/metadata/param-query.test.ts create mode 100644 packages/openapi-spec/test/unit/metadata/param.test.ts diff --git a/packages/openapi-spec/package.json b/packages/openapi-spec/package.json index fa8caa9c6344..b9885d3be5df 100644 --- a/packages/openapi-spec/package.json +++ b/packages/openapi-spec/package.json @@ -6,7 +6,9 @@ "node": ">=6" }, "devDependencies": { - "@loopback/build": "^4.0.0-alpha.7" + "@loopback/build": "^4.0.0-alpha.7", + "@loopback/testlab": "^4.0.0-alpha.16", + "@loopback/openapi-spec-builder": "^4.0.0-alpha.14" }, "scripts": { "build": "npm run build:dist && npm run build:dist6", @@ -16,6 +18,8 @@ "build:apidocs": "lb-apidocs", "clean": "rm -rf loopback-openapi-spec*.tgz dist* package", "prepare": "npm run build && npm run build:apidocs", + "pretest": "npm run build:current", + "unit": "lb-dist mocha --opts ../../test/mocha.opts 'DIST/test/unit/**/*.js'", "verify": "npm pack && tar xf loopback-openapi-spec*.tgz && tree package && npm run clean" }, "author": "IBM", @@ -38,5 +42,10 @@ "repository": { "type": "git", "url": "https://github.com/strongloop/loopback-next.git" + }, + "dependencies": { + "@loopback/context": "^4.0.0-alpha.22", + "lodash": "^4.17.4", + "openapi3-ts": "^0.6.2" } } diff --git a/packages/openapi-spec/src/decorators/controller-decorators/metadata.ts b/packages/openapi-spec/src/decorators/controller-decorators/metadata.ts new file mode 100644 index 000000000000..40226f76caf2 --- /dev/null +++ b/packages/openapi-spec/src/decorators/controller-decorators/metadata.ts @@ -0,0 +1,541 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as assert from 'assert'; +import * as _ from 'lodash'; + +import {Reflector} from '@loopback/context'; +import { + OperationObject, + ParameterLocation, + ParameterObject, + SchemaObject, + ParameterType, + PathsObject, +} from '../../openapi-spec-v2'; + +const debug = require('debug')('loopback:core:router:metadata'); + +const ENDPOINTS_KEY = 'rest:endpoints'; +const API_SPEC_KEY = 'rest:api-spec'; + +// tslint:disable:no-any + +function cloneDeep(val: T): T { + if (val === undefined) { + return {} as T; + } + return _.cloneDeepWith(val, v => { + // Do not clone functions + if (typeof v === 'function') return v; + return undefined; + }); +} + +// !!!!!!! Need to be changed in openapi3 +export interface ControllerSpec { + /** + * The base path on which the Controller API is served. + * If it is not included, the API is served directly under the host. + * The value MUST start with a leading slash (/). + */ + basePath?: string; + + /** + * The available paths and operations for the API. + */ + paths: PathsObject; +} + +/** + * Decorate the given Controller constructor with metadata describing + * the HTTP/REST API the Controller implements/provides. + * + * `@api` can be applied to controller classes. For example, + * ``` + * @api({basePath: '/my'}) + * class MyController { + * // ... + * } + * ``` + * + * @param spec OpenAPI specification describing the endpoints + * handled by this controller + * + * @decorator + */ +export function api(spec: ControllerSpec) { + return function(constructor: Function) { + assert( + typeof constructor === 'function', + 'The @api decorator can be applied to constructors only.', + ); + const apiSpec = resolveControllerSpec(constructor, spec); + Reflector.defineMetadata(API_SPEC_KEY, apiSpec, constructor); + }; +} + +/** + * Data structure for REST related metadata + */ +interface RestEndpoint { + verb: string; + path: string; + spec?: OperationObject; + target: any; +} + +function getEndpoints( + target: any, +): {[property: string]: Partial} { + let endpoints = Reflector.getOwnMetadata(ENDPOINTS_KEY, target); + if (!endpoints) { + // Clone the endpoints so that subclasses won't mutate the metadata + // in the base class + const baseEndpoints = Reflector.getMetadata(ENDPOINTS_KEY, target); + endpoints = cloneDeep(baseEndpoints); + Reflector.defineMetadata(ENDPOINTS_KEY, endpoints, target); + } + return endpoints; +} + +/** + * Build the api spec from class and method level decorations + * @param constructor Controller class + * @param spec API spec + */ +function resolveControllerSpec( + constructor: Function, + spec?: ControllerSpec, +): ControllerSpec { + debug(`Retrieving OpenAPI specification for controller ${constructor.name}`); + + if (spec) { + debug(' using class-level spec defined via @api()', spec); + spec = cloneDeep(spec); + } else { + spec = {paths: {}}; + } + + const endpoints = getEndpoints(constructor.prototype); + + for (const op in endpoints) { + const endpoint = endpoints[op]; + const verb = endpoint.verb!; + const path = endpoint.path!; + + let endpointName = ''; + if (debug.enabled) { + const className = + endpoint.target.constructor.name || + constructor.name || + ''; + const fullMethodName = `${className}.${op}`; + endpointName = `${fullMethodName} (${verb} ${path})`; + } + + let operationSpec = endpoint.spec; + if (!operationSpec) { + // The operation was defined via @operation(verb, path) with no spec + operationSpec = { + responses: {}, + }; + endpoint.spec = operationSpec; + } + + operationSpec['x-operation-name'] = op; + + if (!spec.paths[path]) { + spec.paths[path] = {}; + } + + if (spec.paths[path][verb]) { + // Operations from subclasses override those from the base + debug(` Overriding ${endpointName} - endpoint was already defined`); + } + + debug(` adding ${endpointName}`, operationSpec); + spec.paths[path][verb] = operationSpec; + } + return spec; +} + +/** + * Get the controller spec for the given class + * @param constructor Controller class + */ +export function getControllerSpec(constructor: Function): ControllerSpec { + let spec = Reflector.getOwnMetadata(API_SPEC_KEY, constructor); + if (!spec) { + spec = resolveControllerSpec(constructor, spec); + Reflector.defineMetadata(API_SPEC_KEY, spec, constructor); + } + return spec; +} + +/** + * Expose a Controller method as a REST API operation + * mapped to `GET` request method. + * + * @param path The URL path of this operation, e.g. `/product/{id}` + * @param spec The OpenAPI specification describing parameters and responses + * of this operation. + */ +export function get(path: string, spec?: OperationObject) { + return operation('get', path, spec); +} + +/** + * Expose a Controller method as a REST API operation + * mapped to `POST` request method. + * + * @param path The URL path of this operation, e.g. `/product/{id}` + * @param spec The OpenAPI specification describing parameters and responses + * of this operation. + */ +export function post(path: string, spec?: OperationObject) { + return operation('post', path, spec); +} + +/** + * Expose a Controller method as a REST API operation + * mapped to `PUT` request method. + * + * @param path The URL path of this operation, e.g. `/product/{id}` + * @param spec The OpenAPI specification describing parameters and responses + * of this operation. + */ +export function put(path: string, spec?: OperationObject) { + return operation('put', path, spec); +} + +/** + * Expose a Controller method as a REST API operation + * mapped to `PATCH` request method. + * + * @param path The URL path of this operation, e.g. `/product/{id}` + * @param spec The OpenAPI specification describing parameters and responses + * of this operation. + */ +export function patch(path: string, spec?: OperationObject) { + return operation('patch', path, spec); +} + +/** + * Expose a Controller method as a REST API operation + * mapped to `DELETE` request method. + * + * @param path The URL path of this operation, e.g. `/product/{id}` + * @param spec The OpenAPI specification describing parameters and responses + * of this operation. + */ +export function del(path: string, spec?: OperationObject) { + return operation('delete', path, spec); +} + +/** + * Expose a Controller method as a REST API operation. + * + * @param verb HTTP verb, e.g. `GET` or `POST`. + * @param path The URL path of this operation, e.g. `/product/{id}` + * @param spec The OpenAPI specification describing parameters and responses + * of this operation. + */ +export function operation(verb: string, path: string, spec?: OperationObject) { + return function( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor, + ) { + assert( + typeof target[propertyKey] === 'function', + '@operation decorator can be applied to methods only', + ); + + const endpoints = getEndpoints(target); + let endpoint = endpoints[propertyKey]; + if (!endpoint || endpoint.target !== target) { + // Add the new endpoint metadata for the method + endpoint = {verb, path, spec, target}; + endpoints[propertyKey] = endpoint; + } else { + // Update the endpoint metadata + // It can be created by @param + endpoint.verb = verb; + endpoint.path = path; + endpoint.target = target; + } + + if (!spec) { + // Users can define parameters and responses using decorators + return; + } + + // Decorator are invoked in reverse order of their definition. + // For example, a method decorated with @operation() @param() + // will invoke param() decorator first and operation() second. + // As a result, we need to preserve any partial definitions + // already provided by other decorators. + editOperationSpec(endpoint, overrides => { + const mergedSpec = Object.assign({}, spec, overrides); + + // Merge "responses" definitions + mergedSpec.responses = Object.assign( + {}, + spec.responses, + overrides.responses, + ); + + return mergedSpec; + }); + }; +} + +const paramDecoratorStyle = Symbol('ParamDecoratorStyle'); + +/** + * Describe an input parameter of a Controller method. + * + * `@param` can be applied to method itself or specific parameters. For example, + * ``` + * class MyController { + * @get('/') + * @param(offsetSpec) + * @param(pageSizeSpec) + * list(offset?: number, pageSize?: number) {} + * } + * ``` + * or + * ``` + * class MyController { + * @get('/') + * list( + * @param(offsetSpec) offset?: number, + * @param(pageSizeSpec) pageSize?: number, + * ) {} + * } + * ``` + * Please note mixed usage of `@param` at method/parameter level is not allowed. + * + * @param paramSpec Parameter specification. + */ +export function param(paramSpec: ParameterObject) { + return function( + target: any, + propertyKey: string, + descriptorOrParameterIndex: PropertyDescriptor | number, + ) { + assert( + typeof target[propertyKey] === 'function', + '@param decorator can be applied to methods only', + ); + + const endpoints = getEndpoints(target); + let endpoint = endpoints[propertyKey]; + if (!endpoint || endpoint.target !== target) { + const baseEndpoint = endpoint; + // Add the new endpoint metadata for the method + endpoint = cloneDeep(baseEndpoint); + endpoint.target = target; + endpoints[propertyKey] = endpoint; + } + + editOperationSpec(endpoint, operationSpec => { + let decoratorStyle; + if (typeof descriptorOrParameterIndex === 'number') { + decoratorStyle = 'parameter'; + } else { + decoratorStyle = 'method'; + } + if (!operationSpec.parameters) { + operationSpec.parameters = []; + // Record the @param decorator style to ensure consistency + operationSpec[paramDecoratorStyle] = decoratorStyle; + } else { + // Mixed usage of @param at method/parameter level is not allowed + if (operationSpec[paramDecoratorStyle] !== decoratorStyle) { + throw new Error( + 'Mixed usage of @param at method/parameter level' + + ' is not allowed.', + ); + } + } + + if (typeof descriptorOrParameterIndex === 'number') { + operationSpec.parameters[descriptorOrParameterIndex] = paramSpec; + } else { + operationSpec.parameters.unshift(paramSpec); + } + + return operationSpec; + }); + }; +} + +function editOperationSpec( + endpoint: Partial, + updateFn: (spec: OperationObject) => OperationObject, +) { + let spec = endpoint.spec; + if (!spec) { + spec = { + responses: {}, + }; + } + + spec = updateFn(spec); + endpoint.spec = spec; +} + +export namespace param { + export const query = { + /** + * Define a parameter of "string" type that's read from the query string. + * + * @param name Parameter name. + */ + string: createParamShortcut('query', 'string'), + + /** + * Define a parameter of "number" type that's read from the query string. + * + * @param name Parameter name. + */ + number: createParamShortcut('query', 'number'), + + /** + * Define a parameter of "integer" type that's read from the query string. + * + * @param name Parameter name. + */ + integer: createParamShortcut('query', 'integer'), + + /** + * Define a parameter of "boolean" type that's read from the query string. + * + * @param name Parameter name. + */ + boolean: createParamShortcut('query', 'boolean'), + }; + + export const header = { + /** + * Define a parameter of "string" type that's read from a request header. + * + * @param name Parameter name, it must match the header name + * (e.g. `Content-Type`). + */ + string: createParamShortcut('header', 'string'), + + /** + * Define a parameter of "number" type that's read from a request header. + * + * @param name Parameter name, it must match the header name + * (e.g. `Content-Length`). + */ + number: createParamShortcut('header', 'number'), + + /** + * Define a parameter of "integer" type that's read from a request header. + * + * @param name Parameter name, it must match the header name + * (e.g. `Content-Length`). + */ + integer: createParamShortcut('header', 'integer'), + + /** + * Define a parameter of "boolean" type that's read from a request header. + * + * @param name Parameter name, it must match the header name, + * (e.g. `DNT` or `X-Do-Not-Track`). + */ + boolean: createParamShortcut('header', 'boolean'), + }; + + export const path = { + /** + * Define a parameter of "string" type that's read from request path. + * + * @param name Parameter name matching one of the placeholders in the path + * string. + */ + string: createParamShortcut('path', 'string'), + + /** + * Define a parameter of "number" type that's read from request path. + * + * @param name Parameter name matching one of the placeholders in the path + * string. + */ + number: createParamShortcut('path', 'number'), + + /** + * Define a parameter of "integer" type that's read from request path. + * + * @param name Parameter name matching one of the placeholders in the path + * string. + */ + integer: createParamShortcut('path', 'integer'), + + /** + * Define a parameter of "boolean" type that's read from request path. + * + * @param name Parameter name matching one of the placeholders in the path + * string. + */ + boolean: createParamShortcut('path', 'boolean'), + }; + + export const formData = { + /** + * Define a parameter of "string" type that's read + * from a field in the request body. + * + * @param name Parameter name. + */ + string: createParamShortcut('formData', 'string'), + + /** + * Define a parameter of "number" type that's read + * from a field in the request body. + * + * @param name Parameter name. + */ + number: createParamShortcut('formData', 'number'), + + /** + * Define a parameter of "integer" type that's read + * from a field in the request body. + * + * @param name Parameter name. + */ + integer: createParamShortcut('formData', 'integer'), + + /** + * Define a parameter of "boolean" type that's read + * from a field in the request body. + * + * @param name Parameter name. + */ + boolean: createParamShortcut('formData', 'boolean'), + }; + + /** + * Define a parameter that's set to the full request body. + * + * @param name Parameter name + * @param schema The schema defining the type used for the body parameter. + */ + export const body = function(name: string, schema: SchemaObject) { + return param({name, in: 'body', schema}); + }; +} + +function createParamShortcut(source: ParameterLocation, type: ParameterType) { + // TODO(bajtos) @param.IN.TYPE('foo', {required: true}) + return (name: string) => { + return param({name, in: source, type}); + }; +} diff --git a/packages/openapi-spec/src/index.ts b/packages/openapi-spec/src/index.ts index f20d0cedf7b3..4d0977c04cb4 100644 --- a/packages/openapi-spec/src/index.ts +++ b/packages/openapi-spec/src/index.ts @@ -4,3 +4,4 @@ // License text available at https://opensource.org/licenses/MIT export * from './openapi-spec-v2'; +export * from './decorators/controller-decorators/metadata'; diff --git a/packages/openapi-spec/src/openapi-spec-v3.ts b/packages/openapi-spec/src/openapi-spec-v3.ts new file mode 100644 index 000000000000..324d08ac8135 --- /dev/null +++ b/packages/openapi-spec/src/openapi-spec-v3.ts @@ -0,0 +1,2 @@ +export * from 'openapi3-ts'; + diff --git a/packages/openapi-spec/test/unit/metadata.test.ts b/packages/openapi-spec/test/unit/metadata.test.ts new file mode 100644 index 000000000000..deb31a6a5a9d --- /dev/null +++ b/packages/openapi-spec/test/unit/metadata.test.ts @@ -0,0 +1,357 @@ +// Copyright IBM Corp. 2013,2017. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + get, + api, + getControllerSpec, + operation, + post, + put, + patch, + del, + param, +} from '../..'; +import {expect} from '@loopback/testlab'; +import {anOpenApiSpec, anOperationSpec} from '@loopback/openapi-spec-builder'; + +describe('Routing metadata', () => { + it('returns spec defined via @api()', () => { + const expectedSpec = anOpenApiSpec() + .withOperationReturningString('get', '/greet', 'greet') + .build(); + + @api(expectedSpec) + class MyController { + greet() { + return 'Hello world!'; + } + } + + const actualSpec = getControllerSpec(MyController); + expect(actualSpec).to.eql(expectedSpec); + }); + + it('returns spec defined via @get decorator', () => { + const operationSpec = anOperationSpec() + .withStringResponse() + .build(); + + class MyController { + @get('/greet', operationSpec) + greet() { + return 'Hello world!'; + } + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec).to.eql({ + paths: { + '/greet': { + get: { + 'x-operation-name': 'greet', + ...operationSpec, + }, + }, + }, + }); + }); + + it('returns spec defined via @post decorator', () => { + const operationSpec = anOperationSpec() + .withStringResponse() + .build(); + + class MyController { + @post('/greeting', operationSpec) + createGreeting() {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec).to.eql({ + paths: { + '/greeting': { + post: { + 'x-operation-name': 'createGreeting', + ...operationSpec, + }, + }, + }, + }); + }); + + it('returns spec defined via @put decorator', () => { + const operationSpec = anOperationSpec() + .withStringResponse() + .build(); + + class MyController { + @put('/greeting', operationSpec) + updateGreeting() {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec).to.eql({ + paths: { + '/greeting': { + put: { + 'x-operation-name': 'updateGreeting', + ...operationSpec, + }, + }, + }, + }); + }); + + it('returns spec defined via @patch decorator', () => { + const operationSpec = anOperationSpec() + .withStringResponse() + .build(); + + class MyController { + @patch('/greeting', operationSpec) + patchGreeting() {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec).to.eql({ + paths: { + '/greeting': { + patch: { + 'x-operation-name': 'patchGreeting', + ...operationSpec, + }, + }, + }, + }); + }); + + it('returns spec defined via @del decorator', () => { + const operationSpec = anOperationSpec() + .withStringResponse() + .build(); + + class MyController { + @del('/greeting', operationSpec) + deleteGreeting() {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec).to.eql({ + paths: { + '/greeting': { + delete: { + 'x-operation-name': 'deleteGreeting', + ...operationSpec, + }, + }, + }, + }); + }); + + it('returns spec defined via @operation decorator', () => { + const operationSpec = anOperationSpec() + .withStringResponse() + .build(); + + class MyController { + @operation('post', '/greeting', operationSpec) + createGreeting() {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec).to.eql({ + paths: { + '/greeting': { + post: { + 'x-operation-name': 'createGreeting', + ...operationSpec, + }, + }, + }, + }); + }); + + it('returns default spec for @get with no spec', () => { + class MyController { + @get('/greet') + greet() {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet']['get']).to.eql({ + 'x-operation-name': 'greet', + responses: {}, + }); + }); + + it('returns default spec for @operation with no spec', () => { + class MyController { + @operation('post', '/greeting') + createGreeting() {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greeting']['post']).to.eql({ + 'x-operation-name': 'createGreeting', + responses: {}, + }); + }); + + it('honours specifications from inherited methods', () => { + const operationSpec = anOperationSpec() + .withStringResponse() + .build(); + + class Parent { + @get('/parent', operationSpec) + getParentName() { + return 'The Parent'; + } + } + + class Child extends Parent { + @get('/child', operationSpec) + getChildName() { + return 'The Child'; + } + } + + const actualSpec = getControllerSpec(Child); + + expect(actualSpec).to.eql({ + paths: { + '/parent': { + get: { + 'x-operation-name': 'getParentName', + ...operationSpec, + }, + }, + '/child': { + get: { + 'x-operation-name': 'getChildName', + ...operationSpec, + }, + }, + }, + }); + }); + + it('allows children to override parent REST endpoints', () => { + const operationSpec = anOperationSpec() + .withStringResponse() + .build(); + + class Parent { + @get('/name', operationSpec) + getParentName() { + return 'The Parent'; + } + } + + class Child extends Parent { + @get('/name', operationSpec) + getChildName() { + return 'The Child'; + } + } + + const actualSpec = getControllerSpec(Child); + + expect(actualSpec.paths['/name']['get']).to.have.property( + 'x-operation-name', + 'getChildName', + ); + }); + + it('allows children to override parent REST operations', () => { + const operationSpec = anOperationSpec() + .withStringResponse() + .build(); + + class Parent { + @get('/parent-name', operationSpec) + getName() { + return 'The Parent'; + } + } + + class Child extends Parent { + @get('/child-name', operationSpec) + getName() { + return 'The Child'; + } + } + + const childSpec = getControllerSpec(Child); + const parentSpec = getControllerSpec(Parent); + + expect(childSpec.paths['/child-name']['get']).to.have.property( + 'x-operation-name', + 'getName', + ); + + // The parent endpoint has been overridden + expect(childSpec.paths).to.not.have.property('/parent-name'); + + expect(parentSpec.paths['/parent-name']['get']).to.have.property( + 'x-operation-name', + 'getName', + ); + + // The parent endpoint should not be polluted + expect(parentSpec.paths).to.not.have.property('/child-name'); + }); + + it('allows children to override parent REST parameters', () => { + const operationSpec = anOperationSpec() + .withStringResponse() + .build(); + + class Parent { + @get('/greet', operationSpec) + greet(@param.query.string('msg') msg: string) { + return `Parent: ${msg}`; + } + } + + class Child extends Parent { + greet(@param.query.string('message') msg: string) { + return `Child: ${msg}`; + } + } + + const childSpec = getControllerSpec(Child); + const parentSpec = getControllerSpec(Parent); + + const childGreet = childSpec.paths['/greet']['get']; + expect(childGreet).to.have.property('x-operation-name', 'greet'); + + expect(childGreet.parameters).to.have.property('length', 1); + + expect(childGreet.parameters[0]).to.containEql({ + name: 'message', + in: 'query', + }); + + const parentGreet = parentSpec.paths['/greet']['get']; + expect(parentGreet).to.have.property('x-operation-name', 'greet'); + + expect(parentGreet.parameters).to.have.property('length', 1); + + expect(parentGreet.parameters[0]).to.containEql({ + name: 'msg', + in: 'query', + }); + }); +}); diff --git a/packages/openapi-spec/test/unit/metadata/param-body.test.ts b/packages/openapi-spec/test/unit/metadata/param-body.test.ts new file mode 100644 index 000000000000..e1801ae2d048 --- /dev/null +++ b/packages/openapi-spec/test/unit/metadata/param-body.test.ts @@ -0,0 +1,29 @@ +// Copyright IBM Corp. 2013,2017. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {post, param, getControllerSpec} from '../../..'; +import {expect} from '@loopback/testlab'; + +describe('Routing metadata for parameters', () => { + describe('@param.body', () => { + it('defines a parameter with in:body', () => { + class MyController { + @post('/greeting') + @param.body('data', {type: 'object'}) + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ + { + name: 'data', + in: 'body', + schema: {type: 'object'}, + }, + ]); + }); + }); +}); diff --git a/packages/openapi-spec/test/unit/metadata/param-form-data.test.ts b/packages/openapi-spec/test/unit/metadata/param-form-data.test.ts new file mode 100644 index 000000000000..38726617cabb --- /dev/null +++ b/packages/openapi-spec/test/unit/metadata/param-form-data.test.ts @@ -0,0 +1,89 @@ +// Copyright IBM Corp. 2013,2017. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {post, param, getControllerSpec} from '../../..'; +import {expect} from '@loopback/testlab'; + +describe('Routing metadata for parameters', () => { + describe('@param.formData.string', () => { + it('defines a parameter with in:formData type:string', () => { + class MyController { + @post('/greeting') + @param.formData.string('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ + { + name: 'name', + type: 'string', + in: 'formData', + }, + ]); + }); + }); + + describe('@param.formData.number', () => { + it('defines a parameter with in:formData type:number', () => { + class MyController { + @post('/greeting') + @param.formData.number('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ + { + name: 'name', + type: 'number', + in: 'formData', + }, + ]); + }); + }); + + describe('@param.formData.integer', () => { + it('defines a parameter with in:formData type:integer', () => { + class MyController { + @post('/greeting') + @param.formData.integer('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ + { + name: 'name', + type: 'integer', + in: 'formData', + }, + ]); + }); + }); + + describe('@param.formData.boolean', () => { + it('defines a parameter with in:formData type:boolean', () => { + class MyController { + @post('/greeting') + @param.formData.boolean('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ + { + name: 'name', + type: 'boolean', + in: 'formData', + }, + ]); + }); + }); +}); diff --git a/packages/openapi-spec/test/unit/metadata/param-header.test.ts b/packages/openapi-spec/test/unit/metadata/param-header.test.ts new file mode 100644 index 000000000000..d16d1441a84c --- /dev/null +++ b/packages/openapi-spec/test/unit/metadata/param-header.test.ts @@ -0,0 +1,89 @@ +// Copyright IBM Corp. 2013,2017. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {get, param, getControllerSpec} from '../../..'; +import {expect} from '@loopback/testlab'; + +describe('Routing metadata for parameters', () => { + describe('@param.header.string', () => { + it('defines a parameter with in:header type:string', () => { + class MyController { + @get('/greet') + @param.header.string('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ + { + name: 'name', + type: 'string', + in: 'header', + }, + ]); + }); + }); + + describe('@param.header.number', () => { + it('defines a parameter with in:header type:number', () => { + class MyController { + @get('/greet') + @param.header.number('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ + { + name: 'name', + type: 'number', + in: 'header', + }, + ]); + }); + }); + + describe('@param.header.integer', () => { + it('defines a parameter with in:header type:integer', () => { + class MyController { + @get('/greet') + @param.header.integer('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ + { + name: 'name', + type: 'integer', + in: 'header', + }, + ]); + }); + }); + + describe('@param.header.boolean', () => { + it('defines a parameter with in:header type:boolean', () => { + class MyController { + @get('/greet') + @param.header.boolean('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ + { + name: 'name', + type: 'boolean', + in: 'header', + }, + ]); + }); + }); +}); diff --git a/packages/openapi-spec/test/unit/metadata/param-path.test.ts b/packages/openapi-spec/test/unit/metadata/param-path.test.ts new file mode 100644 index 000000000000..4dbcfe8b2d60 --- /dev/null +++ b/packages/openapi-spec/test/unit/metadata/param-path.test.ts @@ -0,0 +1,89 @@ +// Copyright IBM Corp. 2013,2017. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {get, param, getControllerSpec} from '../../..'; +import {expect} from '@loopback/testlab'; + +describe('Routing metadata for parameters', () => { + describe('@param.path.string', () => { + it('defines a parameter with in:path type:string', () => { + class MyController { + @get('/greet/{name}') + @param.path.string('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet/{name}']['get'].parameters).to.eql([ + { + name: 'name', + type: 'string', + in: 'path', + }, + ]); + }); + }); + + describe('@param.path.number', () => { + it('defines a parameter with in:path type:number', () => { + class MyController { + @get('/greet/{name}') + @param.path.number('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet/{name}']['get'].parameters).to.eql([ + { + name: 'name', + type: 'number', + in: 'path', + }, + ]); + }); + }); + + describe('@param.path.integer', () => { + it('defines a parameter with in:path type:integer', () => { + class MyController { + @get('/greet/{name}') + @param.path.integer('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet/{name}']['get'].parameters).to.eql([ + { + name: 'name', + type: 'integer', + in: 'path', + }, + ]); + }); + }); + + describe('@param.path.boolean', () => { + it('defines a parameter with in:path type:boolean', () => { + class MyController { + @get('/greet/{name}') + @param.path.boolean('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet/{name}']['get'].parameters).to.eql([ + { + name: 'name', + type: 'boolean', + in: 'path', + }, + ]); + }); + }); +}); diff --git a/packages/openapi-spec/test/unit/metadata/param-query.test.ts b/packages/openapi-spec/test/unit/metadata/param-query.test.ts new file mode 100644 index 000000000000..7df9a1286854 --- /dev/null +++ b/packages/openapi-spec/test/unit/metadata/param-query.test.ts @@ -0,0 +1,89 @@ +// Copyright IBM Corp. 2013,2017. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {get, param, getControllerSpec} from '../../..'; +import {expect} from '@loopback/testlab'; + +describe('Routing metadata for parameters', () => { + describe('@param.query.string', () => { + it('defines a parameter with in:query type:string', () => { + class MyController { + @get('/greet') + @param.query.string('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ + { + name: 'name', + type: 'string', + in: 'query', + }, + ]); + }); + }); + + describe('@param.query.number', () => { + it('defines a parameter with in:query type:number', () => { + class MyController { + @get('/greet') + @param.query.number('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ + { + name: 'name', + type: 'number', + in: 'query', + }, + ]); + }); + }); + + describe('@param.query.integer', () => { + it('defines a parameter with in:query type:integer', () => { + class MyController { + @get('/greet') + @param.query.integer('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ + { + name: 'name', + type: 'integer', + in: 'query', + }, + ]); + }); + }); + + describe('@param.query.boolean', () => { + it('defines a parameter with in:query type:boolean', () => { + class MyController { + @get('/greet') + @param.query.boolean('name') + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ + { + name: 'name', + type: 'boolean', + in: 'query', + }, + ]); + }); + }); +}); diff --git a/packages/openapi-spec/test/unit/metadata/param.test.ts b/packages/openapi-spec/test/unit/metadata/param.test.ts new file mode 100644 index 000000000000..f1a5855b9a68 --- /dev/null +++ b/packages/openapi-spec/test/unit/metadata/param.test.ts @@ -0,0 +1,157 @@ +// Copyright IBM Corp. 2013,2017. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {get, param, getControllerSpec, operation} from '../../..'; +import { + OperationObject, + ParameterObject, + ResponsesObject, +} from '@loopback/openapi-spec'; +import {expect} from '@loopback/testlab'; +import {anOperationSpec} from '@loopback/openapi-spec-builder'; + +describe('Routing metadata for parameters', () => { + describe('@param', () => { + it('defines a new parameter', () => { + const paramSpec: ParameterObject = { + name: 'name', + type: 'string', + in: 'query', + }; + + class MyController { + @get('/greet') + @param(paramSpec) + greet(name: string) {} + } + + const actualSpec = getControllerSpec(MyController); + + const expectedSpec = anOperationSpec() + .withOperationName('greet') + .withParameter(paramSpec) + .build(); + + expect(actualSpec.paths['/greet']['get']).to.eql(expectedSpec); + }); + + it('can define multiple parameters in order', () => { + const offsetSpec: ParameterObject = { + name: 'offset', + type: 'number', + in: 'query', + }; + + const pageSizeSpec: ParameterObject = { + name: 'pageSize', + type: 'number', + in: 'query', + }; + + class MyController { + @get('/') + @param(offsetSpec) + @param(pageSizeSpec) + list(offset?: number, pageSize?: number) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/']['get'].parameters).to.eql([ + offsetSpec, + pageSizeSpec, + ]); + }); + + it('can define multiple parameters by arguments', () => { + const offsetSpec: ParameterObject = { + name: 'offset', + type: 'number', + in: 'query', + }; + + const pageSizeSpec: ParameterObject = { + name: 'pageSize', + type: 'number', + in: 'query', + }; + + class MyController { + @get('/') + list( + @param(offsetSpec) offset?: number, + @param(pageSizeSpec) pageSize?: number, + ) {} + } + + const actualSpec = getControllerSpec(MyController); + + expect(actualSpec.paths['/']['get'].parameters).to.eql([ + offsetSpec, + pageSizeSpec, + ]); + }); + // tslint:disable-next-line:max-line-length + it('throws an error if @param is used at both method and parameter level', () => { + expect(() => { + const offsetSpec: ParameterObject = { + name: 'offset', + type: 'number', + in: 'query', + }; + + const pageSizeSpec: ParameterObject = { + name: 'pageSize', + type: 'number', + in: 'query', + }; + // tslint:disable-next-line:no-unused-variable + class MyController { + @get('/') + @param(offsetSpec) + list(offset?: number, @param(pageSizeSpec) pageSize?: number) {} + } + }).to.throw( + /Mixed usage of @param at method\/parameter level is not allowed/, + ); + }); + + it('adds to existing spec provided via @operation', () => { + const offsetSpec: ParameterObject = { + name: 'offset', + type: 'number', + in: 'query', + }; + + const pageSizeSpec: ParameterObject = { + name: 'pageSize', + type: 'number', + in: 'query', + }; + + const responses: ResponsesObject = { + 200: { + schema: { + type: 'string', + }, + description: 'a string response', + }, + }; + + class MyController { + @operation('get', '/', {responses}) + @param(offsetSpec) + @param(pageSizeSpec) + list(offset?: number, pageSize?: number) {} + } + + const apiSpec = getControllerSpec(MyController); + const opSpec: OperationObject = apiSpec.paths['/']['get']; + + expect(opSpec.responses).to.eql(responses); + expect(opSpec.parameters).to.eql([offsetSpec, pageSizeSpec]); + }); + }); +}); From 3ea93b804eaa7e6f9f5f41c0c98996bc4c11c419 Mon Sep 17 00:00:00 2001 From: jannyHou Date: Wed, 13 Dec 2017 12:08:03 -0500 Subject: [PATCH 2/3] refactor: Delete metadata.ts in rest --- packages/rest/src/http-handler.ts | 4 +- packages/rest/src/index.ts | 2 +- packages/rest/src/rest-server.ts | 3 +- packages/rest/src/router/metadata.ts | 540 ------------------ packages/rest/src/router/routing-table.ts | 3 +- .../rest/test/unit/router/metadata.test.ts | 357 ------------ .../unit/router/metadata/param-body.test.ts | 29 - .../router/metadata/param-form-data.test.ts | 89 --- .../unit/router/metadata/param-header.test.ts | 89 --- .../unit/router/metadata/param-path.test.ts | 89 --- .../unit/router/metadata/param-query.test.ts | 89 --- .../test/unit/router/metadata/param.test.ts | 157 ----- 12 files changed, 7 insertions(+), 1444 deletions(-) delete mode 100644 packages/rest/src/router/metadata.ts delete mode 100644 packages/rest/test/unit/router/metadata.test.ts delete mode 100644 packages/rest/test/unit/router/metadata/param-body.test.ts delete mode 100644 packages/rest/test/unit/router/metadata/param-form-data.test.ts delete mode 100644 packages/rest/test/unit/router/metadata/param-header.test.ts delete mode 100644 packages/rest/test/unit/router/metadata/param-path.test.ts delete mode 100644 packages/rest/test/unit/router/metadata/param-query.test.ts delete mode 100644 packages/rest/test/unit/router/metadata/param.test.ts diff --git a/packages/rest/src/http-handler.ts b/packages/rest/src/http-handler.ts index 6ba348ef7266..34b0bc238832 100644 --- a/packages/rest/src/http-handler.ts +++ b/packages/rest/src/http-handler.ts @@ -4,9 +4,9 @@ // License text available at https://opensource.org/licenses/MIT import {Context} from '@loopback/context'; -import {PathsObject} from '@loopback/openapi-spec'; +import {PathsObject, ControllerSpec} from '@loopback/openapi-spec'; import {ServerRequest, ServerResponse} from 'http'; -import {ControllerSpec} from './router/metadata'; +// import {ControllerSpec} from './router/metadata'; import {SequenceHandler} from './sequence'; import { diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index abf3ab3e38be..90ba07d2b275 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -16,7 +16,7 @@ export { parseRequestUrl, } from './router/routing-table'; -export * from './router/metadata'; +export * from '@loopback/openapi-spec'; export * from './providers'; // import all errors from external http-errors package diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 78d562b28c3a..6103856434f3 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -16,7 +16,8 @@ import { import {ServerRequest, ServerResponse, createServer} from 'http'; import * as Http from 'http'; import {Application, CoreBindings, Server} from '@loopback/core'; -import {getControllerSpec} from './router/metadata'; +import {getControllerSpec} from '@loopback/openapi-spec'; +// import {getControllerSpec} from './router/metadata'; import {HttpHandler} from './http-handler'; import {DefaultSequence, SequenceHandler, SequenceFunction} from './sequence'; import { diff --git a/packages/rest/src/router/metadata.ts b/packages/rest/src/router/metadata.ts deleted file mode 100644 index 912afbda4502..000000000000 --- a/packages/rest/src/router/metadata.ts +++ /dev/null @@ -1,540 +0,0 @@ -// Copyright IBM Corp. 2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import * as assert from 'assert'; -import * as _ from 'lodash'; - -import {Reflector} from '@loopback/context'; -import { - OperationObject, - ParameterLocation, - ParameterObject, - SchemaObject, - ParameterType, - PathsObject, -} from '@loopback/openapi-spec'; - -const debug = require('debug')('loopback:core:router:metadata'); - -const ENDPOINTS_KEY = 'rest:endpoints'; -const API_SPEC_KEY = 'rest:api-spec'; - -// tslint:disable:no-any - -function cloneDeep(val: T): T { - if (val === undefined) { - return {} as T; - } - return _.cloneDeepWith(val, v => { - // Do not clone functions - if (typeof v === 'function') return v; - return undefined; - }); -} - -export interface ControllerSpec { - /** - * The base path on which the Controller API is served. - * If it is not included, the API is served directly under the host. - * The value MUST start with a leading slash (/). - */ - basePath?: string; - - /** - * The available paths and operations for the API. - */ - paths: PathsObject; -} - -/** - * Decorate the given Controller constructor with metadata describing - * the HTTP/REST API the Controller implements/provides. - * - * `@api` can be applied to controller classes. For example, - * ``` - * @api({basePath: '/my'}) - * class MyController { - * // ... - * } - * ``` - * - * @param spec OpenAPI specification describing the endpoints - * handled by this controller - * - * @decorator - */ -export function api(spec: ControllerSpec) { - return function(constructor: Function) { - assert( - typeof constructor === 'function', - 'The @api decorator can be applied to constructors only.', - ); - const apiSpec = resolveControllerSpec(constructor, spec); - Reflector.defineMetadata(API_SPEC_KEY, apiSpec, constructor); - }; -} - -/** - * Data structure for REST related metadata - */ -interface RestEndpoint { - verb: string; - path: string; - spec?: OperationObject; - target: any; -} - -function getEndpoints( - target: any, -): {[property: string]: Partial} { - let endpoints = Reflector.getOwnMetadata(ENDPOINTS_KEY, target); - if (!endpoints) { - // Clone the endpoints so that subclasses won't mutate the metadata - // in the base class - const baseEndpoints = Reflector.getMetadata(ENDPOINTS_KEY, target); - endpoints = cloneDeep(baseEndpoints); - Reflector.defineMetadata(ENDPOINTS_KEY, endpoints, target); - } - return endpoints; -} - -/** - * Build the api spec from class and method level decorations - * @param constructor Controller class - * @param spec API spec - */ -function resolveControllerSpec( - constructor: Function, - spec?: ControllerSpec, -): ControllerSpec { - debug(`Retrieving OpenAPI specification for controller ${constructor.name}`); - - if (spec) { - debug(' using class-level spec defined via @api()', spec); - spec = cloneDeep(spec); - } else { - spec = {paths: {}}; - } - - const endpoints = getEndpoints(constructor.prototype); - - for (const op in endpoints) { - const endpoint = endpoints[op]; - const verb = endpoint.verb!; - const path = endpoint.path!; - - let endpointName = ''; - if (debug.enabled) { - const className = - endpoint.target.constructor.name || - constructor.name || - ''; - const fullMethodName = `${className}.${op}`; - endpointName = `${fullMethodName} (${verb} ${path})`; - } - - let operationSpec = endpoint.spec; - if (!operationSpec) { - // The operation was defined via @operation(verb, path) with no spec - operationSpec = { - responses: {}, - }; - endpoint.spec = operationSpec; - } - - operationSpec['x-operation-name'] = op; - - if (!spec.paths[path]) { - spec.paths[path] = {}; - } - - if (spec.paths[path][verb]) { - // Operations from subclasses override those from the base - debug(` Overriding ${endpointName} - endpoint was already defined`); - } - - debug(` adding ${endpointName}`, operationSpec); - spec.paths[path][verb] = operationSpec; - } - return spec; -} - -/** - * Get the controller spec for the given class - * @param constructor Controller class - */ -export function getControllerSpec(constructor: Function): ControllerSpec { - let spec = Reflector.getOwnMetadata(API_SPEC_KEY, constructor); - if (!spec) { - spec = resolveControllerSpec(constructor, spec); - Reflector.defineMetadata(API_SPEC_KEY, spec, constructor); - } - return spec; -} - -/** - * Expose a Controller method as a REST API operation - * mapped to `GET` request method. - * - * @param path The URL path of this operation, e.g. `/product/{id}` - * @param spec The OpenAPI specification describing parameters and responses - * of this operation. - */ -export function get(path: string, spec?: OperationObject) { - return operation('get', path, spec); -} - -/** - * Expose a Controller method as a REST API operation - * mapped to `POST` request method. - * - * @param path The URL path of this operation, e.g. `/product/{id}` - * @param spec The OpenAPI specification describing parameters and responses - * of this operation. - */ -export function post(path: string, spec?: OperationObject) { - return operation('post', path, spec); -} - -/** - * Expose a Controller method as a REST API operation - * mapped to `PUT` request method. - * - * @param path The URL path of this operation, e.g. `/product/{id}` - * @param spec The OpenAPI specification describing parameters and responses - * of this operation. - */ -export function put(path: string, spec?: OperationObject) { - return operation('put', path, spec); -} - -/** - * Expose a Controller method as a REST API operation - * mapped to `PATCH` request method. - * - * @param path The URL path of this operation, e.g. `/product/{id}` - * @param spec The OpenAPI specification describing parameters and responses - * of this operation. - */ -export function patch(path: string, spec?: OperationObject) { - return operation('patch', path, spec); -} - -/** - * Expose a Controller method as a REST API operation - * mapped to `DELETE` request method. - * - * @param path The URL path of this operation, e.g. `/product/{id}` - * @param spec The OpenAPI specification describing parameters and responses - * of this operation. - */ -export function del(path: string, spec?: OperationObject) { - return operation('delete', path, spec); -} - -/** - * Expose a Controller method as a REST API operation. - * - * @param verb HTTP verb, e.g. `GET` or `POST`. - * @param path The URL path of this operation, e.g. `/product/{id}` - * @param spec The OpenAPI specification describing parameters and responses - * of this operation. - */ -export function operation(verb: string, path: string, spec?: OperationObject) { - return function( - target: any, - propertyKey: string, - descriptor: PropertyDescriptor, - ) { - assert( - typeof target[propertyKey] === 'function', - '@operation decorator can be applied to methods only', - ); - - const endpoints = getEndpoints(target); - let endpoint = endpoints[propertyKey]; - if (!endpoint || endpoint.target !== target) { - // Add the new endpoint metadata for the method - endpoint = {verb, path, spec, target}; - endpoints[propertyKey] = endpoint; - } else { - // Update the endpoint metadata - // It can be created by @param - endpoint.verb = verb; - endpoint.path = path; - endpoint.target = target; - } - - if (!spec) { - // Users can define parameters and responses using decorators - return; - } - - // Decorator are invoked in reverse order of their definition. - // For example, a method decorated with @operation() @param() - // will invoke param() decorator first and operation() second. - // As a result, we need to preserve any partial definitions - // already provided by other decorators. - editOperationSpec(endpoint, overrides => { - const mergedSpec = Object.assign({}, spec, overrides); - - // Merge "responses" definitions - mergedSpec.responses = Object.assign( - {}, - spec.responses, - overrides.responses, - ); - - return mergedSpec; - }); - }; -} - -const paramDecoratorStyle = Symbol('ParamDecoratorStyle'); - -/** - * Describe an input parameter of a Controller method. - * - * `@param` can be applied to method itself or specific parameters. For example, - * ``` - * class MyController { - * @get('/') - * @param(offsetSpec) - * @param(pageSizeSpec) - * list(offset?: number, pageSize?: number) {} - * } - * ``` - * or - * ``` - * class MyController { - * @get('/') - * list( - * @param(offsetSpec) offset?: number, - * @param(pageSizeSpec) pageSize?: number, - * ) {} - * } - * ``` - * Please note mixed usage of `@param` at method/parameter level is not allowed. - * - * @param paramSpec Parameter specification. - */ -export function param(paramSpec: ParameterObject) { - return function( - target: any, - propertyKey: string, - descriptorOrParameterIndex: PropertyDescriptor | number, - ) { - assert( - typeof target[propertyKey] === 'function', - '@param decorator can be applied to methods only', - ); - - const endpoints = getEndpoints(target); - let endpoint = endpoints[propertyKey]; - if (!endpoint || endpoint.target !== target) { - const baseEndpoint = endpoint; - // Add the new endpoint metadata for the method - endpoint = cloneDeep(baseEndpoint); - endpoint.target = target; - endpoints[propertyKey] = endpoint; - } - - editOperationSpec(endpoint, operationSpec => { - let decoratorStyle; - if (typeof descriptorOrParameterIndex === 'number') { - decoratorStyle = 'parameter'; - } else { - decoratorStyle = 'method'; - } - if (!operationSpec.parameters) { - operationSpec.parameters = []; - // Record the @param decorator style to ensure consistency - operationSpec[paramDecoratorStyle] = decoratorStyle; - } else { - // Mixed usage of @param at method/parameter level is not allowed - if (operationSpec[paramDecoratorStyle] !== decoratorStyle) { - throw new Error( - 'Mixed usage of @param at method/parameter level' + - ' is not allowed.', - ); - } - } - - if (typeof descriptorOrParameterIndex === 'number') { - operationSpec.parameters[descriptorOrParameterIndex] = paramSpec; - } else { - operationSpec.parameters.unshift(paramSpec); - } - - return operationSpec; - }); - }; -} - -function editOperationSpec( - endpoint: Partial, - updateFn: (spec: OperationObject) => OperationObject, -) { - let spec = endpoint.spec; - if (!spec) { - spec = { - responses: {}, - }; - } - - spec = updateFn(spec); - endpoint.spec = spec; -} - -export namespace param { - export const query = { - /** - * Define a parameter of "string" type that's read from the query string. - * - * @param name Parameter name. - */ - string: createParamShortcut('query', 'string'), - - /** - * Define a parameter of "number" type that's read from the query string. - * - * @param name Parameter name. - */ - number: createParamShortcut('query', 'number'), - - /** - * Define a parameter of "integer" type that's read from the query string. - * - * @param name Parameter name. - */ - integer: createParamShortcut('query', 'integer'), - - /** - * Define a parameter of "boolean" type that's read from the query string. - * - * @param name Parameter name. - */ - boolean: createParamShortcut('query', 'boolean'), - }; - - export const header = { - /** - * Define a parameter of "string" type that's read from a request header. - * - * @param name Parameter name, it must match the header name - * (e.g. `Content-Type`). - */ - string: createParamShortcut('header', 'string'), - - /** - * Define a parameter of "number" type that's read from a request header. - * - * @param name Parameter name, it must match the header name - * (e.g. `Content-Length`). - */ - number: createParamShortcut('header', 'number'), - - /** - * Define a parameter of "integer" type that's read from a request header. - * - * @param name Parameter name, it must match the header name - * (e.g. `Content-Length`). - */ - integer: createParamShortcut('header', 'integer'), - - /** - * Define a parameter of "boolean" type that's read from a request header. - * - * @param name Parameter name, it must match the header name, - * (e.g. `DNT` or `X-Do-Not-Track`). - */ - boolean: createParamShortcut('header', 'boolean'), - }; - - export const path = { - /** - * Define a parameter of "string" type that's read from request path. - * - * @param name Parameter name matching one of the placeholders in the path - * string. - */ - string: createParamShortcut('path', 'string'), - - /** - * Define a parameter of "number" type that's read from request path. - * - * @param name Parameter name matching one of the placeholders in the path - * string. - */ - number: createParamShortcut('path', 'number'), - - /** - * Define a parameter of "integer" type that's read from request path. - * - * @param name Parameter name matching one of the placeholders in the path - * string. - */ - integer: createParamShortcut('path', 'integer'), - - /** - * Define a parameter of "boolean" type that's read from request path. - * - * @param name Parameter name matching one of the placeholders in the path - * string. - */ - boolean: createParamShortcut('path', 'boolean'), - }; - - export const formData = { - /** - * Define a parameter of "string" type that's read - * from a field in the request body. - * - * @param name Parameter name. - */ - string: createParamShortcut('formData', 'string'), - - /** - * Define a parameter of "number" type that's read - * from a field in the request body. - * - * @param name Parameter name. - */ - number: createParamShortcut('formData', 'number'), - - /** - * Define a parameter of "integer" type that's read - * from a field in the request body. - * - * @param name Parameter name. - */ - integer: createParamShortcut('formData', 'integer'), - - /** - * Define a parameter of "boolean" type that's read - * from a field in the request body. - * - * @param name Parameter name. - */ - boolean: createParamShortcut('formData', 'boolean'), - }; - - /** - * Define a parameter that's set to the full request body. - * - * @param name Parameter name - * @param schema The schema defining the type used for the body parameter. - */ - export const body = function(name: string, schema: SchemaObject) { - return param({name, in: 'body', schema}); - }; -} - -function createParamShortcut(source: ParameterLocation, type: ParameterType) { - // TODO(bajtos) @param.IN.TYPE('foo', {required: true}) - return (name: string) => { - return param({name, in: source, type}); - }; -} diff --git a/packages/rest/src/router/routing-table.ts b/packages/rest/src/router/routing-table.ts index 846495e27b6c..1c608a2de87d 100644 --- a/packages/rest/src/router/routing-table.ts +++ b/packages/rest/src/router/routing-table.ts @@ -19,7 +19,8 @@ import { OperationRetval, } from '../internal-types'; -import {ControllerSpec} from './metadata'; +import {ControllerSpec} from '@loopback/openapi-spec'; +// import {ControllerSpec} from './metadata'; import * as assert from 'assert'; import * as url from 'url'; diff --git a/packages/rest/test/unit/router/metadata.test.ts b/packages/rest/test/unit/router/metadata.test.ts deleted file mode 100644 index c00653602e08..000000000000 --- a/packages/rest/test/unit/router/metadata.test.ts +++ /dev/null @@ -1,357 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import { - get, - api, - getControllerSpec, - operation, - post, - put, - patch, - del, - param, -} from '../../..'; -import {expect} from '@loopback/testlab'; -import {anOpenApiSpec, anOperationSpec} from '@loopback/openapi-spec-builder'; - -describe('Routing metadata', () => { - it('returns spec defined via @api()', () => { - const expectedSpec = anOpenApiSpec() - .withOperationReturningString('get', '/greet', 'greet') - .build(); - - @api(expectedSpec) - class MyController { - greet() { - return 'Hello world!'; - } - } - - const actualSpec = getControllerSpec(MyController); - expect(actualSpec).to.eql(expectedSpec); - }); - - it('returns spec defined via @get decorator', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class MyController { - @get('/greet', operationSpec) - greet() { - return 'Hello world!'; - } - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec).to.eql({ - paths: { - '/greet': { - get: { - 'x-operation-name': 'greet', - ...operationSpec, - }, - }, - }, - }); - }); - - it('returns spec defined via @post decorator', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class MyController { - @post('/greeting', operationSpec) - createGreeting() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec).to.eql({ - paths: { - '/greeting': { - post: { - 'x-operation-name': 'createGreeting', - ...operationSpec, - }, - }, - }, - }); - }); - - it('returns spec defined via @put decorator', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class MyController { - @put('/greeting', operationSpec) - updateGreeting() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec).to.eql({ - paths: { - '/greeting': { - put: { - 'x-operation-name': 'updateGreeting', - ...operationSpec, - }, - }, - }, - }); - }); - - it('returns spec defined via @patch decorator', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class MyController { - @patch('/greeting', operationSpec) - patchGreeting() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec).to.eql({ - paths: { - '/greeting': { - patch: { - 'x-operation-name': 'patchGreeting', - ...operationSpec, - }, - }, - }, - }); - }); - - it('returns spec defined via @del decorator', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class MyController { - @del('/greeting', operationSpec) - deleteGreeting() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec).to.eql({ - paths: { - '/greeting': { - delete: { - 'x-operation-name': 'deleteGreeting', - ...operationSpec, - }, - }, - }, - }); - }); - - it('returns spec defined via @operation decorator', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class MyController { - @operation('post', '/greeting', operationSpec) - createGreeting() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec).to.eql({ - paths: { - '/greeting': { - post: { - 'x-operation-name': 'createGreeting', - ...operationSpec, - }, - }, - }, - }); - }); - - it('returns default spec for @get with no spec', () => { - class MyController { - @get('/greet') - greet() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get']).to.eql({ - 'x-operation-name': 'greet', - responses: {}, - }); - }); - - it('returns default spec for @operation with no spec', () => { - class MyController { - @operation('post', '/greeting') - createGreeting() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greeting']['post']).to.eql({ - 'x-operation-name': 'createGreeting', - responses: {}, - }); - }); - - it('honours specifications from inherited methods', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class Parent { - @get('/parent', operationSpec) - getParentName() { - return 'The Parent'; - } - } - - class Child extends Parent { - @get('/child', operationSpec) - getChildName() { - return 'The Child'; - } - } - - const actualSpec = getControllerSpec(Child); - - expect(actualSpec).to.eql({ - paths: { - '/parent': { - get: { - 'x-operation-name': 'getParentName', - ...operationSpec, - }, - }, - '/child': { - get: { - 'x-operation-name': 'getChildName', - ...operationSpec, - }, - }, - }, - }); - }); - - it('allows children to override parent REST endpoints', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class Parent { - @get('/name', operationSpec) - getParentName() { - return 'The Parent'; - } - } - - class Child extends Parent { - @get('/name', operationSpec) - getChildName() { - return 'The Child'; - } - } - - const actualSpec = getControllerSpec(Child); - - expect(actualSpec.paths['/name']['get']).to.have.property( - 'x-operation-name', - 'getChildName', - ); - }); - - it('allows children to override parent REST operations', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class Parent { - @get('/parent-name', operationSpec) - getName() { - return 'The Parent'; - } - } - - class Child extends Parent { - @get('/child-name', operationSpec) - getName() { - return 'The Child'; - } - } - - const childSpec = getControllerSpec(Child); - const parentSpec = getControllerSpec(Parent); - - expect(childSpec.paths['/child-name']['get']).to.have.property( - 'x-operation-name', - 'getName', - ); - - // The parent endpoint has been overridden - expect(childSpec.paths).to.not.have.property('/parent-name'); - - expect(parentSpec.paths['/parent-name']['get']).to.have.property( - 'x-operation-name', - 'getName', - ); - - // The parent endpoint should not be polluted - expect(parentSpec.paths).to.not.have.property('/child-name'); - }); - - it('allows children to override parent REST parameters', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class Parent { - @get('/greet', operationSpec) - greet(@param.query.string('msg') msg: string) { - return `Parent: ${msg}`; - } - } - - class Child extends Parent { - greet(@param.query.string('message') msg: string) { - return `Child: ${msg}`; - } - } - - const childSpec = getControllerSpec(Child); - const parentSpec = getControllerSpec(Parent); - - const childGreet = childSpec.paths['/greet']['get']; - expect(childGreet).to.have.property('x-operation-name', 'greet'); - - expect(childGreet.parameters).to.have.property('length', 1); - - expect(childGreet.parameters[0]).to.containEql({ - name: 'message', - in: 'query', - }); - - const parentGreet = parentSpec.paths['/greet']['get']; - expect(parentGreet).to.have.property('x-operation-name', 'greet'); - - expect(parentGreet.parameters).to.have.property('length', 1); - - expect(parentGreet.parameters[0]).to.containEql({ - name: 'msg', - in: 'query', - }); - }); -}); diff --git a/packages/rest/test/unit/router/metadata/param-body.test.ts b/packages/rest/test/unit/router/metadata/param-body.test.ts deleted file mode 100644 index 82c0c85d98e4..000000000000 --- a/packages/rest/test/unit/router/metadata/param-body.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {post, param, getControllerSpec} from '../../../..'; -import {expect} from '@loopback/testlab'; - -describe('Routing metadata for parameters', () => { - describe('@param.body', () => { - it('defines a parameter with in:body', () => { - class MyController { - @post('/greeting') - @param.body('data', {type: 'object'}) - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ - { - name: 'data', - in: 'body', - schema: {type: 'object'}, - }, - ]); - }); - }); -}); diff --git a/packages/rest/test/unit/router/metadata/param-form-data.test.ts b/packages/rest/test/unit/router/metadata/param-form-data.test.ts deleted file mode 100644 index 0cd239204ce3..000000000000 --- a/packages/rest/test/unit/router/metadata/param-form-data.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {post, param, getControllerSpec} from '../../../..'; -import {expect} from '@loopback/testlab'; - -describe('Routing metadata for parameters', () => { - describe('@param.formData.string', () => { - it('defines a parameter with in:formData type:string', () => { - class MyController { - @post('/greeting') - @param.formData.string('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ - { - name: 'name', - type: 'string', - in: 'formData', - }, - ]); - }); - }); - - describe('@param.formData.number', () => { - it('defines a parameter with in:formData type:number', () => { - class MyController { - @post('/greeting') - @param.formData.number('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ - { - name: 'name', - type: 'number', - in: 'formData', - }, - ]); - }); - }); - - describe('@param.formData.integer', () => { - it('defines a parameter with in:formData type:integer', () => { - class MyController { - @post('/greeting') - @param.formData.integer('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ - { - name: 'name', - type: 'integer', - in: 'formData', - }, - ]); - }); - }); - - describe('@param.formData.boolean', () => { - it('defines a parameter with in:formData type:boolean', () => { - class MyController { - @post('/greeting') - @param.formData.boolean('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ - { - name: 'name', - type: 'boolean', - in: 'formData', - }, - ]); - }); - }); -}); diff --git a/packages/rest/test/unit/router/metadata/param-header.test.ts b/packages/rest/test/unit/router/metadata/param-header.test.ts deleted file mode 100644 index e993d56c23f9..000000000000 --- a/packages/rest/test/unit/router/metadata/param-header.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {get, param, getControllerSpec} from '../../../..'; -import {expect} from '@loopback/testlab'; - -describe('Routing metadata for parameters', () => { - describe('@param.header.string', () => { - it('defines a parameter with in:header type:string', () => { - class MyController { - @get('/greet') - @param.header.string('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'string', - in: 'header', - }, - ]); - }); - }); - - describe('@param.header.number', () => { - it('defines a parameter with in:header type:number', () => { - class MyController { - @get('/greet') - @param.header.number('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'number', - in: 'header', - }, - ]); - }); - }); - - describe('@param.header.integer', () => { - it('defines a parameter with in:header type:integer', () => { - class MyController { - @get('/greet') - @param.header.integer('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'integer', - in: 'header', - }, - ]); - }); - }); - - describe('@param.header.boolean', () => { - it('defines a parameter with in:header type:boolean', () => { - class MyController { - @get('/greet') - @param.header.boolean('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'boolean', - in: 'header', - }, - ]); - }); - }); -}); diff --git a/packages/rest/test/unit/router/metadata/param-path.test.ts b/packages/rest/test/unit/router/metadata/param-path.test.ts deleted file mode 100644 index 4db8bb8de30a..000000000000 --- a/packages/rest/test/unit/router/metadata/param-path.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {get, param, getControllerSpec} from '../../../..'; -import {expect} from '@loopback/testlab'; - -describe('Routing metadata for parameters', () => { - describe('@param.path.string', () => { - it('defines a parameter with in:path type:string', () => { - class MyController { - @get('/greet/{name}') - @param.path.string('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet/{name}']['get'].parameters).to.eql([ - { - name: 'name', - type: 'string', - in: 'path', - }, - ]); - }); - }); - - describe('@param.path.number', () => { - it('defines a parameter with in:path type:number', () => { - class MyController { - @get('/greet/{name}') - @param.path.number('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet/{name}']['get'].parameters).to.eql([ - { - name: 'name', - type: 'number', - in: 'path', - }, - ]); - }); - }); - - describe('@param.path.integer', () => { - it('defines a parameter with in:path type:integer', () => { - class MyController { - @get('/greet/{name}') - @param.path.integer('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet/{name}']['get'].parameters).to.eql([ - { - name: 'name', - type: 'integer', - in: 'path', - }, - ]); - }); - }); - - describe('@param.path.boolean', () => { - it('defines a parameter with in:path type:boolean', () => { - class MyController { - @get('/greet/{name}') - @param.path.boolean('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet/{name}']['get'].parameters).to.eql([ - { - name: 'name', - type: 'boolean', - in: 'path', - }, - ]); - }); - }); -}); diff --git a/packages/rest/test/unit/router/metadata/param-query.test.ts b/packages/rest/test/unit/router/metadata/param-query.test.ts deleted file mode 100644 index 8a7ff881f0dc..000000000000 --- a/packages/rest/test/unit/router/metadata/param-query.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {get, param, getControllerSpec} from '../../../..'; -import {expect} from '@loopback/testlab'; - -describe('Routing metadata for parameters', () => { - describe('@param.query.string', () => { - it('defines a parameter with in:query type:string', () => { - class MyController { - @get('/greet') - @param.query.string('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'string', - in: 'query', - }, - ]); - }); - }); - - describe('@param.query.number', () => { - it('defines a parameter with in:query type:number', () => { - class MyController { - @get('/greet') - @param.query.number('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'number', - in: 'query', - }, - ]); - }); - }); - - describe('@param.query.integer', () => { - it('defines a parameter with in:query type:integer', () => { - class MyController { - @get('/greet') - @param.query.integer('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'integer', - in: 'query', - }, - ]); - }); - }); - - describe('@param.query.boolean', () => { - it('defines a parameter with in:query type:boolean', () => { - class MyController { - @get('/greet') - @param.query.boolean('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'boolean', - in: 'query', - }, - ]); - }); - }); -}); diff --git a/packages/rest/test/unit/router/metadata/param.test.ts b/packages/rest/test/unit/router/metadata/param.test.ts deleted file mode 100644 index 61d54377852d..000000000000 --- a/packages/rest/test/unit/router/metadata/param.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {get, param, getControllerSpec, operation} from '../../../..'; -import { - OperationObject, - ParameterObject, - ResponsesObject, -} from '@loopback/openapi-spec'; -import {expect} from '@loopback/testlab'; -import {anOperationSpec} from '@loopback/openapi-spec-builder'; - -describe('Routing metadata for parameters', () => { - describe('@param', () => { - it('defines a new parameter', () => { - const paramSpec: ParameterObject = { - name: 'name', - type: 'string', - in: 'query', - }; - - class MyController { - @get('/greet') - @param(paramSpec) - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - const expectedSpec = anOperationSpec() - .withOperationName('greet') - .withParameter(paramSpec) - .build(); - - expect(actualSpec.paths['/greet']['get']).to.eql(expectedSpec); - }); - - it('can define multiple parameters in order', () => { - const offsetSpec: ParameterObject = { - name: 'offset', - type: 'number', - in: 'query', - }; - - const pageSizeSpec: ParameterObject = { - name: 'pageSize', - type: 'number', - in: 'query', - }; - - class MyController { - @get('/') - @param(offsetSpec) - @param(pageSizeSpec) - list(offset?: number, pageSize?: number) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/']['get'].parameters).to.eql([ - offsetSpec, - pageSizeSpec, - ]); - }); - - it('can define multiple parameters by arguments', () => { - const offsetSpec: ParameterObject = { - name: 'offset', - type: 'number', - in: 'query', - }; - - const pageSizeSpec: ParameterObject = { - name: 'pageSize', - type: 'number', - in: 'query', - }; - - class MyController { - @get('/') - list( - @param(offsetSpec) offset?: number, - @param(pageSizeSpec) pageSize?: number, - ) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/']['get'].parameters).to.eql([ - offsetSpec, - pageSizeSpec, - ]); - }); - // tslint:disable-next-line:max-line-length - it('throws an error if @param is used at both method and parameter level', () => { - expect(() => { - const offsetSpec: ParameterObject = { - name: 'offset', - type: 'number', - in: 'query', - }; - - const pageSizeSpec: ParameterObject = { - name: 'pageSize', - type: 'number', - in: 'query', - }; - // tslint:disable-next-line:no-unused-variable - class MyController { - @get('/') - @param(offsetSpec) - list(offset?: number, @param(pageSizeSpec) pageSize?: number) {} - } - }).to.throw( - /Mixed usage of @param at method\/parameter level is not allowed/, - ); - }); - - it('adds to existing spec provided via @operation', () => { - const offsetSpec: ParameterObject = { - name: 'offset', - type: 'number', - in: 'query', - }; - - const pageSizeSpec: ParameterObject = { - name: 'pageSize', - type: 'number', - in: 'query', - }; - - const responses: ResponsesObject = { - 200: { - schema: { - type: 'string', - }, - description: 'a string response', - }, - }; - - class MyController { - @operation('get', '/', {responses}) - @param(offsetSpec) - @param(pageSizeSpec) - list(offset?: number, pageSize?: number) {} - } - - const apiSpec = getControllerSpec(MyController); - const opSpec: OperationObject = apiSpec.paths['/']['get']; - - expect(opSpec.responses).to.eql(responses); - expect(opSpec.parameters).to.eql([offsetSpec, pageSizeSpec]); - }); - }); -}); From 846e791e3a6f20723ee5c479832663a458a068f2 Mon Sep 17 00:00:00 2001 From: jannyHou Date: Thu, 14 Dec 2017 17:09:18 -0500 Subject: [PATCH 3/3] Upgrade to v3 --- .../controller-decorators/metadata.ts | 2 +- packages/openapi-spec/src/index.ts | 2 +- packages/openapi-spec/src/openapi-spec-v2.ts | 2061 ++++++++--------- packages/openapi-spec/src/openapi-spec-v3.ts | 157 ++ .../integration/rest-server.integration.ts | 2 +- .../rest-server.open-api-spec.test.ts | 4 +- packages/testlab/src/validate-api-spec.ts | 4 +- 7 files changed, 1184 insertions(+), 1048 deletions(-) diff --git a/packages/openapi-spec/src/decorators/controller-decorators/metadata.ts b/packages/openapi-spec/src/decorators/controller-decorators/metadata.ts index 40226f76caf2..392ba7b950ac 100644 --- a/packages/openapi-spec/src/decorators/controller-decorators/metadata.ts +++ b/packages/openapi-spec/src/decorators/controller-decorators/metadata.ts @@ -14,7 +14,7 @@ import { SchemaObject, ParameterType, PathsObject, -} from '../../openapi-spec-v2'; +} from '../../openapi-spec-v3'; const debug = require('debug')('loopback:core:router:metadata'); diff --git a/packages/openapi-spec/src/index.ts b/packages/openapi-spec/src/index.ts index 4d0977c04cb4..79b56bd6085c 100644 --- a/packages/openapi-spec/src/index.ts +++ b/packages/openapi-spec/src/index.ts @@ -3,5 +3,5 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -export * from './openapi-spec-v2'; +export * from './openapi-spec-v3'; export * from './decorators/controller-decorators/metadata'; diff --git a/packages/openapi-spec/src/openapi-spec-v2.ts b/packages/openapi-spec/src/openapi-spec-v2.ts index 1bf1e09ed6fc..95668716bc41 100644 --- a/packages/openapi-spec/src/openapi-spec-v2.ts +++ b/packages/openapi-spec/src/openapi-spec-v2.ts @@ -1,1041 +1,1020 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/openapi-spec -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -/* - * OpenApiSpec - A typescript representation of OpenApi/Swagger Spec 2.0 - */ - -// tslint:disable:max-line-length - -/** - * Custom extensions can use arbitrary type as the value, - * e.g. a string, an object or an array. - */ -// tslint:disable-next-line:no-any -export type ExtensionValue = any; - -/** - * While the Swagger Specification tries to accommodate most use cases, - * additional data can be added to extend the specification at certain points. - * - * The extensions properties are always prefixed by "x-" and can have any valid - * JSON format value. - * - * The extensions may or may not be supported by the available tooling, but - * those may be extended as well to add requested support (if tools are internal - * or open-sourced). - */ -export interface Extensible { - [extension: string]: ExtensionValue; -} - -/** - * This is the root document object for the API specification. - *

Specification: - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object - */ -export interface OpenApiSpec extends Extensible { - /** - * Specifies the Swagger Specification version being used. - * It can be used by the Swagger UI and other clients to interpret - * the API listing. The value MUST be "2.0". - */ - swagger: '2.0'; - - /** - * Provides metadata about the API. - * The metadata can be used by the clients if needed. - */ - info: InfoObject; - - /** - * The host (name or ip) serving the API. - * This MUST be the host only and does not include the scheme nor sub-paths. - * It MAY include a port. If the host is not included, - * the host serving the documentation is to be used (including the port). - * The host does not support - * [path templating](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathTemplating). - */ - host?: string; - - /** - * The base path on which the API is served, which is relative to the host. - * If it is not included, the API is served directly under the host. - * The value MUST start with a leading slash (/). - * The basePath does not support - * [path templating](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathTemplating). - */ - basePath: string; - - /** - * The transfer protocol of the API. Values MUST be from the list: "http", - * "https", "ws", "wss". If the schemes is not included, the default scheme - * to be used is the one used to access the Swagger definition itself. - */ - schemes?: Array; - - /** - * A list of MIME types the APIs can consume. This is global to all APIs but - * can be overridden on specific API calls. Value MUST be as described under - * Mime Types. - */ - consumes?: Array; - - /** - * A list of MIME types the APIs can produce. This is global to all APIs but - * can be overridden on specific API calls. Value MUST be as described under - * Mime Types. - */ - produces?: Array; - - /** - * The available paths and operations for the API. - */ - paths: PathsObject; - - /** - * An object to hold parameters that can be used across operations. This - * property does not define global parameters for all operations. - */ - parameters?: ParametersDefinitionsObject; - - /** - * An object to hold responses that can be used across operations. This - * property does not define global responses for all operations. - */ - responses?: ResponsesDefinitionsObject; - - /** - * An object to hold data types produced and consumed by operations. - */ - definitions?: DefinitionsObject; - - /** - * Security scheme definitions that can be used across the specification. - */ - securityDefinitions?: SecurityDefinitionsObject; - - /** - * A declaration of which security schemes are applied for the API as a whole. - * The list of values describes alternative security schemes that can be used - * (that is, there is a logical OR between the security requirements). - * Individual operations can override this definition. - */ - security?: Array; - - /** - * A list of tags used by the specification with additional metadata. The - * order of the tags can be used to reflect on their order by the parsing - * tools. Not all tags that are used by the Operation Object must be declared. - * The tags that are not declared may be organized randomly or based on the - * tools' logic. Each tag name in the list MUST be unique. - */ - tags?: Array; - - /** - * Additional external documentation. - */ - externalDocs?: ExternalDocumentationObject; -} - -/** - * The object provides metadata about the API. - * The metadata can be used by the clients if needed, - * and can be presented in the Swagger-UI for convenience. - *

Specification: - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#infoObject - */ -export interface InfoObject extends Extensible { - /** - * The title of the application. - */ - title: string; - - /** - * A short description of the application. - * [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) - * can be used for rich text representation. - */ - description?: string; - - /** - * The Terms of Service for the API. - */ - termsOfService?: string; - - /** - * The contact information for the exposed API. - */ - contact?: ContactObject; - - /** - * The license information for the exposed API. - */ - license?: LicenseObject; - - /** - * Provides the version of the application API - * (not to be confused with the specification version). - */ - version: string; -} - -/** - * Contact information for the exposed API. - */ -export interface ContactObject extends Extensible { - /** - * The identifying name of the contact person/organization. - */ - name?: string; - - /** - *The URL pointing to the contact information. MUST be in the format of a URL. - */ - url?: string; - - /** - * The email address of the contact person/organization. MUST be in the format - * of an email address. - */ - email?: string; -} - -/** - * License information for the exposed API. - */ -export interface LicenseObject extends Extensible { - /** - * The license name used for the API. - */ - name: string; - - /** - * A URL to the license used for the API. MUST be in the format of a URL. - */ - url?: string; -} - -/** - * Allows referencing an external resource for extended documentation. - */ -export interface ExternalDocumentationObject extends Extensible { - /** - * A short description of the target documentation. GFM syntax can be used for - * rich text representation. - */ - description?: string; - - /** - * The URL for the target documentation. Value MUST be in the format of a URL. - */ - url: string; -} - -/** - * Allows adding meta data to a single tag that is used by the Operation Object. - * It is not mandatory to have a Tag Object per tag used there. - */ -export interface TagObject extends Extensible { - /** - * The name of the tag. - */ - name: string; - - /** - * A short description for the tag. GFM syntax can be used for rich text - * representation. - */ - description?: string; - - /** - * Additional external documentation for this tag. - */ - externalDocs?: ExternalDocumentationObject; -} - -/** - * Allows the definition of a security scheme that can be used by the - * operations. Supported schemes are basic authentication, an API key (either - * as a header or as a query parameter) and OAuth2's common flows (implicit, - * password, application and access code). - */ -export interface SecuritySchemeObject extends Extensible { - /** - * The type of the security scheme. Valid values are "basic", "apiKey" or - * "oauth2". - */ - type: 'basic' | 'apiKey' | 'oauth2'; - - /** - * A short description for security scheme. - */ - description?: string; - - /** - * The name of the header or query parameter to be used. - */ - name?: string; - - /** - * The location of the API key. Valid values are "query" or "header". - */ - in?: 'query' | 'header'; - - /** - * The flow used by the OAuth2 security scheme. Valid values are "implicit", - * "password", "application" or "accessCode". - */ - flow?: 'implicit' | 'password' | 'application' | 'accessCode'; - - /** - * ("implicit", "accessCode") Required. The authorization URL to be used for - * this flow. This SHOULD be in the form of a URL. - */ - authorizationUrl?: string; - - /** - * ("password", "application", "accessCode") Required. The token URL to be - * used for this flow. This SHOULD be in the form of a URL. - */ - tokenUrl?: string; - - /** - * The available scopes for the OAuth2 security scheme. - */ - scopes?: ScopesObject; -} - -/** - * Maps names to a given type of values - */ -export interface MapObject { - /** - * Maps between a name and object - */ - [name: string]: T; -} - -/** - * Lists the available scopes for an OAuth2 security scheme. - */ -export interface ScopesObject extends MapObject, Extensible { - /** - * Maps between a name of a scope to a short description of it (as the value - * of the property). - */ - [name: string]: string; -} - -/** - * A declaration of the security schemes available to be used in the - * specification. This does not enforce the security schemes on the operations - * and only serves to provide the relevant details for each scheme. - */ -export interface SecurityDefinitionsObject - extends MapObject { - /** - * A single security scheme definition, mapping a "name" to the scheme it - * defines. - */ - [name: string]: SecuritySchemeObject; -} - -/** - * Lists the required security schemes to execute this operation. The object can - * have multiple security schemes declared in it which are all required (that - * is, there is a logical AND between the schemes). - * The name used for each property MUST correspond to a security scheme declared - * in the Security Definitions. - */ -export interface SecurityRequirementObject extends MapObject> { - /** - * Each name must correspond to a security scheme which is declared in the - * Security Definitions. If the security scheme is of type "oauth2", then the - * value is a list of scope names required for the execution. For other - * security scheme types, the array MUST be empty. - */ - [name: string]: Array; -} - -/** - * An object to hold data types that can be consumed and produced by operations. - * These data types can be primitives, arrays or models. - */ -export interface DefinitionsObject extends MapObject { - /** - * A single definition, mapping a "name" to the schema it defines. - */ - [name: string]: SchemaObject; -} - -/** - * An object to hold parameters to be reused across operations. Parameter - * definitions can be referenced to the ones defined here. - * - * This does not define global operation parameters. - */ -export interface ParametersDefinitionsObject - extends MapObject { - /** - * A single parameter definition, mapping a "name" to the parameter it - * defines. - */ - [name: string]: ParameterObject; -} - -/** - * An object to hold responses to be reused across operations. Response - * definitions can be referenced to the ones defined here. - * - * This does not define global operation responses. - */ -export interface ResponsesDefinitionsObject extends MapObject { - /** - * A single response definition, mapping a "name" to the response it defines. - */ - [name: string]: ResponseObject; -} - -/** - * Holds the relative paths to the individual endpoints. - * The path is appended to the basePath in order to construct the full URL. - * The Paths may be empty, due to ACL constraints. - *

Specification: - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#paths-object - */ -export interface PathsObject - extends MapObject { - [httpPathOrSwaggerExtension: string]: PathItemObject | ExtensionValue; -} - -/** - * Describes the operations available on a single path. - * A Path Item may be empty, due to ACL constraints. - * The path itself is still exposed to the documentation viewer - * but they will not know which operations and parameters are available. - *

Specification: - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathItemObject - */ -export interface PathItemObject extends Extensible { - /** - * Allows for an external definition of this path item. The referenced - * structure MUST be in the format of a Path Item Object. If there are - * conflicts between the referenced definition and this Path Item's - * definition, the behavior is undefined. - */ - $ref?: string; - - /** - * A definition of a GET operation on this path. - */ - get?: OperationObject; - - /** - * A definition of a PUT operation on this path. - */ - put?: OperationObject; - - /** - * A definition of a POST operation on this path. - */ - post?: OperationObject; - - /** - * A definition of a DELETE operation on this path. - */ - delete?: OperationObject; - - /** - * A definition of a OPTIONS operation on this path. - */ - options?: OperationObject; - - /** - * A definition of a HEAD operation on this path. - */ - head?: OperationObject; - - /** - * A definition of a PATCH operation on this path. - */ - patch?: OperationObject; - - /** - * A list of parameters that are applicable for all the operations described - * under this path. These parameters can be overridden at the operation level, - * but cannot be removed there. The list MUST NOT include duplicated - * parameters. A unique parameter is defined by a combination of a name and - * location. The list can use the Reference Object to link to parameters that - * are defined at the Swagger Object's parameters. There can be one "body" - * parameter at most. - */ - parameters?: Array; -} - -/** - * Describes a single API operation on a path. - *

Specification: - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operationObject - */ -export interface OperationObject extends Extensible { - /** - * IBM/LoopBack extension: The name of the controller method implementing - * this operation. - */ - 'x-operation-name'?: string; - - /** - * A list of parameters that are applicable for this operation. - * If a parameter is already defined at the Path Item, - * the new definition will override it, but can never remove it. - * The list MUST NOT include duplicated parameters. - * A unique parameter is defined by a combination of a name and location. - * The list can use the Reference Object to link to parameters that are - * defined at the Swagger Object's parameters. - * There can be one "body" parameter at most. - */ - parameters?: Array; - - /** - * The list of possible responses as they are returned from executing - * this operation. - */ - responses: ResponsesObject; - - /** - * A list of tags for API documentation control. Tags can be used for logical - * grouping of operations by resources or any other qualifier. - */ - tags?: Array; - - /** - * A short summary of what the operation does. For maximum readability in the - * swagger-ui, this field SHOULD be less than 120 characters. - */ - summary?: string; - - /** - * A verbose explanation of the operation behavior. GFM syntax can be used for - * rich text representation. - */ - description?: string; - - /** - * Additional external documentation for this operation. - */ - externalDocs?: ExternalDocumentationObject; - - /** - * Unique string used to identify the operation. The id MUST be unique among - * all operations described in the API. Tools and libraries MAY use the - * operationId to uniquely identify an operation, therefore, it is recommended - * to follow common programming naming conventions. - */ - operationId?: string; - - /** - * A list of MIME types the operation can consume. This overrides the consumes - * definition at the Swagger Object. An empty value MAY be used to clear the - * global definition. Value MUST be as described under Mime Types. - */ - consumes?: Array; - - /** - * A list of MIME types the operation can produce. This overrides the produces - * definition at the Swagger Object. An empty value MAY be used to clear the - * global definition. Value MUST be as described under Mime Types. - */ - produces?: Array; - - /** - * The transfer protocol of the API. Values MUST be from the list: "http", - * "https", "ws", "wss". If the schemes is not included, the default scheme - * to be used is the one used to access the Swagger definition itself. - */ - schemes?: Array; - - /** - * Declares this operation to be deprecated. Usage of the declared operation - * should be refrained. Default value is false. - */ - deprecated?: boolean; - - /** - * A declaration of which security schemes are applied for this operation. - * The list of values describes alternative security schemes that can be used - * (that is, there is a logical OR between the security requirements). This - * definition overrides any declared top-level security. To remove a top-level - * security declaration, an empty array can be used. - */ - security?: SecurityRequirementObject; -} - -export type ParameterType = - | 'string' - | 'number' - | 'integer' - | 'boolean' - | 'array' - | 'file'; - -/** - * Simple type - primitive types or array of such types. It is used by parameter - * definitions that are not located in "body". - */ -export interface SimpleType { - /** - * The type of the parameter. Since the parameter is not located at - * the request body, it is limited to simple types (that is, not an object). - * The value MUST be one of "string", "number", "integer", "boolean", - * "array" or "file". If type is "file", the `consumes` MUST be either - * "multipart/form-data", " application/x-www-form-urlencoded" or both - * and the parameter MUST be `in` "formData". - */ - type?: ParameterType; - - /** - * The extending format for the previously mentioned type. See - * [Data Type Formats](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#dataTypeFormat) - * for further details. - */ - format?: string; - - /** - * Sets the ability to pass empty-valued parameters. This is valid only for - * either query or formData parameters and allows you to send a parameter - * with a name only or an empty value. Default value is false. - */ - allowEmptyValue?: boolean; - - /** - * Required if type is "array". Describes the type of items in the array. - */ - items?: ItemsObject; - - /** - * Determines the format of the array if type array is used. Possible values - * are: - * - csv: comma separated values foo,bar. - * - ssv: space separated values foo bar. - * - tsv: tab separated values foo\tbar. - * - pipes: pipe separated values foo|bar. - * - multi: corresponds to multiple parameter instances instead of multiple - * values for a single instance foo=bar&foo=baz. This is valid only for - * parameters in "query" or "formData". - * - * Default value is csv. - */ - collectionFormat?: string; - - /** - * Declares the value of the parameter that the server will use if none is - * provided, for example a "count" to control the number of results per page - * might default to 100 if not supplied by the client in the request. (Note: - * "default" has no meaning for required parameters.) See - * https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-6.2. - * Unlike JSON Schema this value MUST conform to the defined type for this - * parameter. - */ - default?: ExtensionValue; - - /** - * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.2. - */ - maximum?: number; - - /** - * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.2. - */ - exclusiveMaximum?: number; - - /** - * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.3. - */ - minimum?: number; - - /** - * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.3. - */ - exclusiveMinimum?: number; - - /** - * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.1. - */ - maxLength?: number; - - /** - * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.2. - */ - minLength?: number; - - /** - * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.3. - */ - pattern?: string; - - /** - * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.2. - */ - maxItems?: number; - - /** - * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.3. - */ - minItems?: number; - - /** - * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.4. - */ - uniqueItems?: boolean; - - /** - * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.1. - */ - enum?: Array; - - /** - * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.1. - */ - multipleOf?: number; -} - -/** - * The internal type of the array. The value MUST be one of "string", - * "number", "integer", "boolean", or "array". Files and models are not - * allowed. - */ -export type ItemType = 'string' | 'number' | 'integer' | 'boolean' | 'array'; - -/** - * A limited subset of JSON-Schema's items object. It is used by parameter - * definitions that are not located in "body". Please note it only differs - * from SimpleType with parameter types excluding `file`. - */ -export interface ItemsObject extends SimpleType { - type: ItemType; -} - -/** - * Describes a single operation parameter. - *

Specification: - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameterObject - */ -export interface ParameterObject extends SimpleType, Extensible { - /** - * The name of the parameter. Parameter names are case sensitive. - * - If `in` is "path", the `name` field MUST correspond to the associated - * path segment from the `path` field in the Paths Object. - * See [Path Templating](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathTemplating) - * for further information. - * - For all other cases, the `name` corresponds to the parameter name used - * based on the `in` property. - */ - name: string; - - /** - * The location of the parameter. - * Possible values are "query", "header", "path", "formData" or "body". - */ - in: ParameterLocation; - - /** - * A brief description of the parameter. This could contain examples of use. - * [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) - * can be used for rich text representation. - */ - description?: string; - - /** - * Determines whether this parameter is mandatory. - * If the parameter is `in` "path", this property is required and - * its value MUST be `true`. Otherwise, the property MAY be included - * and its default value is `false`. - */ - required?: boolean; - - /** - * _If `in` is any value other than "body":_, the valid properties are - * inherited from `SimpleType`. - */ - - /** - * _If in is "body":_ - * The schema defining the type used for the body parameter. - */ - schema?: SchemaObject; -} - -/** - * The location of a parameter. - * Possible values are "query", "header", "path", "formData" or "body". - *

Specification: - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameterIn - */ -export type ParameterLocation = - | 'query' - | 'header' - | 'path' - | 'formData' - | 'body'; - -/** - * A simple object to allow referencing other definitions in the specification. - * It can be used to reference parameters and responses that are defined - * at the top level for reuse. - * The Reference Object is a [JSON Reference](http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-02) - * that uses a [JSON Pointer](https://tools.ietf.org/html/rfc6901) as its value. - * For this specification, only canonical dereferencing is supported. - *

Specification: - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#referenceObject - *

**Example** - * ```json - * { - * "$ref": "#/definitions/Pet" - * } - * ``` - */ -export interface ReferenceObject { - /** - * The reference string. - */ - $ref: string; -} - -/** - * A container for the expected responses of an operation. - * The container maps a HTTP response code to the expected response. - * It is not expected from the documentation to necessarily cover all - * possible HTTP response codes, since they may not be known in advance. - * However, it is expected from the documentation to cover a successful - * operation response and any known errors. - *

The `default` can be used as the default response object for all - * HTTP codes that are not covered individually by the specification. - *

The `ResponsesObject` MUST contain at least one response code, - * and it SHOULD be the response for a successful operation call. - *

Specification: - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#responsesObject - */ -export interface ResponsesObject - extends MapObject, - Extensible { - /** - * The documentation of responses other than the ones declared for specific - * HTTP response codes. It can be used to cover undeclared responses. - * Reference Object can be used to link to a response that is defined at - * the Swagger Object's responses section. - */ - default?: ResponseObject | ReferenceObject; -} - -/** - * Describes a single response from an API Operation. - *

Specification: - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#responseObject - */ -export interface ResponseObject extends Extensible { - /** - * A short description of the response. - * [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) - * can be used for rich text representation. - */ - description: string; - - /** - * A definition of the response structure. - * It can be a primitive, an array or an object. - * If this field does not exist, it means no content is returned - * as part of the response. As an extension to the `SchemaObject`, - * its root type value may also be "file". - * This SHOULD be accompanied by a relevant `produces` mime-type. - */ - schema: SchemaObject; - - /** - * A list of headers that are sent with the response. - */ - headers?: HeadersObject; - - /** - * An example of the response message. - */ - examples?: ExampleObject; -} - -/** - * Allows sharing examples for operation responses. - */ -export interface ExampleObject extends MapObject { - /** - * The name of the property MUST be one of the Operation produces values - * (either implicit or inherited). The value SHOULD be an example of what - * such a response would look like. - */ - [mimeType: string]: ExtensionValue; -} - -/** - * Lists the headers that can be sent as part of a response. - */ -export interface HeadersObject extends MapObject { - /** - * The name of the property corresponds to the name of the header. The value - * describes the type of the header. - */ - [name: string]: HeaderObject; -} - -export interface HeaderObject extends ItemsObject, Extensible { - /** - * A short description of the header. - */ - description: string; -} - -/** - * The Schema Object allows the definition of input and output data types. - * These types can be objects, but also primitives and arrays. - * This object is based on the [JSON Schema Specification Draft 4](http://json-schema.org/) - * and uses a predefined subset of it. On top of this subset, there are - * extensions provided by this specification to allow for more complete documentation. - *

Specification: - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject - */ -export interface SchemaObject extends Extensible { - /** - * The following properties are taken directly from the JSON Schema - * definition and follow the same specifications: - */ - $ref?: string; - format?: string; - title?: string; - description?: string; - default?: ExtensionValue; - multipleOf?: number; - maximum?: number; - exclusiveMaximum?: number; - minimum?: number; - exclusiveMinimum?: number; - maxLength?: number; - minLength?: number; - pattern?: string; - maxItems?: number; - minItems?: number; - uniqueItems?: boolean; - maxProperties?: number; - minProperties?: number; - required?: Array; - enum?: Array; - type?: string; - - /** - * The following properties are taken from the JSON Schema definition but - * their definitions were adjusted to the Swagger Specification. Their - * definition is the same as the one from JSON Schema, only where the original - * definition references the JSON Schema definition, the Schema Object - * definition is used instead. - */ - allOf?: Array; - properties?: MapObject; - additionalProperties?: SchemaObject; - items?: SchemaObject; - - /** - * Other than the JSON Schema subset fields, the following fields may be used - * for further schema documentation. - */ - - /** - * Adds support for polymorphism. The discriminator is the schema property - * name that is used to differentiate between other schema that inherit this - * schema. The property name used MUST be defined at this schema and it MUST - * be in the required property list. When used, the value MUST be the name of - * this schema or any schema that inherits it. - */ - discriminator?: string; - - /** - * Relevant only for Schema "properties" definitions. Declares the property - * as "read only". This means that it MAY be sent as part of a response but - * MUST NOT be sent as part of the request. Properties marked as readOnly - * being true SHOULD NOT be in the required list of the defined schema. - * Default value is false. - */ - readOnly?: boolean; - - /** - * This MAY be used only on properties schemas. It has no effect on root - * schemas. Adds Additional metadata to describe the XML representation format - * of this property. - */ - xml?: XMLObject; - - /** - * Additional external documentation for this schema. - */ - externalDocs?: ExternalDocumentationObject; - - /** - * A free-form property to include an example of an instance for this schema. - */ - example?: ExtensionValue; -} - -/** - * A metadata object that allows for more fine-tuned XML model definitions. - * - * When using arrays, XML element names are not inferred (for singular/plural - * forms) and the name property should be used to add that information. See - * examples for expected behavior. - */ -export interface XMLObject extends Extensible { - /** - * Replaces the name of the element/attribute used for the described schema - * property. When defined within the Items Object (items), it will affect the - * name of the individual XML elements within the list. When defined alongside - * type being array (outside the items), it will affect the wrapping element - * and only if wrapped is true. If wrapped is false, it will be ignored. - */ - name?: string; - - /** - * The URL of the namespace definition. Value SHOULD be in the form of a URL. - */ - namespace?: string; - - /** - * The prefix to be used for the name. - */ - prefix?: string; - - /** - * Declares whether the property definition translates to an attribute - * instead of an element. Default value is false. - */ - attribute?: boolean; - - /** - * MAY be used only for an array definition. Signifies whether the array is - * wrapped (for example, ) or unwrapped - * (). Default value is false. The definition takes effect - * only when defined alongside type being array (outside the items). - */ - wrapped?: boolean; -} - -/** - * Create an empty OpenApiSpec object that's still a valid Swagger document. - */ -export function createEmptyApiSpec(): OpenApiSpec { - return { - swagger: '2.0', - basePath: '/', - info: { - title: 'LoopBack Application', - version: '1.0.0', - }, - paths: {}, - }; -} +// // Copyright IBM Corp. 2013,2017. All Rights Reserved. +// // Node module: @loopback/openapi-spec +// // This file is licensed under the MIT License. +// // License text available at https://opensource.org/licenses/MIT + +// /* +// * OpenApiSpec - A typescript representation of OpenApi/Swagger Spec 2.0 +// */ + +// // tslint:disable:max-line-length + +// /** +// * Custom extensions can use arbitrary type as the value, +// * e.g. a string, an object or an array. +// */ +// // tslint:disable-next-line:no-any +// export type ExtensionValue = any; + +// /** +// * While the Swagger Specification tries to accommodate most use cases, +// * additional data can be added to extend the specification at certain points. +// * +// * The extensions properties are always prefixed by "x-" and can have any valid +// * JSON format value. +// * +// * The extensions may or may not be supported by the available tooling, but +// * those may be extended as well to add requested support (if tools are internal +// * or open-sourced). +// */ +// export interface Extensible { +// [extension: string]: ExtensionValue; +// } + +// /** +// * This is the root document object for the API specification. +// *

Specification: +// * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object +// */ +// export interface OpenApiSpec extends Extensible { +// /** +// * Specifies the Swagger Specification version being used. +// * It can be used by the Swagger UI and other clients to interpret +// * the API listing. The value MUST be "2.0". +// */ +// swagger: '2.0'; + +// /** +// * Provides metadata about the API. +// * The metadata can be used by the clients if needed. +// */ +// info: InfoObject; + +// /** +// * The host (name or ip) serving the API. +// * This MUST be the host only and does not include the scheme nor sub-paths. +// * It MAY include a port. If the host is not included, +// * the host serving the documentation is to be used (including the port). +// * The host does not support +// * [path templating](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathTemplating). +// */ +// host?: string; + +// /** +// * The base path on which the API is served, which is relative to the host. +// * If it is not included, the API is served directly under the host. +// * The value MUST start with a leading slash (/). +// * The basePath does not support +// * [path templating](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathTemplating). +// */ +// basePath: string; + +// /** +// * The transfer protocol of the API. Values MUST be from the list: "http", +// * "https", "ws", "wss". If the schemes is not included, the default scheme +// * to be used is the one used to access the Swagger definition itself. +// */ +// schemes?: Array; + +// /** +// * A list of MIME types the APIs can consume. This is global to all APIs but +// * can be overridden on specific API calls. Value MUST be as described under +// * Mime Types. +// */ +// consumes?: Array; + +// /** +// * A list of MIME types the APIs can produce. This is global to all APIs but +// * can be overridden on specific API calls. Value MUST be as described under +// * Mime Types. +// */ +// produces?: Array; + +// /** +// * The available paths and operations for the API. +// */ +// paths: PathsObject; + +// /** +// * An object to hold parameters that can be used across operations. This +// * property does not define global parameters for all operations. +// */ +// parameters?: ParametersDefinitionsObject; + +// /** +// * An object to hold responses that can be used across operations. This +// * property does not define global responses for all operations. +// */ +// responses?: ResponsesDefinitionsObject; + +// /** +// * An object to hold data types produced and consumed by operations. +// */ +// definitions?: DefinitionsObject; + +// /** +// * Security scheme definitions that can be used across the specification. +// */ +// securityDefinitions?: SecurityDefinitionsObject; + +// /** +// * A declaration of which security schemes are applied for the API as a whole. +// * The list of values describes alternative security schemes that can be used +// * (that is, there is a logical OR between the security requirements). +// * Individual operations can override this definition. +// */ +// security?: Array; + +// /** +// * A list of tags used by the specification with additional metadata. The +// * order of the tags can be used to reflect on their order by the parsing +// * tools. Not all tags that are used by the Operation Object must be declared. +// * The tags that are not declared may be organized randomly or based on the +// * tools' logic. Each tag name in the list MUST be unique. +// */ +// tags?: Array; + +// /** +// * Additional external documentation. +// */ +// externalDocs?: ExternalDocumentationObject; +// } + +// /** +// * The object provides metadata about the API. +// * The metadata can be used by the clients if needed, +// * and can be presented in the Swagger-UI for convenience. +// *

Specification: +// * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#infoObject +// */ +// export interface InfoObject extends Extensible { +// /** +// * The title of the application. +// */ +// title: string; + +// /** +// * A short description of the application. +// * [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) +// * can be used for rich text representation. +// */ +// description?: string; + +// /** +// * The Terms of Service for the API. +// */ +// termsOfService?: string; + +// /** +// * The contact information for the exposed API. +// */ +// contact?: ContactObject; + +// /** +// * The license information for the exposed API. +// */ +// license?: LicenseObject; + +// /** +// * Provides the version of the application API +// * (not to be confused with the specification version). +// */ +// version: string; +// } + +// /** +// * Contact information for the exposed API. +// */ +// export interface ContactObject extends Extensible { +// /** +// * The identifying name of the contact person/organization. +// */ +// name?: string; + +// /** +// *The URL pointing to the contact information. MUST be in the format of a URL. +// */ +// url?: string; + +// /** +// * The email address of the contact person/organization. MUST be in the format +// * of an email address. +// */ +// email?: string; +// } + +// /** +// * License information for the exposed API. +// */ +// export interface LicenseObject extends Extensible { +// /** +// * The license name used for the API. +// */ +// name: string; + +// /** +// * A URL to the license used for the API. MUST be in the format of a URL. +// */ +// url?: string; +// } + +// /** +// * Allows referencing an external resource for extended documentation. +// */ +// export interface ExternalDocumentationObject extends Extensible { +// /** +// * A short description of the target documentation. GFM syntax can be used for +// * rich text representation. +// */ +// description?: string; + +// /** +// * The URL for the target documentation. Value MUST be in the format of a URL. +// */ +// url: string; +// } + +// /** +// * Allows adding meta data to a single tag that is used by the Operation Object. +// * It is not mandatory to have a Tag Object per tag used there. +// */ +// export interface TagObject extends Extensible { +// /** +// * The name of the tag. +// */ +// name: string; + +// /** +// * A short description for the tag. GFM syntax can be used for rich text +// * representation. +// */ +// description?: string; + +// /** +// * Additional external documentation for this tag. +// */ +// externalDocs?: ExternalDocumentationObject; +// } + +// /** +// * Allows the definition of a security scheme that can be used by the +// * operations. Supported schemes are basic authentication, an API key (either +// * as a header or as a query parameter) and OAuth2's common flows (implicit, +// * password, application and access code). +// */ +// export interface SecuritySchemeObject extends Extensible { +// /** +// * The type of the security scheme. Valid values are "basic", "apiKey" or +// * "oauth2". +// */ +// type: 'basic' | 'apiKey' | 'oauth2'; + +// /** +// * A short description for security scheme. +// */ +// description?: string; + +// /** +// * The name of the header or query parameter to be used. +// */ +// name?: string; + +// /** +// * The location of the API key. Valid values are "query" or "header". +// */ +// in?: 'query' | 'header'; + +// /** +// * The flow used by the OAuth2 security scheme. Valid values are "implicit", +// * "password", "application" or "accessCode". +// */ +// flow?: 'implicit' | 'password' | 'application' | 'accessCode'; + +// /** +// * ("implicit", "accessCode") Required. The authorization URL to be used for +// * this flow. This SHOULD be in the form of a URL. +// */ +// authorizationUrl?: string; + +// /** +// * ("password", "application", "accessCode") Required. The token URL to be +// * used for this flow. This SHOULD be in the form of a URL. +// */ +// tokenUrl?: string; + +// /** +// * The available scopes for the OAuth2 security scheme. +// */ +// scopes?: ScopesObject; +// } + +// /** +// * Maps names to a given type of values +// */ +// export interface MapObject { +// /** +// * Maps between a name and object +// */ +// [name: string]: T; +// } + +// /** +// * Lists the available scopes for an OAuth2 security scheme. +// */ +// export interface ScopesObject extends MapObject, Extensible { +// /** +// * Maps between a name of a scope to a short description of it (as the value +// * of the property). +// */ +// [name: string]: string; +// } + +// /** +// * A declaration of the security schemes available to be used in the +// * specification. This does not enforce the security schemes on the operations +// * and only serves to provide the relevant details for each scheme. +// */ +// export interface SecurityDefinitionsObject +// extends MapObject { +// /** +// * A single security scheme definition, mapping a "name" to the scheme it +// * defines. +// */ +// [name: string]: SecuritySchemeObject; +// } + +// /** +// * Lists the required security schemes to execute this operation. The object can +// * have multiple security schemes declared in it which are all required (that +// * is, there is a logical AND between the schemes). +// * The name used for each property MUST correspond to a security scheme declared +// * in the Security Definitions. +// */ +// export interface SecurityRequirementObject extends MapObject> { +// /** +// * Each name must correspond to a security scheme which is declared in the +// * Security Definitions. If the security scheme is of type "oauth2", then the +// * value is a list of scope names required for the execution. For other +// * security scheme types, the array MUST be empty. +// */ +// [name: string]: Array; +// } + +// /** +// * An object to hold data types that can be consumed and produced by operations. +// * These data types can be primitives, arrays or models. +// */ +// export interface DefinitionsObject extends MapObject { +// /** +// * A single definition, mapping a "name" to the schema it defines. +// */ +// [name: string]: SchemaObject; +// } + +// /** +// * An object to hold parameters to be reused across operations. Parameter +// * definitions can be referenced to the ones defined here. +// * +// * This does not define global operation parameters. +// */ +// export interface ParametersDefinitionsObject +// extends MapObject { +// /** +// * A single parameter definition, mapping a "name" to the parameter it +// * defines. +// */ +// [name: string]: ParameterObject; +// } + +// /** +// * An object to hold responses to be reused across operations. Response +// * definitions can be referenced to the ones defined here. +// * +// * This does not define global operation responses. +// */ +// export interface ResponsesDefinitionsObject extends MapObject { +// /** +// * A single response definition, mapping a "name" to the response it defines. +// */ +// [name: string]: ResponseObject; +// } + +// /** +// * Holds the relative paths to the individual endpoints. +// * The path is appended to the basePath in order to construct the full URL. +// * The Paths may be empty, due to ACL constraints. +// *

Specification: +// * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#paths-object +// */ +// export interface PathsObject +// extends MapObject { +// [httpPathOrSwaggerExtension: string]: PathItemObject | ExtensionValue; +// } + +// /** +// * Describes the operations available on a single path. +// * A Path Item may be empty, due to ACL constraints. +// * The path itself is still exposed to the documentation viewer +// * but they will not know which operations and parameters are available. +// *

Specification: +// * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathItemObject +// */ +// export interface PathItemObject extends Extensible { +// /** +// * Allows for an external definition of this path item. The referenced +// * structure MUST be in the format of a Path Item Object. If there are +// * conflicts between the referenced definition and this Path Item's +// * definition, the behavior is undefined. +// */ +// $ref?: string; + +// /** +// * A definition of a GET operation on this path. +// */ +// get?: OperationObject; + +// /** +// * A definition of a PUT operation on this path. +// */ +// put?: OperationObject; + +// /** +// * A definition of a POST operation on this path. +// */ +// post?: OperationObject; + +// /** +// * A definition of a DELETE operation on this path. +// */ +// delete?: OperationObject; + +// /** +// * A definition of a OPTIONS operation on this path. +// */ +// options?: OperationObject; + +// /** +// * A definition of a HEAD operation on this path. +// */ +// head?: OperationObject; + +// /** +// * A definition of a PATCH operation on this path. +// */ +// patch?: OperationObject; + +// /** +// * A list of parameters that are applicable for all the operations described +// * under this path. These parameters can be overridden at the operation level, +// * but cannot be removed there. The list MUST NOT include duplicated +// * parameters. A unique parameter is defined by a combination of a name and +// * location. The list can use the Reference Object to link to parameters that +// * are defined at the Swagger Object's parameters. There can be one "body" +// * parameter at most. +// */ +// parameters?: Array; +// } + +// /** +// * Describes a single API operation on a path. +// *

Specification: +// * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operationObject +// */ +// export interface OperationObject extends Extensible { +// /** +// * IBM/LoopBack extension: The name of the controller method implementing +// * this operation. +// */ +// 'x-operation-name'?: string; + +// /** +// * A list of parameters that are applicable for this operation. +// * If a parameter is already defined at the Path Item, +// * the new definition will override it, but can never remove it. +// * The list MUST NOT include duplicated parameters. +// * A unique parameter is defined by a combination of a name and location. +// * The list can use the Reference Object to link to parameters that are +// * defined at the Swagger Object's parameters. +// * There can be one "body" parameter at most. +// */ +// parameters?: Array; + +// /** +// * The list of possible responses as they are returned from executing +// * this operation. +// */ +// responses: ResponsesObject; + +// /** +// * A list of tags for API documentation control. Tags can be used for logical +// * grouping of operations by resources or any other qualifier. +// */ +// tags?: Array; + +// /** +// * A short summary of what the operation does. For maximum readability in the +// * swagger-ui, this field SHOULD be less than 120 characters. +// */ +// summary?: string; + +// /** +// * A verbose explanation of the operation behavior. GFM syntax can be used for +// * rich text representation. +// */ +// description?: string; + +// /** +// * Additional external documentation for this operation. +// */ +// externalDocs?: ExternalDocumentationObject; + +// /** +// * Unique string used to identify the operation. The id MUST be unique among +// * all operations described in the API. Tools and libraries MAY use the +// * operationId to uniquely identify an operation, therefore, it is recommended +// * to follow common programming naming conventions. +// */ +// operationId?: string; + +// /** +// * A list of MIME types the operation can consume. This overrides the consumes +// * definition at the Swagger Object. An empty value MAY be used to clear the +// * global definition. Value MUST be as described under Mime Types. +// */ +// consumes?: Array; + +// /** +// * A list of MIME types the operation can produce. This overrides the produces +// * definition at the Swagger Object. An empty value MAY be used to clear the +// * global definition. Value MUST be as described under Mime Types. +// */ +// produces?: Array; + +// /** +// * The transfer protocol of the API. Values MUST be from the list: "http", +// * "https", "ws", "wss". If the schemes is not included, the default scheme +// * to be used is the one used to access the Swagger definition itself. +// */ +// schemes?: Array; + +// /** +// * Declares this operation to be deprecated. Usage of the declared operation +// * should be refrained. Default value is false. +// */ +// deprecated?: boolean; + +// /** +// * A declaration of which security schemes are applied for this operation. +// * The list of values describes alternative security schemes that can be used +// * (that is, there is a logical OR between the security requirements). This +// * definition overrides any declared top-level security. To remove a top-level +// * security declaration, an empty array can be used. +// */ +// security?: SecurityRequirementObject; +// } + +// /** +// * Simple type - primitive types or array of such types. It is used by parameter +// * definitions that are not located in "body". +// */ +// export interface SimpleType { +// /** +// * The type of the parameter. Since the parameter is not located at +// * the request body, it is limited to simple types (that is, not an object). +// * The value MUST be one of "string", "number", "integer", "boolean", +// * "array" or "file". If type is "file", the `consumes` MUST be either +// * "multipart/form-data", " application/x-www-form-urlencoded" or both +// * and the parameter MUST be `in` "formData". +// */ +// type?: ParameterType; + +// /** +// * The extending format for the previously mentioned type. See +// * [Data Type Formats](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#dataTypeFormat) +// * for further details. +// */ +// format?: string; + +// /** +// * Sets the ability to pass empty-valued parameters. This is valid only for +// * either query or formData parameters and allows you to send a parameter +// * with a name only or an empty value. Default value is false. +// */ +// allowEmptyValue?: boolean; + +// /** +// * Required if type is "array". Describes the type of items in the array. +// */ +// items?: ItemsObject; + +// /** +// * Determines the format of the array if type array is used. Possible values +// * are: +// * - csv: comma separated values foo,bar. +// * - ssv: space separated values foo bar. +// * - tsv: tab separated values foo\tbar. +// * - pipes: pipe separated values foo|bar. +// * - multi: corresponds to multiple parameter instances instead of multiple +// * values for a single instance foo=bar&foo=baz. This is valid only for +// * parameters in "query" or "formData". +// * +// * Default value is csv. +// */ +// collectionFormat?: string; + +// /** +// * Declares the value of the parameter that the server will use if none is +// * provided, for example a "count" to control the number of results per page +// * might default to 100 if not supplied by the client in the request. (Note: +// * "default" has no meaning for required parameters.) See +// * https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-6.2. +// * Unlike JSON Schema this value MUST conform to the defined type for this +// * parameter. +// */ +// default?: ExtensionValue; + +// /** +// * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.2. +// */ +// maximum?: number; + +// /** +// * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.2. +// */ +// exclusiveMaximum?: number; + +// /** +// * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.3. +// */ +// minimum?: number; + +// /** +// * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.3. +// */ +// exclusiveMinimum?: number; + +// /** +// * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.1. +// */ +// maxLength?: number; + +// /** +// * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.2. +// */ +// minLength?: number; + +// /** +// * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.3. +// */ +// pattern?: string; + +// /** +// * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.2. +// */ +// maxItems?: number; + +// /** +// * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.3. +// */ +// minItems?: number; + +// /** +// * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.4. +// */ +// uniqueItems?: boolean; + +// /** +// * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.1. +// */ +// enum?: Array; + +// /** +// * See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.1. +// */ +// multipleOf?: number; +// } + +// /** +// * The internal type of the array. The value MUST be one of "string", +// * "number", "integer", "boolean", or "array". Files and models are not +// * allowed. +// */ +// export type ItemType = 'string' | 'number' | 'integer' | 'boolean' | 'array'; + +// /** +// * A limited subset of JSON-Schema's items object. It is used by parameter +// * definitions that are not located in "body". Please note it only differs +// * from SimpleType with parameter types excluding `file`. +// */ +// export interface ItemsObject extends SimpleType { +// type: ItemType; +// } + +// /** +// * Describes a single operation parameter. +// *

Specification: +// * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameterObject +// */ +// export interface ParameterObject extends SimpleType, Extensible { +// /** +// * The name of the parameter. Parameter names are case sensitive. +// * - If `in` is "path", the `name` field MUST correspond to the associated +// * path segment from the `path` field in the Paths Object. +// * See [Path Templating](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathTemplating) +// * for further information. +// * - For all other cases, the `name` corresponds to the parameter name used +// * based on the `in` property. +// */ +// name: string; + +// /** +// * The location of the parameter. +// * Possible values are "query", "header", "path", "formData" or "body". +// */ +// in: ParameterLocation; + +// /** +// * A brief description of the parameter. This could contain examples of use. +// * [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) +// * can be used for rich text representation. +// */ +// description?: string; + +// /** +// * Determines whether this parameter is mandatory. +// * If the parameter is `in` "path", this property is required and +// * its value MUST be `true`. Otherwise, the property MAY be included +// * and its default value is `false`. +// */ +// required?: boolean; + +// /** +// * _If `in` is any value other than "body":_, the valid properties are +// * inherited from `SimpleType`. +// */ + +// /** +// * _If in is "body":_ +// * The schema defining the type used for the body parameter. +// */ +// schema?: SchemaObject; +// } + +// /** +// * A simple object to allow referencing other definitions in the specification. +// * It can be used to reference parameters and responses that are defined +// * at the top level for reuse. +// * The Reference Object is a [JSON Reference](http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-02) +// * that uses a [JSON Pointer](https://tools.ietf.org/html/rfc6901) as its value. +// * For this specification, only canonical dereferencing is supported. +// *

Specification: +// * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#referenceObject +// *

**Example** +// * ```json +// * { +// * "$ref": "#/definitions/Pet" +// * } +// * ``` +// */ +// export interface ReferenceObject { +// /** +// * The reference string. +// */ +// $ref: string; +// } + +// /** +// * A container for the expected responses of an operation. +// * The container maps a HTTP response code to the expected response. +// * It is not expected from the documentation to necessarily cover all +// * possible HTTP response codes, since they may not be known in advance. +// * However, it is expected from the documentation to cover a successful +// * operation response and any known errors. +// *

The `default` can be used as the default response object for all +// * HTTP codes that are not covered individually by the specification. +// *

The `ResponsesObject` MUST contain at least one response code, +// * and it SHOULD be the response for a successful operation call. +// *

Specification: +// * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#responsesObject +// */ +// export interface ResponsesObject +// extends MapObject, +// Extensible { +// /** +// * The documentation of responses other than the ones declared for specific +// * HTTP response codes. It can be used to cover undeclared responses. +// * Reference Object can be used to link to a response that is defined at +// * the Swagger Object's responses section. +// */ +// default?: ResponseObject | ReferenceObject; +// } + +// /** +// * Describes a single response from an API Operation. +// *

Specification: +// * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#responseObject +// */ +// export interface ResponseObject extends Extensible { +// /** +// * A short description of the response. +// * [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) +// * can be used for rich text representation. +// */ +// description: string; + +// /** +// * A definition of the response structure. +// * It can be a primitive, an array or an object. +// * If this field does not exist, it means no content is returned +// * as part of the response. As an extension to the `SchemaObject`, +// * its root type value may also be "file". +// * This SHOULD be accompanied by a relevant `produces` mime-type. +// */ +// schema: SchemaObject; + +// /** +// * A list of headers that are sent with the response. +// */ +// headers?: HeadersObject; + +// /** +// * An example of the response message. +// */ +// examples?: ExampleObject; +// } + +// /** +// * Allows sharing examples for operation responses. +// */ +// export interface ExampleObject extends MapObject { +// /** +// * The name of the property MUST be one of the Operation produces values +// * (either implicit or inherited). The value SHOULD be an example of what +// * such a response would look like. +// */ +// [mimeType: string]: ExtensionValue; +// } + +// /** +// * Lists the headers that can be sent as part of a response. +// */ +// export interface HeadersObject extends MapObject { +// /** +// * The name of the property corresponds to the name of the header. The value +// * describes the type of the header. +// */ +// [name: string]: HeaderObject; +// } + +// export interface HeaderObject extends ItemsObject, Extensible { +// /** +// * A short description of the header. +// */ +// description: string; +// } + +// /** +// * The Schema Object allows the definition of input and output data types. +// * These types can be objects, but also primitives and arrays. +// * This object is based on the [JSON Schema Specification Draft 4](http://json-schema.org/) +// * and uses a predefined subset of it. On top of this subset, there are +// * extensions provided by this specification to allow for more complete documentation. +// *

Specification: +// * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject +// */ +// export interface SchemaObject extends Extensible { +// /** +// * The following properties are taken directly from the JSON Schema +// * definition and follow the same specifications: +// */ +// $ref?: string; +// format?: string; +// title?: string; +// description?: string; +// default?: ExtensionValue; +// multipleOf?: number; +// maximum?: number; +// exclusiveMaximum?: number; +// minimum?: number; +// exclusiveMinimum?: number; +// maxLength?: number; +// minLength?: number; +// pattern?: string; +// maxItems?: number; +// minItems?: number; +// uniqueItems?: boolean; +// maxProperties?: number; +// minProperties?: number; +// required?: Array; +// enum?: Array; +// type?: string; + +// /** +// * The following properties are taken from the JSON Schema definition but +// * their definitions were adjusted to the Swagger Specification. Their +// * definition is the same as the one from JSON Schema, only where the original +// * definition references the JSON Schema definition, the Schema Object +// * definition is used instead. +// */ +// allOf?: Array; +// properties?: MapObject; +// additionalProperties?: SchemaObject; +// items?: SchemaObject; + +// /** +// * Other than the JSON Schema subset fields, the following fields may be used +// * for further schema documentation. +// */ + +// /** +// * Adds support for polymorphism. The discriminator is the schema property +// * name that is used to differentiate between other schema that inherit this +// * schema. The property name used MUST be defined at this schema and it MUST +// * be in the required property list. When used, the value MUST be the name of +// * this schema or any schema that inherits it. +// */ +// discriminator?: string; + +// /** +// * Relevant only for Schema "properties" definitions. Declares the property +// * as "read only". This means that it MAY be sent as part of a response but +// * MUST NOT be sent as part of the request. Properties marked as readOnly +// * being true SHOULD NOT be in the required list of the defined schema. +// * Default value is false. +// */ +// readOnly?: boolean; + +// /** +// * This MAY be used only on properties schemas. It has no effect on root +// * schemas. Adds Additional metadata to describe the XML representation format +// * of this property. +// */ +// xml?: XMLObject; + +// /** +// * Additional external documentation for this schema. +// */ +// externalDocs?: ExternalDocumentationObject; + +// /** +// * A free-form property to include an example of an instance for this schema. +// */ +// example?: ExtensionValue; +// } + +// /** +// * A metadata object that allows for more fine-tuned XML model definitions. +// * +// * When using arrays, XML element names are not inferred (for singular/plural +// * forms) and the name property should be used to add that information. See +// * examples for expected behavior. +// */ +// export interface XMLObject extends Extensible { +// /** +// * Replaces the name of the element/attribute used for the described schema +// * property. When defined within the Items Object (items), it will affect the +// * name of the individual XML elements within the list. When defined alongside +// * type being array (outside the items), it will affect the wrapping element +// * and only if wrapped is true. If wrapped is false, it will be ignored. +// */ +// name?: string; + +// /** +// * The URL of the namespace definition. Value SHOULD be in the form of a URL. +// */ +// namespace?: string; + +// /** +// * The prefix to be used for the name. +// */ +// prefix?: string; + +// /** +// * Declares whether the property definition translates to an attribute +// * instead of an element. Default value is false. +// */ +// attribute?: boolean; + +// /** +// * MAY be used only for an array definition. Signifies whether the array is +// * wrapped (for example, ) or unwrapped +// * (). Default value is false. The definition takes effect +// * only when defined alongside type being array (outside the items). +// */ +// wrapped?: boolean; +// } + +// /** +// * Create an empty OpenApiSpec object that's still a valid Swagger document. +// */ +// export function createEmptyApiSpec(): OpenApiSpec { +// return { +// swagger: '2.0', +// basePath: '/', +// info: { +// title: 'LoopBack Application', +// version: '1.0.0', +// }, +// paths: {}, +// }; +// } diff --git a/packages/openapi-spec/src/openapi-spec-v3.ts b/packages/openapi-spec/src/openapi-spec-v3.ts index 324d08ac8135..fdbfc82f00a9 100644 --- a/packages/openapi-spec/src/openapi-spec-v3.ts +++ b/packages/openapi-spec/src/openapi-spec-v3.ts @@ -1,2 +1,159 @@ export * from 'openapi3-ts'; +import * as OAS3 from 'openapi3-ts'; +export type OpenApiSpec = OAS3.OpenAPIObject; +/** + * Custom extensions can use arbitrary type as the value, + * e.g. a string, an object or an array. + */ +// tslint:disable-next-line:no-any +export type ExtensionValue = any; + +/** + * The location of a parameter. + * Possible values are "query", "header", "path", "formData" or "body". + *

Specification: + * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameterIn + */ +export type ParameterLocation = +| 'query' +| 'header' +| 'path' +| 'formData' +| 'body'; + +export type ParameterType = +| 'string' +| 'number' +| 'integer' +| 'boolean' +| 'array' +| 'file'; + +/** + * Maps names to a given type of values + */ +export interface MapObject { + /** + * Maps between a name and object + */ + [name: string]: T; + } + + /** + * Lists the available scopes for an OAuth2 security scheme. + */ +export interface ScopesObject extends MapObject, OAS3.ISpecificationExtension { + /** + * Maps between a name of a scope to a short description of it (as the value + * of the property). + */ + [name: string]: string; + } + +/** + * A declaration of the security schemes available to be used in the + * specification. This does not enforce the security schemes on the operations + * and only serves to provide the relevant details for each scheme. + */ +export interface SecurityDefinitionsObject +extends MapObject { +/** + * A single security scheme definition, mapping a "name" to the scheme it + * defines. + */ +[name: string]: OAS3.SecuritySchemeObject; +} + +/** + * An object to hold parameters to be reused across operations. Parameter + * definitions can be referenced to the ones defined here. + * + * This does not define global operation parameters. + */ +export interface ParametersDefinitionsObject +extends MapObject { +/** + * A single parameter definition, mapping a "name" to the parameter it + * defines. + */ +[name: string]: OAS3.ParameterObject; +} + +/** + * An object to hold responses to be reused across operations. Response + * definitions can be referenced to the ones defined here. + * + * This does not define global operation responses. + */ +export interface ResponsesDefinitionsObject extends MapObject { + /** + * A single response definition, mapping a "name" to the response it defines. + */ + [name: string]: OAS3.ResponseObject; + } + +/** + * A container for the expected responses of an operation. + * The container maps a HTTP response code to the expected response. + * It is not expected from the documentation to necessarily cover all + * possible HTTP response codes, since they may not be known in advance. + * However, it is expected from the documentation to cover a successful + * operation response and any known errors. + *

The `default` can be used as the default response object for all + * HTTP codes that are not covered individually by the specification. + *

The `ResponsesObject` MUST contain at least one response code, + * and it SHOULD be the response for a successful operation call. + *

Specification: + * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#responsesObject + */ +export interface ResponsesObject +extends MapObject, + OAS3.ISpecificationExtension { +/** + * The documentation of responses other than the ones declared for specific + * HTTP response codes. It can be used to cover undeclared responses. + * Reference Object can be used to link to a response that is defined at + * the Swagger Object's responses section. + */ +default?: OAS3.ResponseObject | OAS3.ReferenceObject; +} + +/** + * Lists the headers that can be sent as part of a response. + */ +export interface HeadersObject extends MapObject { + /** + * The name of the property corresponds to the name of the header. The value + * describes the type of the header. + */ + [name: string]: OAS3.HeaderObject; + } + + +/** + * Holds the relative paths to the individual endpoints. + * The path is appended to the basePath in order to construct the full URL. + * The Paths may be empty, due to ACL constraints. + *

Specification: + * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#paths-object + */ +export interface PathsObject +extends MapObject { +[httpPathOrSwaggerExtension: string]: OAS3.PathItemObject | ExtensionValue; +} + +/** + * Create an empty OpenApiSpec object that's still a valid Swagger document. + */ +export function createEmptyApiSpec(): OpenApiSpec { + return { + openapi: '3.0.0', + basePath: '/', + info: { + title: 'LoopBack Application', + version: '1.0.0', + }, + paths: {}, + }; +} \ No newline at end of file diff --git a/packages/rest/test/integration/rest-server.integration.ts b/packages/rest/test/integration/rest-server.integration.ts index 612e087eb1b7..e1e2ec4f782c 100644 --- a/packages/rest/test/integration/rest-server.integration.ts +++ b/packages/rest/test/integration/rest-server.integration.ts @@ -79,7 +79,7 @@ describe('RestServer (integration)', () => { const response = await createClientForHandler(server.handleHttp).get( '/swagger.yaml', ); - expect(response.text).to.eql(`swagger: '2.0' + expect(response.text).to.eql(`openapi: '3.0.0' basePath: / info: title: LoopBack Application diff --git a/packages/rest/test/unit/rest-server/rest-server.open-api-spec.test.ts b/packages/rest/test/unit/rest-server/rest-server.open-api-spec.test.ts index 2d8205d72b1a..273bb835a7ef 100644 --- a/packages/rest/test/unit/rest-server/rest-server.open-api-spec.test.ts +++ b/packages/rest/test/unit/rest-server/rest-server.open-api-spec.test.ts @@ -19,7 +19,7 @@ describe('RestServer.getApiSpec()', () => { it('honours API defined via app.api()', () => { server.api({ - swagger: '2.0', + openapi: '3.0.0', info: { title: 'Test API', version: '1.0.0', @@ -32,7 +32,7 @@ describe('RestServer.getApiSpec()', () => { const spec = server.getApiSpec(); expect(spec).to.deepEqual({ - swagger: '2.0', + openapi: '3.0.0', info: { title: 'Test API', version: '1.0.0', diff --git a/packages/testlab/src/validate-api-spec.ts b/packages/testlab/src/validate-api-spec.ts index e73b482cf4f9..fca3414a0531 100644 --- a/packages/testlab/src/validate-api-spec.ts +++ b/packages/testlab/src/validate-api-spec.ts @@ -16,8 +16,8 @@ export async function validateApiSpec(spec: OpenApiSpec): Promise { // workaround for unhelpful message returned by SwaggerParser // TODO(bajtos) contribute these improvements to swagger-parser - if (!spec.swagger) { - throw new Error('Missing required property: swagger at #/'); + if (!spec.openapi) { + throw new Error('Missing required property: openapi at #/'); } if (!spec.info) {