diff --git a/docs/site/OpenAPI-spec-generator.md b/docs/site/OpenAPI-spec-generator.md
new file mode 100644
index 000000000000..558e00e76117
--- /dev/null
+++ b/docs/site/OpenAPI-spec-generator.md
@@ -0,0 +1,47 @@
+---
+lang: en
+title: 'OpenAPI spec generator'
+keywords: LoopBack 4.0, LoopBack 4
+sidebar: lb4_sidebar
+permalink: /doc/en/lb4/OpenAPI-spec-generator.html
+---
+
+{% include content/generator-create-app.html lang=page.lang %}
+
+### Synopsis
+
+Saves OpenAPI spec to a json or yaml file.
+
+```sh
+lb4 openapi-spec [options]
+```
+
+### Options
+
+`--out` or `-o` : File name for the OpenAPI spec
+
+{% include_relative includes/CLI-std-options.md %}
+
+### Interactive Prompts
+
+The tool will prompt you for:
+
+- **File name for the OpenAPI spec.** _(outFile)_ If the `--out` had been
+ supplied from the command line, the prompt is skipped.
+
+### Output
+
+Once all the prompts have been answered, the CLI will do the following:
+
+- Start the application without listening on an Http port.
+- Load the OpenAPI spec
+- Save the spec to the file in json or yaml format depending on the extension
+
+```
+lb4 openapi-spec
+
+? File name for the OpenAPI spec: dist/openapi.json
+Server is running at undefined
+create dist/openapi.json
+OpenAPI spec is written to dist/openapi.json.
+```
diff --git a/docs/site/tables/lb4-artifact-commands.html b/docs/site/tables/lb4-artifact-commands.html
index c8a02a383181..df42328caaa6 100644
--- a/docs/site/tables/lb4-artifact-commands.html
+++ b/docs/site/tables/lb4-artifact-commands.html
@@ -54,7 +54,7 @@
lb4 openapi |
- Generate controllers and models from OpenAPI specs |
+ Generate controllers, service proxies, and models from OpenAPI specs |
OpenAPI generator |
@@ -81,5 +81,11 @@
Generate rest configs for model endpoints |
Model endpoint generator |
+
+
+ lb4 openapi-spec |
+ Save OpenAPI spec to a json or yaml file |
+ OpenAPI spec generator |
+
diff --git a/packages/cli/.yo-rc.json b/packages/cli/.yo-rc.json
index 59a3d36be430..c955ae2ba038 100644
--- a/packages/cli/.yo-rc.json
+++ b/packages/cli/.yo-rc.json
@@ -1643,6 +1643,78 @@
},
"arguments": [],
"name": "copyright"
+ },
+ "openapi-spec": {
+ "options": {
+ "help": {
+ "name": "help",
+ "type": "Boolean",
+ "alias": "h",
+ "description": "Print the generator's options and usage"
+ },
+ "skip-cache": {
+ "name": "skip-cache",
+ "type": "Boolean",
+ "description": "Do not remember prompt answers",
+ "default": false
+ },
+ "skip-install": {
+ "name": "skip-install",
+ "type": "Boolean",
+ "description": "Do not automatically install dependencies",
+ "default": false
+ },
+ "force-install": {
+ "name": "force-install",
+ "type": "Boolean",
+ "description": "Fail on install dependencies error",
+ "default": false
+ },
+ "ask-answered": {
+ "type": "Boolean",
+ "description": "Show prompts for already configured options",
+ "default": false,
+ "name": "ask-answered",
+ "hide": false
+ },
+ "out": {
+ "alias": "o",
+ "type": "String",
+ "required": false,
+ "description": "File name for the OpenAPI spec to be written",
+ "name": "out",
+ "hide": false
+ },
+ "config": {
+ "type": "String",
+ "alias": "c",
+ "description": "JSON file name or value to configure options",
+ "name": "config",
+ "hide": false
+ },
+ "yes": {
+ "type": "Boolean",
+ "alias": "y",
+ "description": "Skip all confirmation prompts with default or provided value",
+ "name": "yes",
+ "hide": false
+ },
+ "format": {
+ "type": "Boolean",
+ "description": "Format generated code using npm run lint:fix",
+ "name": "format",
+ "hide": false
+ },
+ "packageManager": {
+ "type": "String",
+ "description": "Change the default package manager",
+ "alias": "pm",
+ "name": "packageManager",
+ "hide": false
+ }
+ },
+ "arguments": [],
+ "name": "openapi-spec"
}
}
}
diff --git a/packages/cli/generators/openapi-spec/index.js b/packages/cli/generators/openapi-spec/index.js
new file mode 100644
index 000000000000..d694bd816197
--- /dev/null
+++ b/packages/cli/generators/openapi-spec/index.js
@@ -0,0 +1,79 @@
+// Copyright IBM Corp. 2019,2020. All Rights Reserved.
+// Node module: @loopback/cli
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+'use strict';
+
+const BaseGenerator = require('../../lib/base-generator');
+const g = require('../../lib/globalize');
+const {dump} = require('js-yaml');
+
+module.exports = class OpenApiSpecGenerator extends BaseGenerator {
+ // Note: arguments and options should be defined in the constructor.
+ constructor(args, opts) {
+ super(args, opts);
+ }
+
+ _setupGenerator() {
+ this.option('out', {
+ alias: 'o',
+ type: String,
+ required: false,
+ description: g.f('File name for the OpenAPI spec to be written'),
+ });
+ return super._setupGenerator();
+ }
+
+ setOptions() {
+ return super.setOptions();
+ }
+
+ checkLoopBackProject() {
+ if (this.shouldExit()) return;
+ return super.checkLoopBackProject();
+ }
+
+ async promptFileName() {
+ if (this.shouldExit()) return;
+ if (this.options.out) {
+ this.outFile = this.options.out;
+ return;
+ }
+ const prompts = [
+ {
+ type: 'input',
+ name: 'outFile',
+ message: g.f('File name for the OpenAPI spec:'),
+ default: 'dist/openapi.json',
+ },
+ ];
+ const answers = await this.prompt(prompts);
+ this.outFile = answers && answers.outFile;
+ }
+
+ async saveSpec() {
+ if (this.shouldExit()) return;
+ this.log('Loading application %s...', this.packageJson.name);
+ // Load the `main` function
+ const main = require(this.destinationRoot()).main;
+ const app = await main({rest: {listenOnStart: false}});
+
+ // Get the OpenAPI spec
+ const spec = await app.restServer.getApiSpec();
+ const outFile = this.outFile || 'dist/openapi.json';
+ const fileName = this.outFile.toLowerCase();
+ if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) {
+ const yaml = dump(spec);
+ this.fs.write(outFile, yaml);
+ } else {
+ this.fs.writeJSON(outFile, spec, null, 2);
+ }
+ await app.stop();
+ }
+
+ async end() {
+ this.log('The OpenAPI spec has been saved to %s.', this.outFile);
+ await super.end();
+ }
+};
diff --git a/packages/cli/lib/cli.js b/packages/cli/lib/cli.js
index 3c1c59b2f832..830d5d5c4674 100644
--- a/packages/cli/lib/cli.js
+++ b/packages/cli/lib/cli.js
@@ -116,6 +116,10 @@ function setupGenerators() {
path.join(__dirname, '../generators/copyright'),
PREFIX + 'copyright',
);
+ env.register(
+ path.join(__dirname, '../generators/openapi-spec'),
+ PREFIX + 'openapi-spec',
+ );
return env;
}
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 1afdd6ffffd2..acf1f7e72a71 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -53,6 +53,7 @@
"fs-extra": "^9.0.0",
"glob": "^7.1.6",
"inquirer-autocomplete-prompt": "^1.0.2",
+ "js-yaml": "^3.13.1",
"json5": "^2.1.3",
"latest-version": "^5.1.0",
"lodash": "^4.17.15",
diff --git a/packages/cli/snapshots/integration/cli/cli.integration.snapshots.js b/packages/cli/snapshots/integration/cli/cli.integration.snapshots.js
index e363653a1b7b..f81178adf021 100644
--- a/packages/cli/snapshots/integration/cli/cli.integration.snapshots.js
+++ b/packages/cli/snapshots/integration/cli/cli.integration.snapshots.js
@@ -26,6 +26,7 @@ Available commands:
lb4 update
lb4 rest-crud
lb4 copyright
+ lb4 openapi-spec
lb4 install-completion
lb4 uninstall-completion
`;
@@ -50,6 +51,7 @@ Available commands:
lb4 update
lb4 rest-crud
lb4 copyright
+ lb4 openapi-spec
lb4 install-completion
lb4 uninstall-completion
`;
@@ -1701,6 +1703,78 @@ exports[`cli saves command metadata to .yo-rc.json 1`] = `
},
"arguments": [],
"name": "copyright"
+ },
+ "openapi-spec": {
+ "options": {
+ "help": {
+ "name": "help",
+ "type": "Boolean",
+ "alias": "h",
+ "description": "Print the generator's options and usage"
+ },
+ "skip-cache": {
+ "name": "skip-cache",
+ "type": "Boolean",
+ "description": "Do not remember prompt answers",
+ "default": false
+ },
+ "skip-install": {
+ "name": "skip-install",
+ "type": "Boolean",
+ "description": "Do not automatically install dependencies",
+ "default": false
+ },
+ "force-install": {
+ "name": "force-install",
+ "type": "Boolean",
+ "description": "Fail on install dependencies error",
+ "default": false
+ },
+ "ask-answered": {
+ "type": "Boolean",
+ "description": "Show prompts for already configured options",
+ "default": false,
+ "name": "ask-answered",
+ "hide": false
+ },
+ "out": {
+ "alias": "o",
+ "type": "String",
+ "required": false,
+ "description": "File name for the OpenAPI spec to be written",
+ "name": "out",
+ "hide": false
+ },
+ "config": {
+ "type": "String",
+ "alias": "c",
+ "description": "JSON file name or value to configure options",
+ "name": "config",
+ "hide": false
+ },
+ "yes": {
+ "type": "Boolean",
+ "alias": "y",
+ "description": "Skip all confirmation prompts with default or provided value",
+ "name": "yes",
+ "hide": false
+ },
+ "format": {
+ "type": "Boolean",
+ "description": "Format generated code using npm run lint:fix",
+ "name": "format",
+ "hide": false
+ },
+ "packageManager": {
+ "type": "String",
+ "description": "Change the default package manager",
+ "alias": "pm",
+ "name": "packageManager",
+ "hide": false
+ }
+ },
+ "arguments": [],
+ "name": "openapi-spec"
}
}
}
diff --git a/packages/cli/snapshots/integration/generators/openapi-spec.integration.snapshots.js b/packages/cli/snapshots/integration/generators/openapi-spec.integration.snapshots.js
new file mode 100644
index 000000000000..a7f50178bb5f
--- /dev/null
+++ b/packages/cli/snapshots/integration/generators/openapi-spec.integration.snapshots.js
@@ -0,0 +1,1594 @@
+// IMPORTANT
+// This snapshot file is auto-generated, but designed for humans.
+// It should be checked into source control and tracked carefully.
+// Re-generate by setting UPDATE_SNAPSHOTS=1 and running tests.
+// Make sure to inspect the changes in the snapshots below.
+// Do not ignore changes!
+
+'use strict';
+
+exports[`lb4 openapi-spec generates json spec to dist/openapi.json 1`] = `
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "@loopback/example-todo",
+ "version": "3.4.1",
+ "description": "Tutorial example on how to build an application with LoopBack 4.",
+ "contact": {
+ "name": "IBM Corp."
+ }
+ },
+ "paths": {
+ "/todos/{id}": {
+ "put": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "replaceTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "204": {
+ "description": "Todo PUT success"
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ },
+ "x-parameter-index": 1
+ },
+ "operationId": "TodoController.replaceTodo"
+ },
+ "patch": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "updateTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "204": {
+ "description": "Todo PATCH success"
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TodoPartial"
+ }
+ }
+ },
+ "x-parameter-index": 1
+ },
+ "operationId": "TodoController.updateTodo"
+ },
+ "get": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "findTodoById",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "200": {
+ "description": "Todo model instance",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ },
+ {
+ "name": "items",
+ "in": "query",
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ ],
+ "operationId": "TodoController.findTodoById"
+ },
+ "delete": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "deleteTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "204": {
+ "description": "Todo DELETE success"
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ }
+ ],
+ "operationId": "TodoController.deleteTodo"
+ }
+ },
+ "/todos": {
+ "post": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "createTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "200": {
+ "description": "Todo model instance",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NewTodo"
+ }
+ }
+ }
+ },
+ "operationId": "TodoController.createTodo"
+ },
+ "get": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "findTodos",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "200": {
+ "description": "Array of Todo model instances",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "filter",
+ "in": "query",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo.Filter"
+ }
+ }
+ }
+ }
+ ],
+ "operationId": "TodoController.findTodos"
+ }
+ }
+ },
+ "servers": [
+ {
+ "url": "/"
+ }
+ ],
+ "components": {
+ "schemas": {
+ "Todo": {
+ "title": "Todo",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "title": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "string"
+ },
+ "remindAtGeo": {
+ "type": "string"
+ },
+ "tag": {}
+ },
+ "required": [
+ "title"
+ ],
+ "additionalProperties": false
+ },
+ "NewTodo": {
+ "title": "NewTodo",
+ "description": "(tsType: Omit, schemaOptions: { title: 'NewTodo', exclude: [ 'id' ] })",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "string"
+ },
+ "remindAtGeo": {
+ "type": "string"
+ },
+ "tag": {}
+ },
+ "required": [
+ "title"
+ ],
+ "additionalProperties": false,
+ "x-typescript-type": "Omit"
+ },
+ "TodoPartial": {
+ "title": "TodoPartial",
+ "description": "(tsType: Partial, schemaOptions: { partial: true })",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "title": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "string"
+ },
+ "remindAtGeo": {
+ "type": "string"
+ },
+ "tag": {}
+ },
+ "additionalProperties": false,
+ "x-typescript-type": "Partial"
+ },
+ "Todo.Fields": {
+ "title": "Todo.Fields",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "boolean"
+ },
+ "title": {
+ "type": "boolean"
+ },
+ "desc": {
+ "type": "boolean"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "boolean"
+ },
+ "remindAtGeo": {
+ "type": "boolean"
+ },
+ "tag": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
+ "Todo.Filter": {
+ "type": "object",
+ "title": "Todo.Filter",
+ "properties": {
+ "offset": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "example": 100
+ },
+ "skip": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "order": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "where": {
+ "title": "Todo.WhereFilter",
+ "type": "object",
+ "additionalProperties": true
+ },
+ "fields": {
+ "$ref": "#/components/schemas/Todo.Fields"
+ }
+ },
+ "additionalProperties": false,
+ "x-typescript-type": "@loopback/repository#Filter"
+ }
+ }
+ }
+}
+
+`;
+
+
+exports[`lb4 openapi-spec generates json spec with --out 1`] = `
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "@loopback/example-todo",
+ "version": "3.4.1",
+ "description": "Tutorial example on how to build an application with LoopBack 4.",
+ "contact": {
+ "name": "IBM Corp."
+ }
+ },
+ "paths": {
+ "/todos/{id}": {
+ "put": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "replaceTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "204": {
+ "description": "Todo PUT success"
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ },
+ "x-parameter-index": 1
+ },
+ "operationId": "TodoController.replaceTodo"
+ },
+ "patch": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "updateTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "204": {
+ "description": "Todo PATCH success"
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TodoPartial"
+ }
+ }
+ },
+ "x-parameter-index": 1
+ },
+ "operationId": "TodoController.updateTodo"
+ },
+ "get": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "findTodoById",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "200": {
+ "description": "Todo model instance",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ },
+ {
+ "name": "items",
+ "in": "query",
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ ],
+ "operationId": "TodoController.findTodoById"
+ },
+ "delete": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "deleteTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "204": {
+ "description": "Todo DELETE success"
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ }
+ ],
+ "operationId": "TodoController.deleteTodo"
+ }
+ },
+ "/todos": {
+ "post": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "createTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "200": {
+ "description": "Todo model instance",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NewTodo"
+ }
+ }
+ }
+ },
+ "operationId": "TodoController.createTodo"
+ },
+ "get": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "findTodos",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "200": {
+ "description": "Array of Todo model instances",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "filter",
+ "in": "query",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo.Filter"
+ }
+ }
+ }
+ }
+ ],
+ "operationId": "TodoController.findTodos"
+ }
+ }
+ },
+ "servers": [
+ {
+ "url": "/"
+ }
+ ],
+ "components": {
+ "schemas": {
+ "Todo": {
+ "title": "Todo",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "title": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "string"
+ },
+ "remindAtGeo": {
+ "type": "string"
+ },
+ "tag": {}
+ },
+ "required": [
+ "title"
+ ],
+ "additionalProperties": false
+ },
+ "NewTodo": {
+ "title": "NewTodo",
+ "description": "(tsType: Omit, schemaOptions: { title: 'NewTodo', exclude: [ 'id' ] })",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "string"
+ },
+ "remindAtGeo": {
+ "type": "string"
+ },
+ "tag": {}
+ },
+ "required": [
+ "title"
+ ],
+ "additionalProperties": false,
+ "x-typescript-type": "Omit"
+ },
+ "TodoPartial": {
+ "title": "TodoPartial",
+ "description": "(tsType: Partial, schemaOptions: { partial: true })",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "title": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "string"
+ },
+ "remindAtGeo": {
+ "type": "string"
+ },
+ "tag": {}
+ },
+ "additionalProperties": false,
+ "x-typescript-type": "Partial"
+ },
+ "Todo.Fields": {
+ "title": "Todo.Fields",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "boolean"
+ },
+ "title": {
+ "type": "boolean"
+ },
+ "desc": {
+ "type": "boolean"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "boolean"
+ },
+ "remindAtGeo": {
+ "type": "boolean"
+ },
+ "tag": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
+ "Todo.Filter": {
+ "type": "object",
+ "title": "Todo.Filter",
+ "properties": {
+ "offset": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "example": 100
+ },
+ "skip": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "order": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "where": {
+ "title": "Todo.WhereFilter",
+ "type": "object",
+ "additionalProperties": true
+ },
+ "fields": {
+ "$ref": "#/components/schemas/Todo.Fields"
+ }
+ },
+ "additionalProperties": false,
+ "x-typescript-type": "@loopback/repository#Filter"
+ }
+ }
+ }
+}
+
+`;
+
+
+exports[`lb4 openapi-spec generates json spec with outFile prompt 1`] = `
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "@loopback/example-todo",
+ "version": "3.4.1",
+ "description": "Tutorial example on how to build an application with LoopBack 4.",
+ "contact": {
+ "name": "IBM Corp."
+ }
+ },
+ "paths": {
+ "/todos/{id}": {
+ "put": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "replaceTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "204": {
+ "description": "Todo PUT success"
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ },
+ "x-parameter-index": 1
+ },
+ "operationId": "TodoController.replaceTodo"
+ },
+ "patch": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "updateTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "204": {
+ "description": "Todo PATCH success"
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TodoPartial"
+ }
+ }
+ },
+ "x-parameter-index": 1
+ },
+ "operationId": "TodoController.updateTodo"
+ },
+ "get": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "findTodoById",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "200": {
+ "description": "Todo model instance",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ },
+ {
+ "name": "items",
+ "in": "query",
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ ],
+ "operationId": "TodoController.findTodoById"
+ },
+ "delete": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "deleteTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "204": {
+ "description": "Todo DELETE success"
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ }
+ ],
+ "operationId": "TodoController.deleteTodo"
+ }
+ },
+ "/todos": {
+ "post": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "createTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "200": {
+ "description": "Todo model instance",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NewTodo"
+ }
+ }
+ }
+ },
+ "operationId": "TodoController.createTodo"
+ },
+ "get": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "findTodos",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "200": {
+ "description": "Array of Todo model instances",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "filter",
+ "in": "query",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo.Filter"
+ }
+ }
+ }
+ }
+ ],
+ "operationId": "TodoController.findTodos"
+ }
+ }
+ },
+ "servers": [
+ {
+ "url": "/"
+ }
+ ],
+ "components": {
+ "schemas": {
+ "Todo": {
+ "title": "Todo",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "title": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "string"
+ },
+ "remindAtGeo": {
+ "type": "string"
+ },
+ "tag": {}
+ },
+ "required": [
+ "title"
+ ],
+ "additionalProperties": false
+ },
+ "NewTodo": {
+ "title": "NewTodo",
+ "description": "(tsType: Omit, schemaOptions: { title: 'NewTodo', exclude: [ 'id' ] })",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "string"
+ },
+ "remindAtGeo": {
+ "type": "string"
+ },
+ "tag": {}
+ },
+ "required": [
+ "title"
+ ],
+ "additionalProperties": false,
+ "x-typescript-type": "Omit"
+ },
+ "TodoPartial": {
+ "title": "TodoPartial",
+ "description": "(tsType: Partial, schemaOptions: { partial: true })",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "title": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "string"
+ },
+ "remindAtGeo": {
+ "type": "string"
+ },
+ "tag": {}
+ },
+ "additionalProperties": false,
+ "x-typescript-type": "Partial"
+ },
+ "Todo.Fields": {
+ "title": "Todo.Fields",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "boolean"
+ },
+ "title": {
+ "type": "boolean"
+ },
+ "desc": {
+ "type": "boolean"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "boolean"
+ },
+ "remindAtGeo": {
+ "type": "boolean"
+ },
+ "tag": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
+ "Todo.Filter": {
+ "type": "object",
+ "title": "Todo.Filter",
+ "properties": {
+ "offset": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "example": 100
+ },
+ "skip": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "order": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "where": {
+ "title": "Todo.WhereFilter",
+ "type": "object",
+ "additionalProperties": true
+ },
+ "fields": {
+ "$ref": "#/components/schemas/Todo.Fields"
+ }
+ },
+ "additionalProperties": false,
+ "x-typescript-type": "@loopback/repository#Filter"
+ }
+ }
+ }
+}
+
+`;
+
+
+exports[`lb4 openapi-spec generates yaml spec with --out 1`] = `
+openapi: 3.0.0
+info:
+ title: '@loopback/example-todo'
+ version: 3.4.1
+ description: Tutorial example on how to build an application with LoopBack 4.
+ contact:
+ name: IBM Corp.
+paths:
+ '/todos/{id}':
+ put:
+ x-controller-name: TodoController
+ x-operation-name: replaceTodo
+ tags:
+ - TodoController
+ responses:
+ '204':
+ description: Todo PUT success
+ parameters:
+ - name: id
+ in: path
+ schema:
+ type: number
+ required: true
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Todo'
+ x-parameter-index: 1
+ operationId: TodoController.replaceTodo
+ patch:
+ x-controller-name: TodoController
+ x-operation-name: updateTodo
+ tags:
+ - TodoController
+ responses:
+ '204':
+ description: Todo PATCH success
+ parameters:
+ - name: id
+ in: path
+ schema:
+ type: number
+ required: true
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TodoPartial'
+ x-parameter-index: 1
+ operationId: TodoController.updateTodo
+ get:
+ x-controller-name: TodoController
+ x-operation-name: findTodoById
+ tags:
+ - TodoController
+ responses:
+ '200':
+ description: Todo model instance
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Todo'
+ parameters:
+ - name: id
+ in: path
+ schema:
+ type: number
+ required: true
+ - name: items
+ in: query
+ schema:
+ type: boolean
+ operationId: TodoController.findTodoById
+ delete:
+ x-controller-name: TodoController
+ x-operation-name: deleteTodo
+ tags:
+ - TodoController
+ responses:
+ '204':
+ description: Todo DELETE success
+ parameters:
+ - name: id
+ in: path
+ schema:
+ type: number
+ required: true
+ operationId: TodoController.deleteTodo
+ /todos:
+ post:
+ x-controller-name: TodoController
+ x-operation-name: createTodo
+ tags:
+ - TodoController
+ responses:
+ '200':
+ description: Todo model instance
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Todo'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/NewTodo'
+ operationId: TodoController.createTodo
+ get:
+ x-controller-name: TodoController
+ x-operation-name: findTodos
+ tags:
+ - TodoController
+ responses:
+ '200':
+ description: Array of Todo model instances
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Todo'
+ parameters:
+ - name: filter
+ in: query
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Todo.Filter'
+ operationId: TodoController.findTodos
+servers:
+ - url: /
+components:
+ schemas:
+ Todo:
+ title: Todo
+ properties:
+ id:
+ type: number
+ title:
+ type: string
+ desc:
+ type: string
+ isComplete:
+ type: boolean
+ remindAtAddress:
+ type: string
+ remindAtGeo:
+ type: string
+ tag: {}
+ required:
+ - title
+ additionalProperties: false
+ NewTodo:
+ title: NewTodo
+ description: >-
+ (tsType: Omit, schemaOptions: { title: 'NewTodo', exclude: [
+ 'id' ] })
+ properties:
+ title:
+ type: string
+ desc:
+ type: string
+ isComplete:
+ type: boolean
+ remindAtAddress:
+ type: string
+ remindAtGeo:
+ type: string
+ tag: {}
+ required:
+ - title
+ additionalProperties: false
+ x-typescript-type: 'Omit'
+ TodoPartial:
+ title: TodoPartial
+ description: '(tsType: Partial, schemaOptions: { partial: true })'
+ properties:
+ id:
+ type: number
+ title:
+ type: string
+ desc:
+ type: string
+ isComplete:
+ type: boolean
+ remindAtAddress:
+ type: string
+ remindAtGeo:
+ type: string
+ tag: {}
+ additionalProperties: false
+ x-typescript-type: Partial
+ Todo.Fields:
+ title: Todo.Fields
+ type: object
+ properties:
+ id:
+ type: boolean
+ title:
+ type: boolean
+ desc:
+ type: boolean
+ isComplete:
+ type: boolean
+ remindAtAddress:
+ type: boolean
+ remindAtGeo:
+ type: boolean
+ tag:
+ type: boolean
+ additionalProperties: false
+ Todo.Filter:
+ type: object
+ title: Todo.Filter
+ properties:
+ offset:
+ type: integer
+ minimum: 0
+ limit:
+ type: integer
+ minimum: 1
+ example: 100
+ skip:
+ type: integer
+ minimum: 0
+ order:
+ type: array
+ items:
+ type: string
+ where:
+ title: Todo.WhereFilter
+ type: object
+ additionalProperties: true
+ fields:
+ $ref: '#/components/schemas/Todo.Fields'
+ additionalProperties: false
+ x-typescript-type: '@loopback/repository#Filter'
+
+`;
+
+
+exports[`lb4 openapi-spec generates yml spec with --out 1`] = `
+openapi: 3.0.0
+info:
+ title: '@loopback/example-todo'
+ version: 3.4.1
+ description: Tutorial example on how to build an application with LoopBack 4.
+ contact:
+ name: IBM Corp.
+paths:
+ '/todos/{id}':
+ put:
+ x-controller-name: TodoController
+ x-operation-name: replaceTodo
+ tags:
+ - TodoController
+ responses:
+ '204':
+ description: Todo PUT success
+ parameters:
+ - name: id
+ in: path
+ schema:
+ type: number
+ required: true
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Todo'
+ x-parameter-index: 1
+ operationId: TodoController.replaceTodo
+ patch:
+ x-controller-name: TodoController
+ x-operation-name: updateTodo
+ tags:
+ - TodoController
+ responses:
+ '204':
+ description: Todo PATCH success
+ parameters:
+ - name: id
+ in: path
+ schema:
+ type: number
+ required: true
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TodoPartial'
+ x-parameter-index: 1
+ operationId: TodoController.updateTodo
+ get:
+ x-controller-name: TodoController
+ x-operation-name: findTodoById
+ tags:
+ - TodoController
+ responses:
+ '200':
+ description: Todo model instance
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Todo'
+ parameters:
+ - name: id
+ in: path
+ schema:
+ type: number
+ required: true
+ - name: items
+ in: query
+ schema:
+ type: boolean
+ operationId: TodoController.findTodoById
+ delete:
+ x-controller-name: TodoController
+ x-operation-name: deleteTodo
+ tags:
+ - TodoController
+ responses:
+ '204':
+ description: Todo DELETE success
+ parameters:
+ - name: id
+ in: path
+ schema:
+ type: number
+ required: true
+ operationId: TodoController.deleteTodo
+ /todos:
+ post:
+ x-controller-name: TodoController
+ x-operation-name: createTodo
+ tags:
+ - TodoController
+ responses:
+ '200':
+ description: Todo model instance
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Todo'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/NewTodo'
+ operationId: TodoController.createTodo
+ get:
+ x-controller-name: TodoController
+ x-operation-name: findTodos
+ tags:
+ - TodoController
+ responses:
+ '200':
+ description: Array of Todo model instances
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Todo'
+ parameters:
+ - name: filter
+ in: query
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Todo.Filter'
+ operationId: TodoController.findTodos
+servers:
+ - url: /
+components:
+ schemas:
+ Todo:
+ title: Todo
+ properties:
+ id:
+ type: number
+ title:
+ type: string
+ desc:
+ type: string
+ isComplete:
+ type: boolean
+ remindAtAddress:
+ type: string
+ remindAtGeo:
+ type: string
+ tag: {}
+ required:
+ - title
+ additionalProperties: false
+ NewTodo:
+ title: NewTodo
+ description: >-
+ (tsType: Omit, schemaOptions: { title: 'NewTodo', exclude: [
+ 'id' ] })
+ properties:
+ title:
+ type: string
+ desc:
+ type: string
+ isComplete:
+ type: boolean
+ remindAtAddress:
+ type: string
+ remindAtGeo:
+ type: string
+ tag: {}
+ required:
+ - title
+ additionalProperties: false
+ x-typescript-type: 'Omit'
+ TodoPartial:
+ title: TodoPartial
+ description: '(tsType: Partial, schemaOptions: { partial: true })'
+ properties:
+ id:
+ type: number
+ title:
+ type: string
+ desc:
+ type: string
+ isComplete:
+ type: boolean
+ remindAtAddress:
+ type: string
+ remindAtGeo:
+ type: string
+ tag: {}
+ additionalProperties: false
+ x-typescript-type: Partial
+ Todo.Fields:
+ title: Todo.Fields
+ type: object
+ properties:
+ id:
+ type: boolean
+ title:
+ type: boolean
+ desc:
+ type: boolean
+ isComplete:
+ type: boolean
+ remindAtAddress:
+ type: boolean
+ remindAtGeo:
+ type: boolean
+ tag:
+ type: boolean
+ additionalProperties: false
+ Todo.Filter:
+ type: object
+ title: Todo.Filter
+ properties:
+ offset:
+ type: integer
+ minimum: 0
+ limit:
+ type: integer
+ minimum: 1
+ example: 100
+ skip:
+ type: integer
+ minimum: 0
+ order:
+ type: array
+ items:
+ type: string
+ where:
+ title: Todo.WhereFilter
+ type: object
+ additionalProperties: true
+ fields:
+ $ref: '#/components/schemas/Todo.Fields'
+ additionalProperties: false
+ x-typescript-type: '@loopback/repository#Filter'
+
+`;
diff --git a/packages/cli/test/fixtures/openapi-spec/index.js b/packages/cli/test/fixtures/openapi-spec/index.js
new file mode 100644
index 000000000000..f0072dcf4cdf
--- /dev/null
+++ b/packages/cli/test/fixtures/openapi-spec/index.js
@@ -0,0 +1,14 @@
+const fs = require('fs');
+
+exports.SANDBOX_FILES = [
+ {
+ path: '.',
+ file: 'index.js',
+ content: fs.readFileSync(require.resolve('./index.js.txt')),
+ },
+ {
+ path: '.',
+ file: 'openapi.json',
+ content: fs.readFileSync(require.resolve('./openapi.json')),
+ },
+];
diff --git a/packages/cli/test/fixtures/openapi-spec/index.js.txt b/packages/cli/test/fixtures/openapi-spec/index.js.txt
new file mode 100644
index 000000000000..819f5433776a
--- /dev/null
+++ b/packages/cli/test/fixtures/openapi-spec/index.js.txt
@@ -0,0 +1,16 @@
+// Copyright IBM Corp. 2020. All Rights Reserved.
+// Node module: @loopback/example-rest-crud
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+exports.main = function () {
+ const app = {
+ restServer: {
+ getApiSpec() {
+ return require('./openapi.json');
+ },
+ },
+ stop() {},
+ };
+ return app;
+};
diff --git a/packages/cli/test/fixtures/openapi-spec/openapi.json b/packages/cli/test/fixtures/openapi-spec/openapi.json
new file mode 100644
index 000000000000..406430c5b395
--- /dev/null
+++ b/packages/cli/test/fixtures/openapi-spec/openapi.json
@@ -0,0 +1,362 @@
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "@loopback/example-todo",
+ "version": "3.4.1",
+ "description": "Tutorial example on how to build an application with LoopBack 4.",
+ "contact": {
+ "name": "IBM Corp."
+ }
+ },
+ "paths": {
+ "/todos/{id}": {
+ "put": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "replaceTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "204": {
+ "description": "Todo PUT success"
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ },
+ "x-parameter-index": 1
+ },
+ "operationId": "TodoController.replaceTodo"
+ },
+ "patch": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "updateTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "204": {
+ "description": "Todo PATCH success"
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TodoPartial"
+ }
+ }
+ },
+ "x-parameter-index": 1
+ },
+ "operationId": "TodoController.updateTodo"
+ },
+ "get": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "findTodoById",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "200": {
+ "description": "Todo model instance",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ },
+ {
+ "name": "items",
+ "in": "query",
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ ],
+ "operationId": "TodoController.findTodoById"
+ },
+ "delete": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "deleteTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "204": {
+ "description": "Todo DELETE success"
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "schema": {
+ "type": "number"
+ },
+ "required": true
+ }
+ ],
+ "operationId": "TodoController.deleteTodo"
+ }
+ },
+ "/todos": {
+ "post": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "createTodo",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "200": {
+ "description": "Todo model instance",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NewTodo"
+ }
+ }
+ }
+ },
+ "operationId": "TodoController.createTodo"
+ },
+ "get": {
+ "x-controller-name": "TodoController",
+ "x-operation-name": "findTodos",
+ "tags": [
+ "TodoController"
+ ],
+ "responses": {
+ "200": {
+ "description": "Array of Todo model instances",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "filter",
+ "in": "query",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Todo.Filter"
+ }
+ }
+ }
+ }
+ ],
+ "operationId": "TodoController.findTodos"
+ }
+ }
+ },
+ "servers": [
+ {
+ "url": "/"
+ }
+ ],
+ "components": {
+ "schemas": {
+ "Todo": {
+ "title": "Todo",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "title": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "string"
+ },
+ "remindAtGeo": {
+ "type": "string"
+ },
+ "tag": {}
+ },
+ "required": [
+ "title"
+ ],
+ "additionalProperties": false
+ },
+ "NewTodo": {
+ "title": "NewTodo",
+ "description": "(tsType: Omit, schemaOptions: { title: 'NewTodo', exclude: [ 'id' ] })",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "string"
+ },
+ "remindAtGeo": {
+ "type": "string"
+ },
+ "tag": {}
+ },
+ "required": [
+ "title"
+ ],
+ "additionalProperties": false,
+ "x-typescript-type": "Omit"
+ },
+ "TodoPartial": {
+ "title": "TodoPartial",
+ "description": "(tsType: Partial, schemaOptions: { partial: true })",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "title": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "string"
+ },
+ "remindAtGeo": {
+ "type": "string"
+ },
+ "tag": {}
+ },
+ "additionalProperties": false,
+ "x-typescript-type": "Partial"
+ },
+ "Todo.Fields": {
+ "title": "Todo.Fields",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "boolean"
+ },
+ "title": {
+ "type": "boolean"
+ },
+ "desc": {
+ "type": "boolean"
+ },
+ "isComplete": {
+ "type": "boolean"
+ },
+ "remindAtAddress": {
+ "type": "boolean"
+ },
+ "remindAtGeo": {
+ "type": "boolean"
+ },
+ "tag": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
+ "Todo.Filter": {
+ "type": "object",
+ "title": "Todo.Filter",
+ "properties": {
+ "offset": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "example": 100
+ },
+ "skip": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "order": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "where": {
+ "title": "Todo.WhereFilter",
+ "type": "object",
+ "additionalProperties": true
+ },
+ "fields": {
+ "$ref": "#/components/schemas/Todo.Fields"
+ }
+ },
+ "additionalProperties": false,
+ "x-typescript-type": "@loopback/repository#Filter"
+ }
+ }
+ }
+}
diff --git a/packages/cli/test/integration/generators/openapi-spec.integration.js b/packages/cli/test/integration/generators/openapi-spec.integration.js
new file mode 100644
index 000000000000..be1ffc11e151
--- /dev/null
+++ b/packages/cli/test/integration/generators/openapi-spec.integration.js
@@ -0,0 +1,85 @@
+// Copyright IBM Corp. 2018,2020. All Rights Reserved.
+// Node module: @loopback/cli
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+'use strict';
+
+const path = require('path');
+const testlab = require('@loopback/testlab');
+
+const TestSandbox = testlab.TestSandbox;
+
+const generator = path.join(__dirname, '../../../generators/openapi-spec');
+const SANDBOX_FILES = require('../../fixtures/openapi-spec').SANDBOX_FILES;
+const testUtils = require('../../test-utils');
+const {expectFileToMatchSnapshot} = require('../../snapshots');
+
+// Test Sandbox
+const sandbox = new TestSandbox(path.resolve(__dirname, '../.sandbox'));
+
+describe('lb4 openapi-spec', () => {
+ beforeEach('reset sandbox', async () => {
+ await sandbox.reset();
+ });
+
+ it('generates json spec with --out', async () => {
+ await testUtils
+ .executeGenerator(generator)
+ .inDir(sandbox.path, () =>
+ testUtils.givenLBProject(sandbox.path, {
+ additionalFiles: SANDBOX_FILES,
+ }),
+ )
+ .withOptions({out: 'spec.json'});
+ expectFileToMatchSnapshot('spec.json');
+ });
+
+ it('generates json spec with outFile prompt', async () => {
+ await testUtils
+ .executeGenerator(generator)
+ .inDir(sandbox.path, () =>
+ testUtils.givenLBProject(sandbox.path, {
+ additionalFiles: SANDBOX_FILES,
+ }),
+ )
+ .withPrompts({outFile: 'spec.json'});
+ expectFileToMatchSnapshot('spec.json');
+ });
+
+ it('generates json spec to dist/openapi.json', async () => {
+ await testUtils
+ .executeGenerator(generator)
+ .inDir(sandbox.path, () =>
+ testUtils.givenLBProject(sandbox.path, {
+ additionalFiles: SANDBOX_FILES,
+ }),
+ )
+ .withPrompts({});
+ expectFileToMatchSnapshot('dist/openapi.json');
+ });
+
+ it('generates yaml spec with --out', async () => {
+ await testUtils
+ .executeGenerator(generator)
+ .inDir(sandbox.path, () =>
+ testUtils.givenLBProject(sandbox.path, {
+ additionalFiles: SANDBOX_FILES,
+ }),
+ )
+ .withOptions({out: 'spec.yaml'});
+ expectFileToMatchSnapshot('spec.yaml');
+ });
+
+ it('generates yml spec with --out', async () => {
+ await testUtils
+ .executeGenerator(generator)
+ .inDir(sandbox.path, () =>
+ testUtils.givenLBProject(sandbox.path, {
+ additionalFiles: SANDBOX_FILES,
+ }),
+ )
+ .withOptions({out: 'spec.yml'});
+ expectFileToMatchSnapshot('spec.yml');
+ });
+});