From fc39cfc554d05f8a27d4773184705d43a6e0e3d8 Mon Sep 17 00:00:00 2001 From: Khuda Dad Nomani <32505158+KhudaDad414@users.noreply.github.com> Date: Thu, 9 Feb 2023 11:51:14 +0000 Subject: [PATCH] feat: add optimize command (#397) --- README.md | 1 - docs/usage.md | 1 + package-lock.json | 393 +++++++++++++++++++++++++++-- package.json | 1 + src/commands/optimize.ts | 171 +++++++++++++ test/commands/optimize.test.ts | 140 ++++++++++ test/dummyspec/unoprimizedSpec.yml | 78 ++++++ test/specification-invalid.yml | 23 ++ 8 files changed, 792 insertions(+), 16 deletions(-) create mode 100644 src/commands/optimize.ts create mode 100644 test/commands/optimize.test.ts create mode 100644 test/dummyspec/unoprimizedSpec.yml create mode 100644 test/specification-invalid.yml diff --git a/README.md b/README.md index fbe2825ca10..d32884cb20e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ CLI to work with your AsyncAPI files. Currently under development, we are workin Learn how to install the AsyncAPI CLI from the [installation guide](/docs/installation.md). ## Usage - The [usage guide](/docs/usage.md) provides information about different ways to use the CLI. diff --git a/docs/usage.md b/docs/usage.md index 2bffbe5150b..43957083bd2 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -21,6 +21,7 @@ USAGE COMMANDS config access configs diff find diff between two AsyncAPI files + optimize optimizes your AsyncAPI file by removing unused components, re-using duplicates etc. new creates a new AsyncAPI file start starts a new local instance of Studio validate validate an AsyncAPI file diff --git a/package-lock.json b/package-lock.json index 52c12206c19..392de39c08c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@asyncapi/diff": "^0.4.1", "@asyncapi/generator": "^1.9.12", "@asyncapi/modelina": "^1.0.0-next.40", + "@asyncapi/optimizer": "^0.1.15", "@asyncapi/parser": "^2.0.0-next-major.11", "@asyncapi/studio": "^0.16.1", "@oclif/core": "^1.18.0", @@ -458,6 +459,90 @@ "@openapi-contrib/openapi-schema-to-json-schema": "^3.0.0" } }, + "node_modules/@asyncapi/optimizer": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/@asyncapi/optimizer/-/optimizer-0.1.15.tgz", + "integrity": "sha512-Ddtdjy/xJQrpiH30B4ztsneg22P2FWqu31uCbvMsoKfgv4R4XPQjpknlkLLJ47VbzxwwTonrVD9+B8+TFJLXGw==", + "dependencies": { + "@asyncapi/parser": "^1.17.2", + "js-yaml": "^4.1.0", + "jsonpath-plus": "^6.0.1", + "lodash": "^4.17.21", + "merge-deep": "^3.0.3" + } + }, + "node_modules/@asyncapi/optimizer/node_modules/@asyncapi/parser": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.17.2.tgz", + "integrity": "sha512-xI9GShIuQVfBz5IZ2c9h6cxcPTnfzI1fch6kYL+nF68Q3U669Un0K0TSeyEa57EfhTap53UIRTj7gFIM9QZTNQ==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@asyncapi/specs": "^4.0.1", + "@fmvilas/pseudo-yaml-ast": "^0.3.1", + "ajv": "^6.10.1", + "js-yaml": "^3.13.1", + "json-to-ast": "^2.1.0", + "lodash.clonedeep": "^4.5.0", + "node-fetch": "^2.6.0", + "tiny-merge-patch": "^0.1.2" + } + }, + "node_modules/@asyncapi/optimizer/node_modules/@asyncapi/parser/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@asyncapi/optimizer/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@asyncapi/optimizer/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@asyncapi/optimizer/node_modules/js-yaml/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/@asyncapi/optimizer/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/@asyncapi/optimizer/node_modules/jsonpath-plus": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-6.0.1.tgz", + "integrity": "sha512-EvGovdvau6FyLexFH2OeXfIITlgIbgZoAZe3usiySeaIDm5QS+A10DKNpaPBBqqRSZr2HN6HVNXxtwUAr2apEw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@asyncapi/parser": { "version": "2.0.0-next-major.11", "resolved": "https://registry.npmjs.org/@asyncapi/parser/-/parser-2.0.0-next-major.11.tgz", @@ -6321,6 +6406,14 @@ "deep-equal": "^2.0.5" } }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-differ": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", @@ -7480,6 +7573,40 @@ "node": ">= 0.10" } }, + "node_modules/clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==", + "dependencies": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-deep/node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/clone-stats": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", @@ -11577,7 +11704,17 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "dependencies": { + "for-in": "^1.0.1" + }, "engines": { "node": ">=0.10.0" } @@ -12962,8 +13099,7 @@ "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "node_modules/is-callable": { "version": "1.2.7", @@ -13019,7 +13155,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -13410,7 +13545,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -14610,7 +14744,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, "dependencies": { "is-buffer": "^1.1.5" }, @@ -15307,6 +15440,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-deep": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "dependencies": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -15562,6 +15708,26 @@ "node": ">=0.10.0" } }, + "node_modules/mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==", + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -21546,6 +21712,39 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==", + "dependencies": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -25378,6 +25577,83 @@ "@openapi-contrib/openapi-schema-to-json-schema": "^3.0.0" } }, + "@asyncapi/optimizer": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/@asyncapi/optimizer/-/optimizer-0.1.15.tgz", + "integrity": "sha512-Ddtdjy/xJQrpiH30B4ztsneg22P2FWqu31uCbvMsoKfgv4R4XPQjpknlkLLJ47VbzxwwTonrVD9+B8+TFJLXGw==", + "requires": { + "@asyncapi/parser": "^1.17.2", + "js-yaml": "^4.1.0", + "jsonpath-plus": "^6.0.1", + "lodash": "^4.17.21", + "merge-deep": "^3.0.3" + }, + "dependencies": { + "@asyncapi/parser": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.17.2.tgz", + "integrity": "sha512-xI9GShIuQVfBz5IZ2c9h6cxcPTnfzI1fch6kYL+nF68Q3U669Un0K0TSeyEa57EfhTap53UIRTj7gFIM9QZTNQ==", + "requires": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@asyncapi/specs": "^4.0.1", + "@fmvilas/pseudo-yaml-ast": "^0.3.1", + "ajv": "^6.10.1", + "js-yaml": "^3.13.1", + "json-to-ast": "^2.1.0", + "lodash.clonedeep": "^4.5.0", + "node-fetch": "^2.6.0", + "tiny-merge-patch": "^0.1.2" + }, + "dependencies": { + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + } + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "jsonpath-plus": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-6.0.1.tgz", + "integrity": "sha512-EvGovdvau6FyLexFH2OeXfIITlgIbgZoAZe3usiySeaIDm5QS+A10DKNpaPBBqqRSZr2HN6HVNXxtwUAr2apEw==" + } + } + }, "@asyncapi/parser": { "version": "2.0.0-next-major.11", "resolved": "https://registry.npmjs.org/@asyncapi/parser/-/parser-2.0.0-next-major.11.tgz", @@ -29857,6 +30133,11 @@ "deep-equal": "^2.0.5" } }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==" + }, "array-differ": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", @@ -30722,6 +31003,33 @@ "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", "dev": true }, + "clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==", + "requires": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==" + } + } + }, "clone-stats": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", @@ -33873,8 +34181,15 @@ "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "requires": { + "for-in": "^1.0.1" + } }, "foreach": { "version": "2.0.6", @@ -34931,8 +35246,7 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-callable": { "version": "1.2.7", @@ -34963,8 +35277,7 @@ "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==" }, "is-extglob": { "version": "2.1.1", @@ -35228,8 +35541,7 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" }, "isomorphic-dompurify": { "version": "0.13.0", @@ -36154,7 +36466,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -36725,6 +37036,16 @@ } } }, + "merge-deep": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "requires": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + } + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -36920,6 +37241,22 @@ } } }, + "mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==", + "requires": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "dependencies": { + "for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==" + } + } + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -41359,6 +41696,32 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==", + "requires": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "requires": { + "is-buffer": "^1.0.2" + } + }, + "lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==" + } + } + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 99730b89bb1..d2b940a784f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@asyncapi/converter": "^1.2.0", "@asyncapi/diff": "^0.4.1", "@asyncapi/generator": "^1.9.12", + "@asyncapi/optimizer": "^0.1.15", "@asyncapi/modelina": "^1.0.0-next.40", "@asyncapi/parser": "^2.0.0-next-major.11", "@asyncapi/studio": "^0.16.1", diff --git a/src/commands/optimize.ts b/src/commands/optimize.ts new file mode 100644 index 00000000000..59bd707592d --- /dev/null +++ b/src/commands/optimize.ts @@ -0,0 +1,171 @@ +import { Flags } from '@oclif/core'; +import { Optimizer, Output, Report, ReportElement } from '@asyncapi/optimizer'; +import Command from '../base'; +import { ValidationError } from '../errors/validation-error'; +import { load, Specification } from '../models/SpecificationFile'; +import * as inquirer from 'inquirer'; +import chalk from 'chalk'; +import { promises } from 'fs'; +import { Example } from '@oclif/core/lib/interfaces'; +const { writeFile } = promises; + +export enum Optimizations { + REMOVE_COMPONENTS='remove-components', + REUSE_COMPONENTS='reuse-components', + MOVE_TO_COMPONETS='move-to-components' +} + +export enum Outputs { + TERMINAL='terminal', + NEW_FILE='new-file', + OVERWRITE='overwrite' +} +export default class Optimize extends Command { + static description = 'optimize asyncapi specification file'; + isInteractive = false; + optimizations?: Optimizations[]; + outputMethod?: Outputs; + + static examples: Example[] = [ + 'asyncapi optimize ./asyncapi.yaml', + 'asyncapi optimize ./asyncapi.yaml --no-tty', + 'asyncapi optimize ./asyncapi.yaml --optimization=remove-components,reuse-components,move-to-components --no-tty', + 'asyncapi optimize ./asyncapi.yaml --optimization=remove-components,reuse-components,move-to-components --output=terminal --no-tty', + ]; + + static flags = { + help: Flags.help({ char: 'h' }), + optimization: Flags.string({char: 'p', default: Object.values(Optimizations), options: Object.values(Optimizations), multiple: true, description: 'select the type of optimizations that you want to apply.'}), + output: Flags.string({char: 'o', default: Outputs.TERMINAL, options: Object.values(Outputs), description: 'select where you want the output.'}), + 'no-tty': Flags.boolean({ description: 'do not use an interactive terminal', default: false }), + }; + + static args = [ + { name: 'spec-file', description: 'spec path, url, or context-name', required: false }, + ]; + + async run() { + const { args, flags } = await this.parse(Optimize); //NOSONAR + const filePath = args['spec-file']; + let specFile: Specification; + let optimizer: Optimizer; + let report: Report; + try { + specFile = await load(filePath); + optimizer = new Optimizer(specFile.text()); + report = await optimizer.getReport(); + } catch (err) { + this.error( + new ValidationError({ + type: 'invalid-file', + filepath: filePath, + }) + ); + } + this.isInteractive = !flags['no-tty']; + this.optimizations = flags.optimization as Optimizations[]; + this.outputMethod = flags.output as Outputs; + + if (!(report.moveToComponents?.length || report.removeComponents?.length || report.reuseComponents?.length)) { + this.log(`No optimization has been applied since ${specFile.getFilePath() ?? specFile.getFileURL()} looks optimized!`); + return; + } + + const isTTY = process.stdout.isTTY; + if (this.isInteractive && isTTY) { + await this.interactiveRun(report); + } + + try { + const optimizedDocument = optimizer.getOptimizedDocument({rules: { + moveToComponents: this.optimizations.includes(Optimizations.MOVE_TO_COMPONETS), + removeComponents: this.optimizations.includes(Optimizations.REMOVE_COMPONENTS), + reuseComponents: this.optimizations.includes(Optimizations.REUSE_COMPONENTS) + }, output: Output.YAML}); + + const specPath = specFile.getFilePath(); + let newPath = ''; + if (specPath) { + const pos = specPath.lastIndexOf('.'); + newPath = `${specPath.substring(0,pos) }_optimized.${ specPath.substring(pos+1)}`; + } else { + newPath = 'optimized-asyncapi.yaml'; + } + + if (this.outputMethod === Outputs.TERMINAL) { + this.log(optimizedDocument); + } else if (this.outputMethod === Outputs.NEW_FILE) { + await writeFile(newPath, optimizedDocument, { encoding: 'utf8' }); + this.log(`Created file ${newPath}...`); + } else if (this.outputMethod === Outputs.OVERWRITE) { + await writeFile(specPath ?? 'asyncapi.yaml', optimizedDocument, { encoding: 'utf8' }); + this.log(`Created file ${newPath}...`); + } + } catch (error) { + throw new ValidationError({ + type: 'parser-error', + err: error + }); + } + } + private showOptimizations(elements: ReportElement[] | undefined) { + if (!elements) { + return; + } + for (let i = 0; i < elements.length; i++) { + const element = elements[+i]; + if (element.action==='move') { + this.log(`${chalk.green('move')} ${element.path} to ${element.target} and reference it.`); + } else if (element.action==='reuse') { + this.log(`${chalk.green('reuse')} ${element.target} in ${element.path}.`); + } else if (element.action === 'remove') { + this.log(`${chalk.red('remove')} ${element.path}.`); + } + } + + this.log('\n'); + } + private async interactiveRun(report: Report) { + const canMove = report.moveToComponents?.length; + const canRemove = report.removeComponents?.length; + const canReuse = report.reuseComponents?.length; + const choices = []; + + if (canMove) { + const totalMove = report.moveToComponents?.filter((e) => e.action === 'move').length; + this.log(`\n${chalk.green(totalMove)} components can be moved to the components sections.\nthe following changes will be made:`); + this.showOptimizations(report.moveToComponents); + choices.push({name: 'move to components section', value: Optimizations.MOVE_TO_COMPONETS}); + } + if (canRemove) { + const totalMove = report.removeComponents?.length; + this.log(`${chalk.green(totalMove)} unused components can be removed.\nthe following changes will be made:`); + this.showOptimizations(report.removeComponents); + choices.push({name: 'remove components', value: Optimizations.REMOVE_COMPONENTS}); + } + if (canReuse) { + const totalMove = report.reuseComponents?.length; + this.log(`${chalk.green(totalMove)} components can be reused.\nthe following changes will be made:`); + this.showOptimizations(report.reuseComponents); + choices.push({name: 'reuse components', value: Optimizations.REUSE_COMPONENTS}); + } + const optimizationRes = await inquirer.prompt([{ + name: 'optimization', + message: 'select the type of optimization that you want to apply:', + type: 'checkbox', + default: 'all', + choices + }]); + + this.optimizations = optimizationRes.optimization; + + const outputRes = await inquirer.prompt([{ + name: 'output', + message: 'where do you want to save the result:', + type: 'list', + default: 'log to terminal', + choices: [{name: 'log to terminal',value: Outputs.TERMINAL}, {name: 'create new file', value: Outputs.NEW_FILE}, {name: 'update original', value: Outputs.OVERWRITE}] + }]); + this.outputMethod = outputRes.output; + } +} diff --git a/test/commands/optimize.test.ts b/test/commands/optimize.test.ts new file mode 100644 index 00000000000..53bc4ec432c --- /dev/null +++ b/test/commands/optimize.test.ts @@ -0,0 +1,140 @@ +import path from 'path'; +import { test } from '@oclif/test'; +import { NO_CONTEXTS_SAVED } from '../../src/errors/context-error'; +import TestHelper from '../testHelper'; +import inquirer from 'inquirer'; +import {Optimizations, Outputs} from '../../src/commands/optimize'; + +const testHelper = new TestHelper(); +const optimizedFilePath = './test/specification.yml'; +const unoptimizedFile = './test/dummyspec/unoprimizedSpec.yml'; +const invalidFile = './test/specification-invalid.yml'; + +describe('optimize', () => { + describe('no optimization needed', () => { + beforeEach(() => { + testHelper.createDummyContextFile(); + }); + + afterEach(() => { + testHelper.deleteDummyContextFile(); + }); + + test + .stderr() + .stdout() + .command(['optimize', optimizedFilePath]) + .it('works when file path is passed', (ctx, done) => { + expect(ctx.stdout).toContain(`No optimization has been applied since ${optimizedFilePath} looks optimized!`); + expect(ctx.stderr).toEqual(''); + done(); + }); + + test + .stderr() + .stdout() + .command(['optimize', './test/not-found.yml']) + .it('should throw error if file path is wrong', (ctx, done) => { + expect(ctx.stdout).toEqual(''); + expect(ctx.stderr).toContain('ValidationError'); + done(); + }); + + test + .stderr() + .stdout() + .command(['optimize', 'https://bit.ly/asyncapi']) + .it('works when url is passed', (ctx, done) => { + expect(ctx.stdout).toContain('No optimization has been applied since https://bit.ly/asyncapi looks optimized!'); + expect(ctx.stderr).toEqual(''); + done(); + }); + }); + + describe('with no arguments', () => { + beforeEach(() => { + testHelper.createDummyContextFile(); + }); + + afterEach(() => { + testHelper.setCurrentContext('home'); + testHelper.deleteDummyContextFile(); + }); + + test + .stderr() + .stdout() + .command(['optimize']) + .it('converts from current context', (ctx, done) => { + expect(ctx.stdout).toContain(`No optimization has been applied since ${path.resolve(__dirname, '../specification.yml')} looks optimized!`); + expect(ctx.stderr).toEqual(''); + done(); + }); + + test + .stderr() + .stdout() + .do(() => { + testHelper.unsetCurrentContext(); + testHelper.createDummyContextFile(); + }) + .command(['optimize']) + .it('throws error message if no current context', (ctx, done) => { + expect(ctx.stdout).toEqual(''); + expect(ctx.stderr).toContain('ValidationError'); + done(); + }); + + test + .stderr() + .stdout() + .do(() => { + testHelper.deleteDummyContextFile(); + }) + .command(['optimize']) + .it('throws error message if no context file exists', (ctx, done) => { + expect(ctx.stdout).toEqual(''); + expect(ctx.stderr).toContain('ValidationError'); + done(); + }); + }); + + describe('no-tty flag', () => { + test + .stderr() + .stdout() + .command(['optimize', unoptimizedFile, '--no-tty']) + .it('process without going to interactive mode.', (ctx, done) => { + expect(ctx.stdout).toContain('asyncapi: 2.0.0'); + expect(ctx.stderr).toEqual(''); + done(); + }); + }); + + describe('interactive terminal', () => { + test + .stub(inquirer, 'prompt', () => { + return Promise.resolve({optimization: [Optimizations.REMOVE_COMPONENTS] , output: Outputs.TERMINAL}); + }) + .stderr() + .stdout() + .command(['optimize', unoptimizedFile]) + .it('interactive terminal, only remove components and outputs to terminal', (ctx, done) => { + expect(ctx.stdout).toContain('asyncapi: 2.0.0'); + expect(ctx.stderr).toEqual(''); + done(); + }); + }); + describe('error if the asyncapi file is invalid', () => { + test + .stderr() + .stdout() + .command(['optimize',invalidFile]) + .it('give ValidationError', (ctx, done) => { + expect(ctx.stderr).toContain('ValidationError'); + expect(ctx.stdout).toEqual(''); + done(); + }); + }); +}); + diff --git a/test/dummyspec/unoprimizedSpec.yml b/test/dummyspec/unoprimizedSpec.yml new file mode 100644 index 00000000000..acbed10a65d --- /dev/null +++ b/test/dummyspec/unoprimizedSpec.yml @@ -0,0 +1,78 @@ +asyncapi: 2.0.0 +info: + title: Streetlights API + version: '1.0.0' +channels: + smartylighting/event/{streetlightId}/lighting/measured: + parameters: + #this parameter is duplicated. it can be moved to components and ref-ed from here. + streetlightId: + schema: + type: string + subscribe: + operationId: receiveLightMeasurement + traits: + - bindings: + kafka: + clientId: my-app-id + message: + name: lightMeasured + title: Light measured + contentType: application/json + traits: + - headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + payload: + type: object + properties: + lumens: + type: integer + minimum: 0 + #full form is used, we can ref it to: #/components/schemas/sentAt + sentAt: + type: string + format: date-time + smartylighting/action/{streetlightId}/turn/on: + parameters: + streetlightId: + schema: + type: string + publish: + operationId: turnOn + traits: + - bindings: + kafka: + clientId: my-app-id + message: + name: turnOnOff + title: Turn on/off + traits: + - headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + payload: + type: object + properties: + sentAt: + $ref: "#/components/schemas/sentAt" +components: + messages: + #libarary should be able to find and delete this message, because it is not used anywhere. + unusedMessage: + name: unusedMessage + title: This message is not used in any channel. + + schemas: + #this schema is ref-ed in one channel and used full form in another. library should be able to identify and ref the second channel as well. + sentAt: + type: string + format: date-time \ No newline at end of file diff --git a/test/specification-invalid.yml b/test/specification-invalid.yml new file mode 100644 index 00000000000..0cc34cb56f6 --- /dev/null +++ b/test/specification-invalid.yml @@ -0,0 +1,23 @@ +asyncapi: 2.2.0 +info: + title: Account Service +version: 1.0.0 +description: This service is in charge of processing user signups +channels: + user/signedup: + subscribe: + message: + $ref: '#/components/messages/UserSignedUp' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user