From 72cb8834b3996b7a169aa3eff2a97503085ae24c Mon Sep 17 00:00:00 2001 From: jannyHou Date: Thu, 1 Mar 2018 10:37:39 -0500 Subject: [PATCH] feat: upgrade from swagger 2 to openapi 3 * (rest) Remove GET /swagger.json and GET /swagger.yaml * (all packages) Upgrade packages from using @loopback/openapi-v2 to @loopback/openapi-v3 * (all pakcages) Upgrade packages from using @loopback/openapi-spec to @loopback/openapi-v3-types * (testlab) Replace Swagger validator by OAI3 validator provided by swagger2openapi --- CODEOWNERS | 8 +- packages/authentication/package.json | 2 +- .../test/acceptance/basic-auth.ts | 2 +- packages/boot/package.json | 2 +- .../boot/test/fixtures/multiple.artifact.ts | 2 +- .../src/controllers/ping.controller.ts.ejs | 2 +- .../controller-rest-template.ts.template | 2 +- .../project/templates/package.json.ejs | 2 +- .../project/templates/package.plain.json.ejs | 2 +- packages/cli/lib/dependencies.json | 2 +- packages/cli/test/app.js | 2 +- packages/cli/test/project.js | 4 +- .../docs/controller.md | 37 ++++---- .../example-getting-started/docs/model.md | 40 +------- packages/example-getting-started/package.json | 4 +- .../src/controllers/todo.controller.ts | 24 +++-- .../src/models/todo.model.ts | 27 ------ packages/example-log-extension/package.json | 1 + .../acceptance/log.extension.acceptance.ts | 3 +- packages/openapi-spec-builder/package.json | 6 +- .../src/openapi-spec-builder.ts | 14 ++- packages/openapi-spec/README.md | 4 + packages/openapi-v2/README.md | 4 + .../src/openapi-spec-builder.ts} | 21 +---- .../controller-decorators.test.ts | 5 +- .../param-decorators/param.test.ts | 2 +- packages/openapi-v3/README.md | 13 +-- packages/openapi-v3/src/index.ts | 1 - .../operation-decorators.test.ts | 2 +- .../param-decorators/param.test.ts | 2 +- packages/rest/package.json | 7 +- packages/rest/src/http-handler.ts | 8 +- packages/rest/src/index.ts | 4 +- packages/rest/src/parser.ts | 53 +++++------ packages/rest/src/rest-application.ts | 2 +- packages/rest/src/rest-component.ts | 2 +- packages/rest/src/rest-server.ts | 21 ++--- packages/rest/src/router/routing-table.ts | 4 +- .../acceptance/routing/routing.acceptance.ts | 45 ++++++++- .../sequence/sequence.acceptance.ts | 2 +- .../integration/http-handler.integration.ts | 91 ++++--------------- .../integration/rest-server.integration.ts | 77 ++-------------- packages/rest/test/unit/parser.test.ts | 25 +++-- ...ompatibility.test.ts => re-export.test.ts} | 5 +- .../rest-server.open-api-spec.test.ts | 16 ++-- .../test/unit/router/routing-table.test.ts | 8 +- packages/testlab/package.json | 6 +- packages/testlab/src/validate-api-spec.ts | 32 ++----- 48 files changed, 243 insertions(+), 407 deletions(-) rename packages/{openapi-v3/src/spec-builder/openapi-v3-spec-builder.ts => openapi-v2/src/openapi-spec-builder.ts} (89%) rename packages/rest/test/unit/{backward-compatibility.test.ts => re-export.test.ts} (78%) diff --git a/CODEOWNERS b/CODEOWNERS index 381fa969f1cc..75608a34d2ed 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -15,11 +15,11 @@ packages/example-hello-world/* @b-admike packages/example-log-extension/* @virkt25 packages/example-rpc-server/* @kjdelisle packages/metadata/* @raymondfeng -packages/openapi-spec/* @bajtos @jannyHou +packages/openapi-spec/* @bajtos @jannyHou packages/openapi-spec-builder/* @bajtos @raymondfeng -packages/openapi-v2/* @jannyHou -packages/openapi-v3/* @jannyHou -packages/openapi-v3-types/* @jannyHou +packages/openapi-v2/* @bajtos @jannyHou +packages/openapi-v3/* @bajtos @jannyHou +packages/openapi-v3-types/* @bajtos @jannyHou packages/repository/* @raymondfeng @kjdelisle packages/repository-json-schema/* @shimks packages/rest/* @bajtos @kjdelisle diff --git a/packages/authentication/package.json b/packages/authentication/package.json index 52c0971f1960..d9aa6c075430 100644 --- a/packages/authentication/package.json +++ b/packages/authentication/package.json @@ -23,7 +23,7 @@ "dependencies": { "@loopback/context": "^0.2.0", "@loopback/core": "^0.2.0", - "@loopback/openapi-v2": "^0.2.0", + "@loopback/openapi-v3": "^0.2.0", "@loopback/rest": "^0.2.0", "passport": "^0.4.0", "passport-strategy": "^1.0.0" diff --git a/packages/authentication/test/acceptance/basic-auth.ts b/packages/authentication/test/acceptance/basic-auth.ts index 74f8c2cd47f8..4848498afcc9 100644 --- a/packages/authentication/test/acceptance/basic-auth.ts +++ b/packages/authentication/test/acceptance/basic-auth.ts @@ -17,7 +17,7 @@ import { RestServer, RestComponent, } from '@loopback/rest'; -import {api, get} from '@loopback/openapi-v2'; +import {api, get} from '@loopback/openapi-v3'; import {Client, createClientForHandler} from '@loopback/testlab'; import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; import {inject, Provider, ValueOrPromise} from '@loopback/context'; diff --git a/packages/boot/package.json b/packages/boot/package.json index 158e989b4683..02eefb436d09 100644 --- a/packages/boot/package.json +++ b/packages/boot/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "@loopback/build": "^0.2.0", - "@loopback/openapi-v2": "^0.2.0", + "@loopback/openapi-v3": "^0.2.0", "@loopback/rest": "^0.2.0", "@loopback/testlab": "^0.2.0" }, diff --git a/packages/boot/test/fixtures/multiple.artifact.ts b/packages/boot/test/fixtures/multiple.artifact.ts index cfd598e4b471..b07340c25f21 100644 --- a/packages/boot/test/fixtures/multiple.artifact.ts +++ b/packages/boot/test/fixtures/multiple.artifact.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {get} from '@loopback/openapi-v2'; +import {get} from '@loopback/openapi-v3'; export class ArtifactOne { @get('/one') diff --git a/packages/cli/generators/app/templates/src/controllers/ping.controller.ts.ejs b/packages/cli/generators/app/templates/src/controllers/ping.controller.ts.ejs index 08f2ccd32019..32c92378131e 100644 --- a/packages/cli/generators/app/templates/src/controllers/ping.controller.ts.ejs +++ b/packages/cli/generators/app/templates/src/controllers/ping.controller.ts.ejs @@ -1,5 +1,5 @@ import {ServerRequest} from '@loopback/rest'; -import {get} from '@loopback/openapi-v2'; +import {get} from '@loopback/openapi-v3'; import {inject} from '@loopback/context'; /** diff --git a/packages/cli/generators/controller/templates/src/controllers/controller-rest-template.ts.template b/packages/cli/generators/controller/templates/src/controllers/controller-rest-template.ts.template index 803333101cda..8762dfeed993 100644 --- a/packages/cli/generators/controller/templates/src/controllers/controller-rest-template.ts.template +++ b/packages/cli/generators/controller/templates/src/controllers/controller-rest-template.ts.template @@ -1,5 +1,5 @@ import {Filter, Where} from '@loopback/repository'; -import {post, param, get, put, patch, del} from '@loopback/openapi-v2'; +import {post, param, get, put, patch, del} from '@loopback/openapi-v3'; import {inject} from '@loopback/context'; import {<%= modelName %>} from '../models'; import {<%= repositoryName %>} from '../repositories'; diff --git a/packages/cli/generators/project/templates/package.json.ejs b/packages/cli/generators/project/templates/package.json.ejs index f4c8cab94a24..1b7c5f3a0ccf 100644 --- a/packages/cli/generators/project/templates/package.json.ejs +++ b/packages/cli/generators/project/templates/package.json.ejs @@ -62,7 +62,7 @@ <% if (project.projectType === 'application') { -%> "@loopback/core": "<%= project.dependencies['@loopback/core'] -%>", "@loopback/rest": "<%= project.dependencies['@loopback/rest'] -%>", - "@loopback/openapi-v2": "<%= project.dependencies['@loopback/openapi-v2'] -%>" + "@loopback/openapi-v3": "<%= project.dependencies['@loopback/openapi-v3'] -%>" <% } else { -%> "@loopback/core": "<%= project.dependencies['@loopback/core'] -%>" <% } -%> diff --git a/packages/cli/generators/project/templates/package.plain.json.ejs b/packages/cli/generators/project/templates/package.plain.json.ejs index 03d54cc6b3dc..ad0ae75b8ba1 100644 --- a/packages/cli/generators/project/templates/package.plain.json.ejs +++ b/packages/cli/generators/project/templates/package.plain.json.ejs @@ -62,7 +62,7 @@ <% if (project.projectType === 'application') { -%> "@loopback/core": "<%= project.dependencies['@loopback/core'] -%>", "@loopback/rest": "<%= project.dependencies['@loopback/rest'] -%>", - "@loopback/openapi-v2": "<%= project.dependencies['@loopback/openapi-v2'] -%>" + "@loopback/openapi-v3": "<%= project.dependencies['@loopback/openapi-v3'] -%>" <% } else { -%> "@loopback/core": "<%= project.dependencies['@loopback/core'] -%>" <% } -%> diff --git a/packages/cli/lib/dependencies.json b/packages/cli/lib/dependencies.json index f2625a6d80cf..c3e682789808 100644 --- a/packages/cli/lib/dependencies.json +++ b/packages/cli/lib/dependencies.json @@ -5,7 +5,7 @@ "@loopback/boot": "^0.2.0", "@loopback/core": "^0.2.0", "@loopback/rest": "^0.2.0", - "@loopback/openapi-v2": "^0.2.0", + "@loopback/openapi-v3": "^0.2.0", "@loopback/build": "^0.2.0", "@loopback/testlab": "^0.2.0", "@types/mocha": "^2.2.43", diff --git a/packages/cli/test/app.js b/packages/cli/test/app.js index c7500db7c599..81e183c3e66a 100644 --- a/packages/cli/test/app.js +++ b/packages/cli/test/app.js @@ -50,7 +50,7 @@ describe('app-generator specfic files', () => { assert.fileContent('src/controllers/ping.controller.ts', /ping\(\)/); assert.fileContent( 'src/controllers/ping.controller.ts', - /\'\@loopback\/openapi\-v2\'/ + /\'\@loopback\/openapi\-v3\'/ ); assert.file; diff --git a/packages/cli/test/project.js b/packages/cli/test/project.js index d9a22033ccd3..23da2ae811f4 100644 --- a/packages/cli/test/project.js +++ b/packages/cli/test/project.js @@ -231,7 +231,7 @@ module.exports = function(projGenerator, props, projectType) { ); assert.fileContent( 'package.json', - `"@loopback/openapi-v2": "${deps['@loopback/openapi-v2']}"` + `"@loopback/openapi-v3": "${deps['@loopback/openapi-v3']}"` ); assert.jsonFileContent('package.json', { scripts: { @@ -249,7 +249,7 @@ module.exports = function(projGenerator, props, projectType) { `"@loopback/context": "${deps['@loopback/context']}"` ); assert.noFileContent('package.json', '"@loopback/rest"'); - assert.noFileContent('package.json', '"@loopback/openapi-v2"'); + assert.noFileContent('package.json', '"@loopback/openapi-v3"'); assert.noJsonFileContent('package.json', { start: 'npm run build && node .', }); diff --git a/packages/example-getting-started/docs/controller.md b/packages/example-getting-started/docs/controller.md index d9807690e1fe..3c0ed8e18b27 100644 --- a/packages/example-getting-started/docs/controller.md +++ b/packages/example-getting-started/docs/controller.md @@ -51,9 +51,9 @@ Now that we have the repository wireup, let's create our first handler function. #### src/controllers/todo.controller.ts ```ts -import {post, param} from '@loopback/openapi-v2'; +import {post, param} from '@loopback/openapi-v3'; import {HttpErrors} from '@loopback/rest'; -import {TodoSchema, Todo} from '../models'; +import {Todo} from '../models'; import {repository} from '@loopback/repository'; import {TodoRepository} from '../repositories/index'; @@ -63,8 +63,7 @@ export class TodoController { ) {} @post('/todo') - @param.body('todo', TodoSchema) - async createTodo(todo: Todo) { + async createTodo(@requestBody() todo: Todo) { if (!todo.title) { return Promise.reject(new HttpErrors.BadRequest('title is required')); } @@ -78,7 +77,7 @@ metadata about the route, verb and the format of the incoming request body: - `@post('/todo')` creates metadata for LoopBack's [RestServer]() so that it can redirect requests to this function when the path and verb match. -- `@param.body('todo', TodoSchema)` associates the OpenAPI schema for a Todo +- `@requestBody()` associates the OpenAPI schema for a Todo with the body of the request so that LoopBack can validate the format of an incoming request (**Note**: As of this writing, schematic validation is not yet functional). @@ -95,9 +94,9 @@ verbs: #### src/controllers/todo.controller.ts ```ts -import {post, param, get, put, patch, del} from '@loopback/openapi-v2'; +import {post, param, requestBody, get, put, patch, del} from '@loopback/openapi-v3'; import {HttpErrors} from '@loopback/rest'; -import {TodoSchema, Todo} from '../models'; +import {Todo} from '../models'; import {repository} from '@loopback/repository'; import {TodoRepository} from '../repositories/index'; @@ -107,8 +106,7 @@ export class TodoController { ) {} @post('/todo') - @param.body('todo', TodoSchema) - async createTodo(todo: Todo) { + async createTodo(@requestBody()todo: Todo) { if (!todo.title) { return Promise.reject(new HttpErrors.BadRequest('title is required')); } @@ -116,9 +114,9 @@ export class TodoController { } @get('/todo/{id}') - @param.path.number('id') - @param.query.boolean('items') - async findTodoById(id: number, items?: boolean): Promise { + async findTodoById( + @param.path.number('id') id: number, + @param.query.boolean('items') items?: boolean): Promise { return await this.todoRepo.findById(id); } @@ -128,22 +126,21 @@ export class TodoController { } @put('/todo/{id}') - @param.path.number('id') - @param.body('todo', TodoSchema) - async replaceTodo(id: number, todo: Todo): Promise { + async replaceTodo( + @param.path.number('id') id: number, + @requestBody() todo: Todo): Promise { return await this.todoRepo.replaceById(id, todo); } @patch('/todo/{id}') - @param.path.number('id') - @param.body('todo', TodoSchema) - async updateTodo(id: number, todo: Todo): Promise { + async updateTodo( + @param.path.number('id') id: number, + @param.body() todo: Todo): Promise { return await this.todoRepo.updateById(id, todo); } @del('/todo/{id}') - @param.path.number('id') - async deleteTodo(id: number): Promise { + async deleteTodo(@param.path.number('id') id: number): Promise { return await this.todoRepo.deleteById(id); } } diff --git a/packages/example-getting-started/docs/model.md b/packages/example-getting-started/docs/model.md index f2630f6ba711..21865e994de0 100644 --- a/packages/example-getting-started/docs/model.md +++ b/packages/example-getting-started/docs/model.md @@ -63,7 +63,6 @@ the `getId` function, so that it can retrieve a Todo model's ID as needed. #### src/models/todo.model.ts ```ts import {Entity, property, model} from '@loopback/repository'; -import {SchemaObject} from '@loopback/openapi-spec'; @model() export class Todo extends Entity { @@ -95,44 +94,7 @@ export class Todo extends Entity { } ``` - -Additionally, we'll define a `SchemaObject` that represents our Todo model -as an [OpenAPI Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schema-object). -This will give the OpenAPI spec builder the information it needs to describe the -Todo model on your app's OpenAPI endpoints. - -#### src/models/todo.model.ts -```ts -// Note: This is in the same file as the Todo model class. -// This should come *after* the model class definition! -export const TodoSchema: SchemaObject = { - title: 'todoItem', - properties: { - id: { - type: 'number', - description: 'ID number of the Todo entry.' - }, - title: { - type: 'string', - description: 'Title of the Todo entry.' - }, - desc: { - type: 'number', - description: 'ID number of the Todo entry.' - }, - isComplete: { - type: 'boolean', - description: 'Whether or not the Todo entry is complete.' - } - }, - required: ['title'], -}; -``` - -Now that we have our model and schema, it's time to add a +Now that we have our model, it's time to add a [datasource](datasource.md) so we can perform real CRUD operations! ### Navigation diff --git a/packages/example-getting-started/package.json b/packages/example-getting-started/package.json index c9e50e85d0bf..ad67fc2660e7 100644 --- a/packages/example-getting-started/package.json +++ b/packages/example-getting-started/package.json @@ -28,8 +28,8 @@ "@loopback/boot": "^0.2.0", "@loopback/context": "^0.2.0", "@loopback/core": "^0.2.0", - "@loopback/openapi-spec": "^0.2.0", - "@loopback/openapi-v2": "^0.2.0", + "@loopback/openapi-v3-types": "^0.2.0", + "@loopback/openapi-v3": "^0.2.0", "@loopback/repository": "^0.2.0", "@loopback/rest": "^0.2.0" }, diff --git a/packages/example-getting-started/src/controllers/todo.controller.ts b/packages/example-getting-started/src/controllers/todo.controller.ts index 9033dd4e61f2..5911dec8d7ad 100644 --- a/packages/example-getting-started/src/controllers/todo.controller.ts +++ b/packages/example-getting-started/src/controllers/todo.controller.ts @@ -1,6 +1,14 @@ -import {post, param, get, put, patch, del} from '@loopback/openapi-v2'; +import { + post, + param, + get, + put, + patch, + del, + requestBody, +} from '@loopback/openapi-v3'; import {HttpErrors} from '@loopback/rest'; -import {TodoSchema, Todo} from '../models'; +import {Todo} from '../models'; import {repository} from '@loopback/repository'; import {TodoRepository} from '../repositories'; @@ -11,11 +19,9 @@ export class TodoController { constructor( @repository(TodoRepository.name) protected todoRepo: TodoRepository, ) {} + @post('/todo') - async createTodo( - @param.body('todo', TodoSchema) - todo: Todo, - ) { + async createTodo(@requestBody() todo: Todo) { // TODO(bajtos) This should be handled by the framework // See https://github.com/strongloop/loopback-next/issues/118 if (!todo.title) { @@ -40,8 +46,7 @@ export class TodoController { @put('/todo/{id}') async replaceTodo( @param.path.number('id') id: number, - @param.body('todo', TodoSchema) - todo: Todo, + @requestBody() todo: Todo, ): Promise { // REST adapter does not coerce parameter values coming from string sources // like path & query. As a workaround, we have to cast the value to a number @@ -55,8 +60,7 @@ export class TodoController { @patch('/todo/{id}') async updateTodo( @param.path.number('id') id: number, - @param.body('todo', TodoSchema) - todo: Todo, + @requestBody() todo: Todo, ): Promise { // REST adapter does not coerce parameter values coming from string sources // like path & query. As a workaround, we have to cast the value to a number diff --git a/packages/example-getting-started/src/models/todo.model.ts b/packages/example-getting-started/src/models/todo.model.ts index 43a092bd481a..010740f4025e 100644 --- a/packages/example-getting-started/src/models/todo.model.ts +++ b/packages/example-getting-started/src/models/todo.model.ts @@ -1,5 +1,4 @@ import {Entity, property, model} from '@loopback/repository'; -import {SchemaObject} from '@loopback/openapi-spec'; @model() export class Todo extends Entity { @@ -29,29 +28,3 @@ export class Todo extends Entity { return this.id; } } - -// TODO(bajtos) The schema should be generated from model definition -// See https://github.com/strongloop/loopback-next/issues/700 -// export const TodoSchema = createSchemaFromModel(Todo); -export const TodoSchema: SchemaObject = { - title: 'todoItem', - properties: { - id: { - type: 'number', - description: 'ID number of the Todo entry.', - }, - title: { - type: 'string', - description: 'Title of the Todo entry.', - }, - desc: { - type: 'number', - description: 'ID number of the Todo entry.', - }, - isComplete: { - type: 'boolean', - description: 'Whether or not the Todo entry is complete.', - }, - }, - required: ['title'], -}; diff --git a/packages/example-log-extension/package.json b/packages/example-log-extension/package.json index f99670bab05b..87cf9832d4a1 100644 --- a/packages/example-log-extension/package.json +++ b/packages/example-log-extension/package.json @@ -47,6 +47,7 @@ "dependencies": { "@loopback/context": "^0.2.0", "@loopback/core": "^0.2.0", + "@loopback/openapi-v3": "^0.2.0", "@loopback/rest": "^0.2.0", "chalk": "^2.3.0", "debug": "^3.1.0" diff --git a/packages/example-log-extension/test/acceptance/log.extension.acceptance.ts b/packages/example-log-extension/test/acceptance/log.extension.acceptance.ts index 5382fba8990e..36a6ab6a5dd7 100644 --- a/packages/example-log-extension/test/acceptance/log.extension.acceptance.ts +++ b/packages/example-log-extension/test/acceptance/log.extension.acceptance.ts @@ -6,8 +6,6 @@ import { RestApplication, RestServer, - get, - param, SequenceHandler, RestBindings, FindRoute, @@ -18,6 +16,7 @@ import { ParsedRequest, ServerResponse, } from '@loopback/rest'; +import {get, param} from '@loopback/openapi-v3'; import { LogComponent, LogLevelMixin, diff --git a/packages/openapi-spec-builder/package.json b/packages/openapi-spec-builder/package.json index a217d079fad0..241bcfd07fb1 100644 --- a/packages/openapi-spec-builder/package.json +++ b/packages/openapi-spec-builder/package.json @@ -8,9 +8,9 @@ "scripts": { "build": "lb-tsc es2017", "build:apidocs": "lb-apidocs", - "clean": "lb-clean loopback-openapi-spec*.tgz dist package api-docs", + "clean": "lb-clean loopback-openapi-spec-builder*.tgz dist package api-docs", "prepublishOnly": "npm run build && npm run build:apidocs", - "verify": "npm pack && tar xf loopback-openapi-spec*.tgz && tree package && npm run clean" + "verify": "npm pack && tar xf loopback-openapi-spec-builder*.tgz && tree package && npm run clean" }, "author": "IBM", "copyright.owner": "IBM Corp.", @@ -23,7 +23,7 @@ "Testing" ], "dependencies": { - "@loopback/openapi-spec": "^0.2.0" + "@loopback/openapi-v3-types": "^0.2.0" }, "devDependencies": { "@loopback/build": "^0.2.0" diff --git a/packages/openapi-spec-builder/src/openapi-spec-builder.ts b/packages/openapi-spec-builder/src/openapi-spec-builder.ts index 39946535ac14..cce3a7e40c29 100644 --- a/packages/openapi-spec-builder/src/openapi-spec-builder.ts +++ b/packages/openapi-spec-builder/src/openapi-spec-builder.ts @@ -11,7 +11,8 @@ import { ResponseObject, ParameterObject, createEmptyApiSpec, -} from '@loopback/openapi-spec'; + RequestBodyObject, +} from '@loopback/openapi-v3-types'; /** * Create a new instance of OpenApiSpecBuilder. @@ -137,7 +138,11 @@ export class OperationSpecBuilder extends BuilderBase { withStringResponse(status: number | 'default' = 200): this { return this.withResponse(status, { description: 'The string result.', - schema: {type: 'string'}, + content: { + 'text/plain': { + schema: {type: 'string'}, + }, + }, }); } @@ -154,6 +159,11 @@ export class OperationSpecBuilder extends BuilderBase { return this; } + withRequestBody(requestBodySpec: RequestBodyObject): this { + this._spec.requestBody = requestBodySpec; + return this; + } + /** * Define the operation name (controller method name). * diff --git a/packages/openapi-spec/README.md b/packages/openapi-spec/README.md index 073d8f45a90d..c22bfa753be2 100644 --- a/packages/openapi-spec/README.md +++ b/packages/openapi-spec/README.md @@ -1,3 +1,7 @@ +## DEPRECATION NOTICE + +This package has been deprecated in favor of OpenAPI Spec version 3.0.0, we are no longer maintaining it. + # @loopback/openapi-spec TypeScript type definitions for OpenAPI Spec/Swagger documents. diff --git a/packages/openapi-v2/README.md b/packages/openapi-v2/README.md index 13316e28726d..68c1523c24de 100644 --- a/packages/openapi-v2/README.md +++ b/packages/openapi-v2/README.md @@ -1,3 +1,7 @@ +## DEPRECATION NOTICE + +This package has been deprecated in favor of OpenAPI Spec version 3.0.0, we are no longer maintaining it. + @loopback/openapi-v2 This package contains: diff --git a/packages/openapi-v3/src/spec-builder/openapi-v3-spec-builder.ts b/packages/openapi-v2/src/openapi-spec-builder.ts similarity index 89% rename from packages/openapi-v3/src/spec-builder/openapi-v3-spec-builder.ts rename to packages/openapi-v2/src/openapi-spec-builder.ts index d2bfdc771d82..bad1bf2a5890 100644 --- a/packages/openapi-v3/src/spec-builder/openapi-v3-spec-builder.ts +++ b/packages/openapi-v2/src/openapi-spec-builder.ts @@ -1,8 +1,5 @@ -// This is just a temporary file, in the next PR it will be moved to -// @loopback/openapi-spec-builder - // Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/openapi-spec-builder +// Node module: @loopback/openapi-v2 // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT @@ -14,8 +11,7 @@ import { ResponseObject, ParameterObject, createEmptyApiSpec, - RequestBodyObject, -} from '@loopback/openapi-v3-types'; +} from '@loopback/openapi-spec'; /** * Create a new instance of OpenApiSpecBuilder. @@ -141,13 +137,7 @@ export class OperationSpecBuilder extends BuilderBase { withStringResponse(status: number | 'default' = 200): this { return this.withResponse(status, { description: 'The string result.', - content: { - // TODO(janny) will change it to a default value - // after we figure out the plan for content type - '*/*': { - schema: {type: 'string'}, - }, - }, + schema: {type: 'string'}, }); } @@ -164,11 +154,6 @@ export class OperationSpecBuilder extends BuilderBase { return this; } - withRequestBody(requestBodySpec: RequestBodyObject): this { - this._spec.requestBody = requestBodySpec; - return this; - } - /** * Define the operation name (controller method name). * diff --git a/packages/openapi-v2/test/unit/controller-spec/controller-decorators.test.ts b/packages/openapi-v2/test/unit/controller-spec/controller-decorators.test.ts index 1b7d3c7c01cc..18f6ddeafcf6 100644 --- a/packages/openapi-v2/test/unit/controller-spec/controller-decorators.test.ts +++ b/packages/openapi-v2/test/unit/controller-spec/controller-decorators.test.ts @@ -15,7 +15,10 @@ import { param, } from '../../..'; import {expect} from '@loopback/testlab'; -import {anOpenApiSpec, anOperationSpec} from '@loopback/openapi-spec-builder'; +import { + anOpenApiSpec, + anOperationSpec, +} from '../../../src/openapi-spec-builder'; describe('Routing metadata', () => { it('returns spec defined via @api()', () => { diff --git a/packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts b/packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts index 9a9f1defa454..0229920f1c88 100644 --- a/packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts +++ b/packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts @@ -18,7 +18,7 @@ import { DefinitionsObject, } from '@loopback/openapi-spec'; import {expect} from '@loopback/testlab'; -import {anOperationSpec} from '@loopback/openapi-spec-builder'; +import {anOperationSpec} from '../../../../../src/openapi-spec-builder'; import * as stream from 'stream'; import {model, property} from '@loopback/repository'; diff --git a/packages/openapi-v3/README.md b/packages/openapi-v3/README.md index b5e143a28ac6..b234b14a3b8d 100644 --- a/packages/openapi-v3/README.md +++ b/packages/openapi-v3/README.md @@ -29,8 +29,8 @@ Here is an example of calling function `getControllerSpec` to generate the OpenA ```js import {api, getControllerSpec} from '@loopback/openapi-v3'; -@api(somePathSpec) class MyController { + @get('/greet') greet() { return 'Hello world!'; } @@ -43,24 +43,13 @@ then the `myControllerSpec` will be: ```js { - openapi: '3.0.0', - info: { title: 'LoopBack Application', version: '1.0.0' }, paths: { '/greet': { get: { - responses: { - '200': { - description: 'The string result.', - schema: { type: 'string' } - } - }, 'x-operation-name': 'greet' } } }, - servers: [ - {url: '/'} - ] } ``` diff --git a/packages/openapi-v3/src/index.ts b/packages/openapi-v3/src/index.ts index 59a77ad86633..2946151a34e2 100644 --- a/packages/openapi-v3/src/index.ts +++ b/packages/openapi-v3/src/index.ts @@ -9,4 +9,3 @@ export * from './json-to-schema'; export * from './operation-decorator'; export * from './parameter-decorator'; export * from './request-body-decorator'; -export * from './spec-builder/openapi-v3-spec-builder'; diff --git a/packages/openapi-v3/test/unit/controller-spec/controller-decorators/operation-decorators.test.ts b/packages/openapi-v3/test/unit/controller-spec/controller-decorators/operation-decorators.test.ts index e783bbc65a09..eee72ac91aa5 100644 --- a/packages/openapi-v3/test/unit/controller-spec/controller-decorators/operation-decorators.test.ts +++ b/packages/openapi-v3/test/unit/controller-spec/controller-decorators/operation-decorators.test.ts @@ -15,7 +15,7 @@ import { param, } from '../../../..'; import {expect} from '@loopback/testlab'; -import {anOpenApiSpec, anOperationSpec} from '../../../../'; +import {anOpenApiSpec, anOperationSpec} from '@loopback/openapi-spec-builder'; describe('Routing metadata', () => { it('returns spec defined via @api()', () => { diff --git a/packages/openapi-v3/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts b/packages/openapi-v3/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts index 3554912216a4..6778f52e1492 100644 --- a/packages/openapi-v3/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts +++ b/packages/openapi-v3/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts @@ -9,7 +9,7 @@ import { OperationObject, } from '@loopback/openapi-v3-types'; import {param, get, patch, operation, getControllerSpec} from '../../../../../'; -import {anOperationSpec} from '../../../../../'; +import {anOperationSpec} from '@loopback/openapi-spec-builder'; import {expect} from '@loopback/testlab'; describe('Routing metadata for parameters', () => { diff --git a/packages/rest/package.json b/packages/rest/package.json index ea0b3c5256e5..e20e70535800 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -23,8 +23,8 @@ "dependencies": { "@loopback/context": "^0.2.0", "@loopback/core": "^0.2.0", - "@loopback/openapi-spec": "^0.2.0", - "@loopback/openapi-v2": "^0.2.0", + "@loopback/openapi-v3-types": "^0.2.0", + "@loopback/openapi-v3": "^0.2.0", "@types/http-errors": "^1.6.1", "@types/node": "^8.5.9", "body": "^5.1.0", @@ -32,8 +32,7 @@ "http-errors": "^1.6.1", "js-yaml": "^3.9.1", "lodash": "^4.17.4", - "path-to-regexp": "^2.0.0", - "swagger2openapi": "^2.10.7" + "path-to-regexp": "^2.0.0" }, "devDependencies": { "@loopback/build": "^0.2.0", diff --git a/packages/rest/src/http-handler.ts b/packages/rest/src/http-handler.ts index f87087cd33c7..d934a0f689a2 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, DefinitionsObject} from '@loopback/openapi-spec'; +import {PathsObject, SchemasObject} from '@loopback/openapi-v3-types'; import {ServerRequest, ServerResponse} from 'http'; -import {ControllerSpec} from '@loopback/openapi-v2'; +import {ControllerSpec} from '@loopback/openapi-v3'; import {SequenceHandler} from './sequence'; import { @@ -22,7 +22,7 @@ import {RestBindings} from './keys'; export class HttpHandler { protected _routes: RoutingTable = new RoutingTable(); - protected _apiDefinitions: DefinitionsObject; + protected _apiDefinitions: SchemasObject; public handleRequest: ( request: ServerRequest, @@ -41,7 +41,7 @@ export class HttpHandler { this._routes.registerRoute(route); } - registerApiDefinitions(defs: DefinitionsObject) { + registerApiDefinitions(defs: SchemasObject) { this._apiDefinitions = Object.assign({}, this._apiDefinitions, defs); } diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index 339f47f08e43..d730f186c957 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -35,6 +35,4 @@ export * from './rest-application'; export * from './rest-component'; export * from './rest-server'; export * from './sequence'; - -// Re-export decorators like @get for easier use -export * from '@loopback/openapi-v2'; +export * from '@loopback/openapi-v3'; diff --git a/packages/rest/src/parser.ts b/packages/rest/src/parser.ts index 8c6696d4d022..95d5e0b8f21f 100644 --- a/packages/rest/src/parser.ts +++ b/packages/rest/src/parser.ts @@ -5,7 +5,12 @@ import {ServerRequest} from 'http'; import * as HttpErrors from 'http-errors'; -import {OperationObject, ParameterObject} from '@loopback/openapi-spec'; +import { + OperationObject, + ParameterObject, + isReferenceObject, +} from '@loopback/openapi-v3-types'; +import {REQUEST_BODY_INDEX} from '@loopback/openapi-v3'; import {promisify} from 'util'; import { OperationArgs, @@ -59,7 +64,7 @@ function loadRequestBodyIfNeeded( operationSpec: OperationObject, request: ServerRequest, ): Promise { - if (!hasArgumentsFromBody(operationSpec)) return Promise.resolve(); + if (!operationSpec.requestBody) return Promise.resolve(); const contentType = getContentType(request); if (contentType && !/json/.test(contentType)) { @@ -75,28 +80,27 @@ function loadRequestBodyIfNeeded( }); } -function hasArgumentsFromBody(operationSpec: OperationObject): boolean { - if (!operationSpec.parameters || !operationSpec.parameters.length) - return false; - - for (const paramSpec of operationSpec.parameters) { - if ('$ref' in paramSpec) continue; - const source = (paramSpec as ParameterObject).in; - if (source === 'formData' || source === 'body') return true; - } - return false; -} - function buildOperationArguments( operationSpec: OperationObject, request: ParsedRequest, pathParams: PathParameterValues, body?: MaybeBody, ): OperationArgs { - const args: OperationArgs = []; + let requestBodyIndex: number = -1; + if (operationSpec.requestBody) { + // the type of `operationSpec.requestBody` could be `RequestBodyObject` + // or `ReferenceObject`, resolving a `$ref` value is not supported yet. + if (isReferenceObject(operationSpec.requestBody)) { + throw new Error('$ref requestBody is not supported yet.'); + } + const i = operationSpec.requestBody[REQUEST_BODY_INDEX]; + requestBodyIndex = i ? i : 0; + } + + const paramArgs: OperationArgs = []; for (const paramSpec of operationSpec.parameters || []) { - if ('$ref' in paramSpec) { + if (isReferenceObject(paramSpec)) { // TODO(bajtos) implement $ref parameters // See https://github.com/strongloop/loopback-next/issues/435 throw new Error('$ref parameters are not supported yet.'); @@ -104,25 +108,22 @@ function buildOperationArguments( const spec = paramSpec as ParameterObject; switch (spec.in) { case 'query': - args.push(request.query[spec.name]); + paramArgs.push(request.query[spec.name]); break; case 'path': - args.push(pathParams[spec.name]); + paramArgs.push(pathParams[spec.name]); break; case 'header': - args.push(request.headers[spec.name.toLowerCase()]); - break; - case 'formData': - args.push(body ? body[spec.name] : undefined); - break; - case 'body': - args.push(body); + paramArgs.push(request.headers[spec.name.toLowerCase()]); break; + // TODO(jannyhou) to support `cookie`, + // see issue https://github.com/strongloop/loopback-next/issues/997 default: throw new HttpErrors.NotImplemented( 'Parameters with "in: ' + spec.in + '" are not supported yet.', ); } } - return args; + if (requestBodyIndex > -1) paramArgs.splice(requestBodyIndex, 0, body); + return paramArgs; } diff --git a/packages/rest/src/rest-application.ts b/packages/rest/src/rest-application.ts index 251901517e6c..8a1e0ffd0db6 100644 --- a/packages/rest/src/rest-application.ts +++ b/packages/rest/src/rest-application.ts @@ -11,7 +11,7 @@ import {format} from 'util'; import {RestBindings} from './keys'; import {RouteEntry, RestServer} from '.'; import {ControllerClass} from './router/routing-table'; -import {OperationObject, OpenApiSpec} from '@loopback/openapi-spec'; +import {OperationObject, OpenApiSpec} from '@loopback/openapi-v3-types'; export const ERR_NO_MULTI_SERVER = format( 'RestApplication does not support multiple servers!', diff --git a/packages/rest/src/rest-component.ts b/packages/rest/src/rest-component.ts index 27aab58a4177..685633e3d535 100644 --- a/packages/rest/src/rest-component.ts +++ b/packages/rest/src/rest-component.ts @@ -24,7 +24,7 @@ import { } from './providers'; import {RestServer, RestServerConfig} from './rest-server'; import {DefaultSequence} from '.'; -import {createEmptyApiSpec} from '@loopback/openapi-spec'; +import {createEmptyApiSpec} from '@loopback/openapi-v3-types'; export class RestComponent implements Component { providers: ProviderMap = { diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index bf808cc0f596..90511c7c05a8 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -4,16 +4,15 @@ // License text available at https://opensource.org/licenses/MIT import {AssertionError} from 'assert'; -const swagger2openapi = require('swagger2openapi'); import {safeDump} from 'js-yaml'; import {Binding, Context, Constructor, inject} from '@loopback/context'; import {Route, ControllerRoute, RouteEntry} from './router/routing-table'; import {ParsedRequest} from './internal-types'; -import {OpenApiSpec, OperationObject} from '@loopback/openapi-spec'; +import {OpenApiSpec, OperationObject} from '@loopback/openapi-v3-types'; import {ServerRequest, ServerResponse, createServer} from 'http'; import * as Http from 'http'; import {Application, CoreBindings, Server} from '@loopback/core'; -import {getControllerSpec} from '@loopback/openapi-v2'; +import {getControllerSpec} from '@loopback/openapi-v3'; import {HttpHandler} from './http-handler'; import {DefaultSequence, SequenceHandler, SequenceFunction} from './sequence'; import { @@ -48,8 +47,6 @@ interface OpenApiSpecOptions { const OPENAPI_SPEC_MAPPING: {[key: string]: OpenApiSpecOptions} = { '/openapi.json': {version: '3.0.0', format: 'json'}, '/openapi.yaml': {version: '3.0.0', format: 'yaml'}, - '/swagger.json': {version: '2.0', format: 'json'}, - '/swagger.yaml': {version: '2.0', format: 'yaml'}, }; /** @@ -212,8 +209,8 @@ export class RestServer extends Context implements Server { // controller methods are specified through app.api() spec continue; } - if (apiSpec.definitions) { - this._httpHandler.registerApiDefinitions(apiSpec.definitions); + if (apiSpec.components && apiSpec.components.schemas) { + this._httpHandler.registerApiDefinitions(apiSpec.components.schemas); } this._httpHandler.registerController(ctor, apiSpec); } @@ -279,11 +276,8 @@ export class RestServer extends Context implements Server { response: ServerResponse, options?: OpenApiSpecOptions, ) { - options = options || {version: '2.0', format: 'json'}; + options = options || {version: '3.0.0', format: 'json'}; let specObj = this.getApiSpec(); - if (options.version === '3.0.0') { - specObj = await swagger2openapi.convertObj(specObj, {direct: true}); - } if (options.format === 'json') { const spec = JSON.stringify(specObj, null, 2); response.setHeader('content-type', 'application/json; charset=utf-8'); @@ -305,7 +299,7 @@ export class RestServer extends Context implements Server { options.apiExplorerUrl || 'https://loopback.io/api-explorer'; response.setHeader( 'Location', - `${baseUrl}?url=http://${request.headers.host}/swagger.json`, + `${baseUrl}?url=http://${request.headers.host}/openapi.json`, ); response.end(); } @@ -452,7 +446,8 @@ export class RestServer extends Context implements Server { // accidentally modifying our internal routing data spec.paths = cloneDeep(this.httpHandler.describeApiPaths()); if (defs) { - spec.definitions = cloneDeep(defs); + spec.components = spec.components || {}; + spec.components.schemas = cloneDeep(defs); } return spec; } diff --git a/packages/rest/src/router/routing-table.ts b/packages/rest/src/router/routing-table.ts index b121a3a6ee88..a23eb96fcda1 100644 --- a/packages/rest/src/router/routing-table.ts +++ b/packages/rest/src/router/routing-table.ts @@ -7,7 +7,7 @@ import { OperationObject, ParameterObject, PathsObject, -} from '@loopback/openapi-spec'; +} from '@loopback/openapi-v3-types'; import { Context, Constructor, @@ -24,7 +24,7 @@ import { OperationRetval, } from '../internal-types'; -import {ControllerSpec} from '@loopback/openapi-v2'; +import {ControllerSpec} from '@loopback/openapi-v3'; import * as assert from 'assert'; import * as url from 'url'; diff --git a/packages/rest/test/acceptance/routing/routing.acceptance.ts b/packages/rest/test/acceptance/routing/routing.acceptance.ts index d3e11f6c9c25..5297b13847d9 100644 --- a/packages/rest/test/acceptance/routing/routing.acceptance.ts +++ b/packages/rest/test/acceptance/routing/routing.acceptance.ts @@ -14,7 +14,7 @@ import { SequenceActions, } from '../../..'; -import {api, get, param} from '@loopback/openapi-v2'; +import {api, get, post, param, requestBody} from '@loopback/openapi-v3'; import {Application} from '@loopback/core'; @@ -22,7 +22,7 @@ import { ParameterObject, OperationObject, ResponseObject, -} from '@loopback/openapi-spec'; +} from '@loopback/openapi-v3-types'; import {expect, Client, createClientForHandler} from '@loopback/testlab'; import {anOpenApiSpec, anOperationSpec} from '@loopback/openapi-spec-builder'; @@ -106,8 +106,7 @@ describe('Routing', () => { it('allows controllers to define params via decorators', async () => { class MyController { @get('/greet') - @param.query.string('name') - greet(name: string) { + greet(@param.query.string('name') name: string) { return `hello ${name}`; } } @@ -122,6 +121,44 @@ describe('Routing', () => { ); }); + it('allows controllers to define requestBody via decorator', async () => { + class MyController { + @post('/greet') + greet(@requestBody() message: object) { + return message; + } + } + const app = givenAnApplication(); + const server = await givenAServer(app); + givenControllerInApp(app, MyController); + const greeting = {greeting: 'hello world'}; + return whenIMakeRequestTo(server) + .post('/greet') + .send(greeting) + .expect(greeting); + }); + + it('allows mixed use of @requestBody and @param', async () => { + class MyController { + @post('/greet') + greet( + @param.header.string('language') language: string, + @requestBody() message: object, + ) { + return Object.assign(message, {language: language}); + } + } + const app = givenAnApplication(); + const server = await givenAServer(app); + givenControllerInApp(app, MyController); + + return whenIMakeRequestTo(server) + .post('/greet') + .set('language', 'English') + .send({greeting: 'hello world'}) + .expect({greeting: 'hello world', language: 'English'}); + }); + it('allows controllers to use method DI with mixed params', async () => { class MyController { @get('/greet') diff --git a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts index c44411bee688..814728f85cad 100644 --- a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts +++ b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts @@ -18,7 +18,7 @@ import { RestComponent, RestApplication, } from '../../..'; -import {api} from '@loopback/openapi-v2'; +import {api} from '@loopback/openapi-v3'; import {Application} from '@loopback/core'; import {expect, Client, createClientForHandler} from '@loopback/testlab'; import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; diff --git a/packages/rest/test/integration/http-handler.integration.ts b/packages/rest/test/integration/http-handler.integration.ts index e645cbaf65d6..6e42723f786c 100644 --- a/packages/rest/test/integration/http-handler.integration.ts +++ b/packages/rest/test/integration/http-handler.integration.ts @@ -13,11 +13,11 @@ import { InvokeMethodProvider, RejectProvider, } from '../..'; -import {ControllerSpec, get} from '@loopback/openapi-v2'; +import {ControllerSpec, get} from '@loopback/openapi-v3'; import {Context} from '@loopback/context'; import {Client, createClientForHandler} from '@loopback/testlab'; import * as HttpErrors from 'http-errors'; -import {ParameterObject} from '@loopback/openapi-spec'; +import {ParameterObject, RequestBodyObject} from '@loopback/openapi-v3-types'; import {anOpenApiSpec, anOperationSpec} from '@loopback/openapi-spec-builder'; import {createUnexpectedHttpErrorLogger} from '../helpers'; @@ -231,68 +231,7 @@ describe('HttpHandler', () => { } }); - context('with a formData-parameter route', () => { - beforeEach(givenFormDataParamController); - - it('returns the value sent in json-encoded body', () => { - return client - .post('/show-formdata') - .send({key: 'value'}) - .expect(200, 'value'); - }); - - it('rejects url-encoded request body', () => { - logErrorsExcept(415); - return client - .post('/show-formdata') - .send('key=value') - .expect(415); - }); - - it('returns 400 for malformed JSON body', () => { - logErrorsExcept(400); - return client - .post('/show-formdata') - .set('content-type', 'application/json') - .send('malformed-json') - .expect(400); - }); - - function givenFormDataParamController() { - const spec = anOpenApiSpec() - .withOperation('post', '/show-formdata', { - 'x-operation-name': 'showFormData', - parameters: [ - { - name: 'key', - in: 'formData', - description: 'Any value.', - required: true, - type: 'string', - }, - ], - responses: { - 200: { - schema: { - type: 'string', - }, - description: '', - }, - }, - }) - .build(); - - class RouteParamController { - async showFormData(key: string): Promise { - return key; - } - } - - givenControllerClass(RouteParamController, spec); - } - }); - - context('with a body-parameter route', () => { + context('with a body request route', () => { beforeEach(givenBodyParamController); it('returns the value sent in json-encoded body', () => { @@ -327,19 +266,23 @@ describe('HttpHandler', () => { const spec = anOpenApiSpec() .withOperation('post', '/show-body', { 'x-operation-name': 'showBody', - parameters: [ - { - name: 'data', - in: 'body', - description: 'Any object value.', - required: true, - schema: {type: 'object'}, + requestBody: { + description: 'Any object value.', + required: true, + content: { + 'application/json': { + schema: {type: 'object'}, + }, }, - ], + }, responses: { 200: { - schema: { - type: 'object', + content: { + 'application/json': { + schema: { + type: 'object', + }, + }, }, description: '', }, diff --git a/packages/rest/test/integration/rest-server.integration.ts b/packages/rest/test/integration/rest-server.integration.ts index 4fd0d155ec07..16bce716bd8f 100644 --- a/packages/rest/test/integration/rest-server.integration.ts +++ b/packages/rest/test/integration/rest-server.integration.ts @@ -37,77 +37,12 @@ describe('RestServer (integration)', () => { .expect(500); }); - it('exposes "GET /swagger.json" endpoint', async () => { - const server = await givenAServer({rest: {port: 0}}); - const greetSpec = { - responses: { - 200: { - schema: {type: 'string'}, - description: 'greeting of the day', - }, - }, - }; - server.route(new Route('get', '/greet', greetSpec, function greet() {})); - - const response = await createClientForHandler(server.handleHttp).get( - '/swagger.json', - ); - expect(response.body).to.containDeep({ - basePath: '/', - paths: { - '/greet': { - get: greetSpec, - }, - }, - }); - expect(response.get('Access-Control-Allow-Origin')).to.equal('*'); - expect(response.get('Access-Control-Allow-Credentials')).to.equal('true'); - expect(response.get('Access-Control-Allow-Max-Age')).to.equal('86400'); - }); - - it('exposes "GET /swagger.yaml" endpoint', async () => { - const server = await givenAServer({rest: {port: 0}}); - const greetSpec = { - responses: { - 200: { - schema: {type: 'string'}, - description: 'greeting of the day', - }, - }, - }; - server.route(new Route('get', '/greet', greetSpec, function greet() {})); - - const response = await createClientForHandler(server.handleHttp).get( - '/swagger.yaml', - ); - const expected = yaml.safeLoad(` -swagger: '2.0' -basePath: / -info: - title: LoopBack Application - version: 1.0.0 -paths: - /greet: - get: - responses: - '200': - schema: - type: string - description: greeting of the day - `); - // Use json for comparison to tolerate textual diffs - expect(yaml.safeLoad(response.text)).to.eql(expected); - expect(response.get('Access-Control-Allow-Origin')).to.equal('*'); - expect(response.get('Access-Control-Allow-Credentials')).to.equal('true'); - expect(response.get('Access-Control-Allow-Max-Age')).to.equal('86400'); - }); - it('exposes "GET /openapi.json" endpoint', async () => { const server = await givenAServer({rest: {port: 0}}); const greetSpec = { responses: { 200: { - schema: {type: 'string'}, + content: {'text/plain': {schema: {type: 'string'}}}, description: 'greeting of the day', }, }, @@ -127,7 +62,7 @@ paths: responses: { '200': { content: { - '*/*': { + 'text/plain': { schema: {type: 'string'}, }, }, @@ -148,7 +83,7 @@ paths: const greetSpec = { responses: { 200: { - schema: {type: 'string'}, + content: {'text/plain': {schema: {type: 'string'}}}, description: 'greeting of the day', }, }, @@ -170,7 +105,7 @@ paths: '200': description: greeting of the day content: - '*/*': + 'text/plain': schema: type: string servers: @@ -204,7 +139,7 @@ servers: const url = new RegExp( [ 'https://loopback.io/api-explorer', - '\\?url=http://\\d+.\\d+.\\d+.\\d+:\\d+/swagger.json', + '\\?url=http://\\d+.\\d+.\\d+.\\d+:\\d+/openapi.json', ].join(''), ); expect(response.get('Location')).match(url); @@ -236,7 +171,7 @@ servers: const url = new RegExp( [ 'http://petstore.swagger.io', - '\\?url=http://\\d+.\\d+.\\d+.\\d+:\\d+/swagger.json', + '\\?url=http://\\d+.\\d+.\\d+.\\d+:\\d+/openapi.json', ].join(''), ); expect(response.get('Location')).match(url); diff --git a/packages/rest/test/unit/parser.test.ts b/packages/rest/test/unit/parser.test.ts index fde346371f75..a221a4dc08b4 100644 --- a/packages/rest/test/unit/parser.test.ts +++ b/packages/rest/test/unit/parser.test.ts @@ -12,7 +12,11 @@ import { createResolvedRoute, } from '../..'; import {expect, ShotRequest, ShotRequestOptions} from '@loopback/testlab'; -import {OperationObject, ParameterObject} from '@loopback/openapi-spec'; +import { + OperationObject, + ParameterObject, + RequestBodyObject, +} from '@loopback/openapi-v3-types'; describe('operationArgsParser', () => { it('parses path parameters', async () => { @@ -37,13 +41,10 @@ describe('operationArgsParser', () => { payload: {key: 'value'}, }); - const spec = givenOperationWithParameters([ - { - name: 'data', - schema: {type: 'object'}, - in: 'body', - }, - ]); + const spec = givenOperationWithRequestBody({ + description: 'data', + content: {'application/json': {schema: {type: 'object'}}}, + }); const route = givenResolvedRoute(spec); const args = await parseOperationArgs(req, route); @@ -59,6 +60,14 @@ describe('operationArgsParser', () => { }; } + function givenOperationWithRequestBody(requestBody?: RequestBodyObject) { + return { + 'x-operation-name': 'testOp', + requestBody: requestBody, + responses: {}, + }; + } + function givenRequest(options?: ShotRequestOptions): ParsedRequest { return parseRequestUrl(new ShotRequest(options || {url: '/'})); } diff --git a/packages/rest/test/unit/backward-compatibility.test.ts b/packages/rest/test/unit/re-export.test.ts similarity index 78% rename from packages/rest/test/unit/backward-compatibility.test.ts rename to packages/rest/test/unit/re-export.test.ts index 4de5c57cecce..0a5395e7db1e 100644 --- a/packages/rest/test/unit/backward-compatibility.test.ts +++ b/packages/rest/test/unit/re-export.test.ts @@ -2,11 +2,10 @@ // Node module: @loopback/rest // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT - import {get} from '../..'; -describe('backward-compatibility', () => { - it('exports functions from @loopback/openapi-v2', async () => { +describe('re-export controller decorators', () => { + it('exports functions from @loopback/openapi-v3', async () => { /* tslint:disable-next-line:no-unused-variable */ class Test { // Make sure the decorators are exported 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 17eda6e54eaf..2b05a5d4bc92 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 @@ -6,7 +6,7 @@ import {expect, validateApiSpec} from '@loopback/testlab'; import {Application} from '@loopback/core'; import {RestServer, Route, RestComponent} from '../../..'; -import {get, post, param} from '@loopback/openapi-v2'; +import {get, post, requestBody} from '@loopback/openapi-v3'; import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; import {model, property} from '@loopback/repository'; @@ -21,26 +21,24 @@ 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', }, - host: 'example.com:8080', - basePath: '/api', + servers: [{url: 'example.com:8080/api'}], paths: {}, 'x-foo': 'bar', }); const spec = server.getApiSpec(); expect(spec).to.deepEqual({ - swagger: '2.0', + openapi: '3.0.0', info: { title: 'Test API', version: '1.0.0', }, - host: 'example.com:8080', - basePath: '/api', + servers: [{url: 'example.com:8080/api'}], paths: {}, 'x-foo': 'bar', }); @@ -152,12 +150,12 @@ describe('RestServer.getApiSpec()', () => { } class MyController { @post('/foo') - createFoo(@param.body('foo') foo: MyModel) {} + createFoo(@requestBody() foo: MyModel) {} } app.controller(MyController); const spec = server.getApiSpec(); - expect(spec.definitions).to.deepEqual({ + expect(spec.components && spec.components.schemas).to.deepEqual({ MyModel: { title: 'MyModel', properties: { diff --git a/packages/rest/test/unit/router/routing-table.test.ts b/packages/rest/test/unit/router/routing-table.test.ts index 8903692288f9..dd50c25fc0c4 100644 --- a/packages/rest/test/unit/router/routing-table.test.ts +++ b/packages/rest/test/unit/router/routing-table.test.ts @@ -9,7 +9,7 @@ import { RoutingTable, ControllerRoute, } from '../../..'; -import {getControllerSpec, param, get} from '@loopback/openapi-v2'; +import {getControllerSpec, param, get} from '@loopback/openapi-v3'; import {expect, ShotRequestOptions, ShotRequest} from '@loopback/testlab'; import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; @@ -47,7 +47,7 @@ describe('RoutingTable', () => { expect(params[0]).to.have.properties({ name: 'message', in: 'query', - type: 'string', + schema: {type: 'string'}, }); }); @@ -81,6 +81,10 @@ describe('RoutingTable', () => { .withOperationReturningString('get', '/hello', 'greet') .build(); + // @jannyHou: please note ` anOpenApiSpec()` returns an openapi spec, + // not controller spec, should be FIXED + // the routing table test expects an empty spec for + // interface `ControllerSpec` spec.basePath = '/my'; class TestController {} diff --git a/packages/testlab/package.json b/packages/testlab/package.json index 5fad6786ebc7..ef4603b6d911 100644 --- a/packages/testlab/package.json +++ b/packages/testlab/package.json @@ -17,18 +17,18 @@ "copyright.owner": "IBM Corp.", "license": "MIT", "dependencies": { - "@loopback/openapi-spec": "^0.2.0", + "@loopback/openapi-v3-types": "^0.2.0", "@types/fs-extra": "^5.0.0", + "@types/node": "^9.3.0", "@types/shot": "^3.4.0", "@types/sinon": "^4.1.3", "@types/supertest": "^2.0.0", - "@types/swagger-parser": "^4.0.1", "fs-extra": "^5.0.0", "shot": "^4.0.3", "should": "^13.1.3", "sinon": "^4.1.2", "supertest": "^3.0.0", - "swagger-parser": "^4.0.1" + "swagger2openapi": "^2.11.10" }, "devDependencies": { "@loopback/build": "^0.2.0" diff --git a/packages/testlab/src/validate-api-spec.ts b/packages/testlab/src/validate-api-spec.ts index ba89aa5b3257..f646066dfe56 100644 --- a/packages/testlab/src/validate-api-spec.ts +++ b/packages/testlab/src/validate-api-spec.ts @@ -3,30 +3,18 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import * as SwaggerParser from 'swagger-parser'; -import {OpenApiSpec} from '@loopback/openapi-spec'; +import {OpenApiSpec} from '@loopback/openapi-v3-types'; +const validator = require('swagger2openapi/validate.js'); +import {promisify} from 'util'; -export async function validateApiSpec(spec: OpenApiSpec): Promise { - const opts: SwaggerParser.Options = { - $refs: { - internal: false, - external: false, - }, - } as SwaggerParser.Options; - - // 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 #/'); - } +const validateAsync = promisify(validator.validate); - if (!spec.info) { - throw new Error('Missing required property: info at #/'); - } +export async function validateApiSpec(spec: OpenApiSpec): Promise { + const opts = {}; - if (!spec.paths) { - throw new Error('Missing required property: paths at #/'); + try { + await validateAsync(spec, opts); + } catch (err) { + throw new Error(err); } - - await SwaggerParser.validate(spec, opts); }