diff --git a/package-lock.json b/package-lock.json index 7cf130af..ac03ad7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1419,19 +1419,6 @@ "node": ">=18" } }, - "node_modules/@exodus/schemasafe": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", - "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", - "license": "MIT" - }, - "node_modules/@faker-js/faker": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-5.5.3.tgz", - "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", - "deprecated": "Please update to a newer version.", - "license": "MIT" - }, "node_modules/@gilbarbara/deep-equal": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.3.1.tgz", @@ -4672,16 +4659,6 @@ "undici-types": ">=7.24.0 <7.24.7" } }, - "node_modules/@types/openapi-to-postmanv2": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/openapi-to-postmanv2/-/openapi-to-postmanv2-5.0.0.tgz", - "integrity": "sha512-77mXBtgWH5a/gaN3qZtEumDZRxGtCMOhfizWC4DwkDrlfPW+mzyMRkxa17mNm2v0F1Z1+N3rCcOcH6Q4n6XKGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/postman-collection": "*" - } - }, "node_modules/@types/papaparse": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", @@ -4699,16 +4676,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/postman-collection": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@types/postman-collection/-/postman-collection-3.5.11.tgz", - "integrity": "sha512-BZgBJDdX6jyy9hzSTIMRhCsxhF0IlzPr1i98q2wdkDo8rZrbNoBvs+3/Vw+LOIIAFH1G+FyXo5Fjf8qbawGeHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/prismjs": { "version": "1.26.6", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", @@ -6204,20 +6171,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-draft-04": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", - "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", - "license": "MIT", - "peerDependencies": { - "ajv": "^8.5.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/ajv-formats": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", @@ -6239,6 +6192,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6298,6 +6252,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -6371,12 +6326,6 @@ "node": ">=12" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -6675,12 +6624,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "license": "MIT" - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -6800,15 +6743,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/charset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", - "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -6899,20 +6833,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/clone-regexp": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", @@ -7059,27 +6979,6 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, - "node_modules/compute-gcd": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/compute-gcd/-/compute-gcd-1.2.1.tgz", - "integrity": "sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==", - "dependencies": { - "validate.io-array": "^1.0.3", - "validate.io-function": "^1.0.2", - "validate.io-integer-array": "^1.0.0" - } - }, - "node_modules/compute-lcm": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/compute-lcm/-/compute-lcm-1.1.2.tgz", - "integrity": "sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==", - "dependencies": { - "compute-gcd": "^1.2.1", - "validate.io-array": "^1.0.3", - "validate.io-function": "^1.0.2", - "validate.io-integer-array": "^1.0.0" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7783,12 +7682,6 @@ "dev": true, "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -8026,12 +7919,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "license": "MIT" - }, "node_modules/esbuild": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", @@ -8078,6 +7965,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8311,12 +8199,6 @@ "node": ">=8.6.0" } }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT" - }, "node_modules/fast-shallow-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", @@ -8385,15 +8267,6 @@ } } }, - "node_modules/file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8496,12 +8369,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreach": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", - "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", - "license": "MIT" - }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -8651,15 +8518,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -8882,15 +8740,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphlib": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", - "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.15" - } - }, "node_modules/graphql": { "version": "16.14.2", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", @@ -9191,18 +9040,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-reasons": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/http-reasons/-/http-reasons-0.1.0.tgz", - "integrity": "sha512-P6kYh0lKZ+y29T2Gqz+RlC9WBLhKe8kDmcJ+A+611jFfxdPsbMRQ5aNmFRM3lENqFkK+HTTL+tlQviAiv0AbLQ==", - "license": "Apache-2.0" - }, - "node_modules/http2-client": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", - "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", - "license": "MIT" - }, "node_modules/httpntlm": { "version": "1.8.13", "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.8.13.tgz", @@ -9679,15 +9516,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -10095,6 +9923,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, "funding": [ { "type": "github", @@ -10149,30 +9978,12 @@ "dev": true, "license": "MIT" }, - "node_modules/json-pointer": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", - "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", - "license": "MIT", - "dependencies": { - "foreach": "^2.0.4" - } - }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "license": "(AFL-2.1 OR BSD-3-Clause)" }, - "node_modules/json-schema-compare": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", - "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.4" - } - }, "node_modules/json-schema-library": { "version": "9.3.5", "resolved": "https://registry.npmjs.org/json-schema-library/-/json-schema-library-9.3.5.tgz", @@ -10200,20 +10011,6 @@ "node": ">=0.10" } }, - "node_modules/json-schema-merge-allof": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz", - "integrity": "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==", - "license": "MIT", - "dependencies": { - "compute-lcm": "^1.1.2", - "json-schema-compare": "^0.2.2", - "lodash": "^4.17.20" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -10609,15 +10406,6 @@ "dev": true, "license": "MIT" }, - "node_modules/liquid-json": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/liquid-json/-/liquid-json-0.3.1.tgz", - "integrity": "sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -10634,12 +10422,6 @@ "node": ">=4" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -11854,15 +11636,6 @@ "node": ">= 0.6" } }, - "node_modules/mime-format": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mime-format/-/mime-format-2.0.2.tgz", - "integrity": "sha512-Y5ERWVcyh3sby9Fx2U5F1yatiTFjNsqF5NltihTWI9QgNtr5o3dbCZdcKa1l2wyfhnwwoP9HGNxga7LqZLA6gw==", - "license": "Apache-2.0", - "dependencies": { - "charset": "^1.0.0" - } - }, "node_modules/mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", @@ -12074,15 +11847,6 @@ "node": ">= 0.6" } }, - "node_modules/neotraverse": { - "version": "0.6.15", - "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.15.tgz", - "integrity": "sha512-HZpdkco+JeXq0G+WWpMJ4NsX3pqb5O7eR9uGz3FfoFt+LYzU8iRWp49nJtud6hsDoywM8tIrDo3gjgmOqJA8LA==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/nested-error-stacks": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz", @@ -12128,27 +11892,6 @@ } } }, - "node_modules/node-fetch-h2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", - "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", - "license": "MIT", - "dependencies": { - "http2-client": "^1.2.5" - }, - "engines": { - "node": "4.x || >=6.0.0" - } - }, - "node_modules/node-readfiles": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", - "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", - "license": "MIT", - "dependencies": { - "es6-promise": "^3.2.1" - } - }, "node_modules/node-releases": { "version": "2.0.53", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", @@ -12319,132 +12062,6 @@ "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", "license": "MIT" }, - "node_modules/oas-kit-common": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", - "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", - "license": "BSD-3-Clause", - "dependencies": { - "fast-safe-stringify": "^2.0.7" - } - }, - "node_modules/oas-linter": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", - "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@exodus/schemasafe": "^1.0.0-rc.2", - "should": "^13.2.1", - "yaml": "^1.10.0" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-linter/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/oas-resolver": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", - "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", - "license": "BSD-3-Clause", - "dependencies": { - "node-fetch-h2": "^2.3.0", - "oas-kit-common": "^1.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "resolve": "resolve.js" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-resolver-browser": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/oas-resolver-browser/-/oas-resolver-browser-2.5.6.tgz", - "integrity": "sha512-Jw5elT/kwUJrnGaVuRWe1D7hmnYWB8rfDDjBnpQ+RYY/dzAewGXeTexXzt4fGEo6PUE4eqKqPWF79MZxxvMppA==", - "license": "BSD-3-Clause", - "dependencies": { - "node-fetch-h2": "^2.3.0", - "oas-kit-common": "^1.0.8", - "path-browserify": "^1.0.1", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "resolve": "resolve.js" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-resolver-browser/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/oas-resolver/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/oas-schema-walker": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", - "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", - "license": "BSD-3-Clause", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-validator": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", - "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", - "license": "BSD-3-Clause", - "dependencies": { - "call-me-maybe": "^1.0.1", - "oas-kit-common": "^1.0.8", - "oas-linter": "^3.2.2", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "reftools": "^1.1.9", - "should": "^13.2.1", - "yaml": "^1.10.0" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-validator/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/oauth-1.0a": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz", @@ -12460,15 +12077,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -12577,70 +12185,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi-to-postmanv2": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-6.3.3.tgz", - "integrity": "sha512-o0u6qqMMRLt7eAyFBpL/lCresQ1VqQFGe6/iPMBvNkWDXrpgGo5jo+YnP1e0MwF3tdUKLfkHssufGWMdojpPLg==", - "license": "Apache-2.0", - "dependencies": { - "ajv": "^8.11.0", - "ajv-draft-04": "1.0.0", - "ajv-formats": "2.1.1", - "async": "3.2.6", - "commander": "2.20.3", - "graphlib": "2.1.8", - "js-yaml": "4.3.0", - "json-pointer": "0.6.2", - "json-schema-merge-allof": "0.8.1", - "lodash": "4.18.1", - "neotraverse": "0.6.15", - "oas-resolver-browser": "2.5.6", - "object-hash": "3.0.0", - "openapi-types": "^12.1.3", - "path-browserify": "1.0.1", - "postman-collection": "^5.0.0", - "swagger2openapi": "7.0.8", - "yaml": "1.10.2" - }, - "bin": { - "openapi2postmanv2": "bin/openapi2postmanv2.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/openapi-to-postmanv2/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/openapi-to-postmanv2/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "license": "MIT" - }, "node_modules/own-keys": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", @@ -12941,12 +12485,6 @@ "node": ">= 0.8" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "license": "MIT" - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -13139,76 +12677,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postman-collection": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-5.3.1.tgz", - "integrity": "sha512-+ixY4KEGerw3I5dE6obXgXx31na8URU5ODNIA6Rjkbt3/BUpNRk03pxUAZFHr5dDXwCikJSa7pt5o0x1QXT77w==", - "license": "Apache-2.0", - "dependencies": { - "@faker-js/faker": "5.5.3", - "file-type": "3.9.0", - "http-reasons": "0.1.0", - "iconv-lite": "0.6.3", - "liquid-json": "0.3.1", - "lodash": "4.18.1", - "mime": "3.0.0", - "mime-format": "2.0.2", - "postman-url-encoder": "3.0.8", - "semver": "7.7.1", - "uuid": "8.3.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/postman-collection/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postman-collection/node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/postman-collection/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/postman-url-encoder": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/postman-url-encoder/-/postman-url-encoder-3.0.8.tgz", - "integrity": "sha512-EOgUMBazo7JNP4TDrd64TsooCiWzzo4143Ws8E8WYGEpn2PKpq+S4XRTDhuRTYHm3VKOpUZs7ZYZq7zSDuesqA==", - "license": "Apache-2.0", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prettier": { "version": "3.9.6", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", @@ -13285,15 +12753,6 @@ "node": ">= 0.10" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.15.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", @@ -13595,15 +13054,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/reftools": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", - "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", - "license": "BSD-3-Clause", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -13721,15 +13171,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -14223,60 +13664,6 @@ "integrity": "sha512-jHPXQQk9d/QXCvJuLPYMOYWez3c43sORAgcIEoV7bFv5AJSJRAOyw5lQO12PMfd385qiLRCaDt7OtEzgrIGZUA==", "license": "MIT" }, - "node_modules/should": { - "version": "13.2.3", - "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", - "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", - "license": "MIT", - "dependencies": { - "should-equal": "^2.0.0", - "should-format": "^3.0.3", - "should-type": "^1.4.0", - "should-type-adaptors": "^1.0.1", - "should-util": "^1.0.0" - } - }, - "node_modules/should-equal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", - "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", - "license": "MIT", - "dependencies": { - "should-type": "^1.4.0" - } - }, - "node_modules/should-format": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", - "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", - "license": "MIT", - "dependencies": { - "should-type": "^1.3.0", - "should-type-adaptors": "^1.0.1" - } - }, - "node_modules/should-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", - "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", - "license": "MIT" - }, - "node_modules/should-type-adaptors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", - "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", - "license": "MIT", - "dependencies": { - "should-type": "^1.3.0", - "should-util": "^1.0.0" - } - }, - "node_modules/should-util": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", - "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", - "license": "MIT" - }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -14564,20 +13951,6 @@ "dev": true, "license": "MIT" }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.padend": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", @@ -14685,18 +14058,6 @@ "node": ">=4" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -14811,42 +14172,6 @@ "dev": true, "license": "MIT" }, - "node_modules/swagger2openapi": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", - "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", - "license": "BSD-3-Clause", - "dependencies": { - "call-me-maybe": "^1.0.1", - "node-fetch": "^2.6.1", - "node-fetch-h2": "^2.3.0", - "node-readfiles": "^0.2.0", - "oas-kit-common": "^1.0.8", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "oas-validator": "^5.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "boast": "boast.js", - "oas-validate": "oas-validate.js", - "swagger2openapi": "swagger2openapi.js" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/swagger2openapi/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/tabbable": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", @@ -15467,16 +14792,6 @@ "dev": true, "license": "(WTFPL OR MIT)" }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/valid-url": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", @@ -15493,39 +14808,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/validate.io-array": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", - "integrity": "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==", - "license": "MIT" - }, - "node_modules/validate.io-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz", - "integrity": "sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==" - }, - "node_modules/validate.io-integer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz", - "integrity": "sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==", - "dependencies": { - "validate.io-number": "^1.0.3" - } - }, - "node_modules/validate.io-integer-array": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz", - "integrity": "sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==", - "dependencies": { - "validate.io-array": "^1.0.3", - "validate.io-integer": "^1.0.4" - } - }, - "node_modules/validate.io-number": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz", - "integrity": "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -16348,56 +15630,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -16434,15 +15666,6 @@ "node": ">=0.6.0" } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -16468,33 +15691,6 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/yauzl": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", @@ -16840,11 +16036,7 @@ "name": "@yaak/importer-openapi", "version": "0.1.0", "dependencies": { - "openapi-to-postmanv2": "^6.3.3", "yaml": "^2.8.3" - }, - "devDependencies": { - "@types/openapi-to-postmanv2": "^5.0.0" } }, "plugins/importer-postman": { diff --git a/plugins/importer-openapi/package.json b/plugins/importer-openapi/package.json index e540073c..e312db11 100644 --- a/plugins/importer-openapi/package.json +++ b/plugins/importer-openapi/package.json @@ -10,10 +10,6 @@ "test": "vp test --run tests" }, "dependencies": { - "openapi-to-postmanv2": "^6.3.3", "yaml": "^2.8.3" - }, - "devDependencies": { - "@types/openapi-to-postmanv2": "^5.0.0" } } diff --git a/plugins/importer-openapi/src/index.ts b/plugins/importer-openapi/src/index.ts index a5ed442e..d9f2fb96 100644 --- a/plugins/importer-openapi/src/index.ts +++ b/plugins/importer-openapi/src/index.ts @@ -1,7 +1,38 @@ -import { convertPostman } from "@yaak/importer-postman/src"; -import type { Context, PluginDefinition } from "@yaakapp/api"; +import type { + Context, + Environment, + Folder, + HttpRequest, + HttpRequestHeader, + HttpUrlParameter, + PartialImportResources, + PluginDefinition, + Workspace, +} from "@yaakapp/api"; import type { ImportPluginResponse } from "@yaakapp/api/lib/plugins/ImporterPlugin"; -import { convert } from "openapi-to-postmanv2"; +import YAML from "yaml"; + +type AtLeast = Partial & Pick; +type UnknownRecord = Record; +type ImportResources = { + workspaces: AtLeast[]; + environments: AtLeast[]; + folders: AtLeast[]; + httpRequests: AtLeast[]; +}; + +const HTTP_METHODS = ["delete", "get", "head", "options", "patch", "post", "put", "trace"]; +const BODY_CONTENT_TYPE_PREFERENCE = [ + "application/json", + "application/x-www-form-urlencoded", + "multipart/form-data", + "application/xml", + "text/plain", +]; +const MAX_EXAMPLE_DEPTH = 8; +const MAX_EXAMPLE_PROPERTIES = 25; +const MAX_DESCRIPTION_ITEMS = 40; +const MAX_NAME_LENGTH = 100; export const plugin: PluginDefinition = { importer: { @@ -14,23 +45,1051 @@ export const plugin: PluginDefinition = { }; export async function convertOpenApi(contents: string): Promise { - // oxlint-disable-next-line no-explicit-any - let postmanCollection: any; - try { - postmanCollection = await new Promise((resolve, reject) => { - // oxlint-disable-next-line no-explicit-any - convert({ type: "string", data: contents }, {}, (err, result: any) => { - if (err != null) reject(err); + const spec = parseSpec(contents); + if (!isOpenApiSpec(spec)) return undefined; - if (Array.isArray(result.output) && result.output.length > 0) { - resolve(result.output[0].data); - } - }); + const importState = new ImportState(spec); + const workspace: ImportResources["workspaces"][0] = { + model: "workspace", + id: importState.generateId("workspace"), + name: stringAt(spec.info, "title") ?? "OpenAPI Import", + description: importInfoDescription(toRecord(spec.info)), + }; + + const resources: ImportResources = { + workspaces: [workspace], + environments: [], + folders: [], + httpRequests: [], + }; + const baseUrl = importBaseUrl(spec); + const requestBaseUrl = baseUrl.length > 0 ? "${[baseUrl]}" : ""; + + if (baseUrl.length > 0) { + resources.environments.push({ + model: "environment", + id: importState.generateId("environment"), + workspaceId: workspace.id, + name: "Global Variables", + variables: [{ name: "baseUrl", value: baseUrl }], + parentModel: "workspace", + parentId: null, + sortPriority: importState.nextSortPriority(), }); - } catch { - // Probably not an OpenAPI file, so skip it - return undefined; } - return convertPostman(JSON.stringify(postmanCollection)); + const folderIdsByTag = new Map(); + const routeLabels = new Map(); + for (const tag of toArray(spec.tags)) { + const tagRecord = toRecord(tag); + const name = stringAt(tagRecord, "name"); + if (name == null || folderIdsByTag.has(name)) continue; + + const folder: ImportResources["folders"][0] = { + model: "folder", + id: importState.generateId("folder"), + workspaceId: workspace.id, + name, + description: importTagDescription(tagRecord), + folderId: null, + sortPriority: importState.nextSortPriority(), + }; + resources.folders.push(folder); + folderIdsByTag.set(name, folder.id); + } + + for (const [rawPath, rawPathItem] of Object.entries(toRecord(spec.paths))) { + const pathItem = importState.resolve(rawPathItem); + if (!isRecord(pathItem)) continue; + + const pathParameters = toArray(pathItem.parameters); + for (const method of HTTP_METHODS) { + const operation = importState.resolve(pathItem[method]); + if (!isRecord(operation)) continue; + + const folderId = findOrCreateFolderId({ + folderIdsByTag, + importState, + operation, + resources, + workspaceId: workspace.id, + }); + + const request = importOperation({ + importState, + method, + operation, + path: rawPath, + pathItem, + pathParameters, + requestBaseUrl, + spec, + workspaceId: workspace.id, + folderId, + }); + routeLabels.set(request.id, `${method.toUpperCase()} ${rawPath}`); + resources.httpRequests.push(request); + } + } + + if (resources.httpRequests.length === 0) return undefined; + + disambiguateNames(resources.httpRequests, routeLabels); + + return { + resources: deleteUndefinedAttrs( + convertTemplateSyntax({ + environments: resources.environments, + folders: resources.folders, + grpcRequests: [], + httpRequests: resources.httpRequests, + websocketRequests: [], + workspaces: resources.workspaces, + }), + ) as PartialImportResources, + }; +} + +/** + * Two operations sharing a summary are indistinguishable once imported, so the + * colliding ones get their route appended. Names that are already unique within + * their folder are left alone. + */ +function disambiguateNames( + requests: ImportResources["httpRequests"], + routeLabels: Map, +): void { + const counts = new Map(); + for (const request of requests) { + const key = `${request.folderId ?? ""} ${request.name}`; + counts.set(key, (counts.get(key) ?? 0) + 1); + } + + for (const request of requests) { + const key = `${request.folderId ?? ""} ${request.name}`; + const routeLabel = routeLabels.get(request.id); + if ((counts.get(key) ?? 0) < 2 || routeLabel == null) continue; + if (request.name === routeLabel) continue; + request.name = `${request.name} (${routeLabel})`; + } +} + +function importOperation({ + importState, + method, + operation, + path, + pathItem, + pathParameters, + requestBaseUrl, + spec, + workspaceId, + folderId, +}: { + importState: ImportState; + method: string; + operation: UnknownRecord; + path: string; + pathItem: UnknownRecord; + pathParameters: unknown[]; + requestBaseUrl: string; + spec: UnknownRecord; + workspaceId: string; + folderId: string | null; +}): ImportResources["httpRequests"][0] { + importState.beginOperation(); + const parameters = mergeParameters({ + importState, + pathParameters, + operationParameters: toArray(operation.parameters), + }); + const body = importBody({ importState, operation, parameters, spec }); + const urlParameters = importUrlParameters({ importState, parameters }); + const headers = mergeHeaders( + importHeaderParameters({ importState, parameters }), + body.headers, + importAcceptHeader({ importState, operation, spec }), + ); + const authentication = importAuthentication({ importState, operation, spec }); + + // Built after everything else, so it can report the refs they left unresolved + const description = importOperationDescription({ + importState, + operation, + parameters, + bodyContentType: body.bodyType, + }); + + return { + model: "http_request", + id: importState.generateId("http_request"), + workspaceId, + folderId, + name: importOperationName(operation, method, path), + description, + method: method.toUpperCase(), + url: buildOperationUrl(operationBaseUrl({ operation, pathItem, requestBaseUrl }), path), + urlParameters, + headers, + body: body.body, + bodyType: body.bodyType, + sortPriority: importState.nextSortPriority(), + ...authentication, + }; +} + +/** + * A parameter is identified by its name and location, and an operation may + * redeclare one from its path item to change it. Keeping both copies would + * import the stale one alongside the override, so the operation's wins. + */ +function mergeParameters({ + importState, + pathParameters, + operationParameters, +}: { + importState: ImportState; + pathParameters: unknown[]; + operationParameters: unknown[]; +}): unknown[] { + const merged: unknown[] = []; + const indexByKey = new Map(); + + for (const parameter of [...pathParameters, ...operationParameters]) { + const resolved = importState.resolve(parameter); + const name = stringAt(resolved, "name"); + const location = stringAt(resolved, "in"); + // Anything missing an identity can't be matched up, so it is kept as-is + if (name == null || location == null) { + merged.push(resolved); + continue; + } + + const key = `${location} ${name}`; + const existing = indexByKey.get(key); + if (existing == null) { + indexByKey.set(key, merged.length); + merged.push(resolved); + } else { + merged[existing] = resolved; + } + } + + return merged; +} + +/** Operation-level `servers` beat path-level, which beat the spec-level base URL */ +function operationBaseUrl({ + operation, + pathItem, + requestBaseUrl, +}: { + operation: UnknownRecord; + pathItem: UnknownRecord; + requestBaseUrl: string; +}): string { + for (const servers of [operation.servers, pathItem.servers]) { + const override = toArray(servers) + .map((s) => interpolateServerUrl(toRecord(s))) + .find((url) => url.length > 0); + // Overrides are inlined rather than shared, since only the spec-level base + // URL becomes the baseUrl variable + if (override != null) return override; + } + return requestBaseUrl; +} + +/** + * Swagger 2.0 declares response types up front in `produces`; OpenAPI 3 only + * lists them per response, so successful responses stand in. Both become an + * Accept header, which is what the Postman-based importer used to produce. + */ +function importAcceptHeader({ + importState, + operation, + spec, +}: { + importState: ImportState; + operation: UnknownRecord; + spec: UnknownRecord; +}): HttpRequestHeader[] { + const produces = + toArray(operation.produces ?? spec.produces).find((c): c is string => typeof c === "string") ?? + successResponseContentType(importState, operation); + // `*/*` is what a request accepts by default, so stating it just adds noise + if (produces == null || produces === "*/*") return []; + return [{ enabled: true, name: "Accept", value: produces }]; +} + +/** The content type of the first successful response, by the usual preference */ +function successResponseContentType( + importState: ImportState, + operation: UnknownRecord, +): string | null { + for (const [status, response] of Object.entries(toRecord(operation.responses))) { + if (!status.startsWith("2") && status !== "default") continue; + + const content = toRecord(toRecord(importState.resolve(response)).content); + const contentType = chooseContentType(Object.keys(content)); + if (contentType != null) return contentType; + } + return null; +} + +function parseSpec(contents: string): unknown { + try { + return JSON.parse(contents); + } catch { + // Fall through to YAML. + } + + try { + return YAML.parse(contents); + } catch { + return null; + } +} + +function isOpenApiSpec(value: unknown): value is UnknownRecord { + const spec = toRecord(value); + const openapi = stringAt(spec, "openapi"); + const swagger = stringAt(spec, "swagger"); + return isRecord(spec.paths) && (openapi?.startsWith("3.") === true || swagger === "2.0"); +} + +function importInfoDescription(info: UnknownRecord): string | undefined { + const parts = [ + stringAt(info, "description"), + stringAt(info, "termsOfService") + ? `Terms of service: ${stringAt(info, "termsOfService")}` + : null, + isRecord(info.contact) && stringAt(info.contact, "email") + ? `Contact: ${stringAt(info.contact, "email")}` + : null, + isRecord(info.license) && stringAt(info.license, "name") + ? `License: ${stringAt(info.license, "name")}${ + stringAt(info.license, "url") ? ` (${stringAt(info.license, "url")})` : "" + }` + : null, + ].filter(isPresent); + return parts.length > 0 ? parts.join("\n\n") : undefined; +} + +function importTagDescription(tag: UnknownRecord): string | undefined { + const externalDocs = toRecord(tag.externalDocs); + const parts = [ + stringAt(tag, "description"), + stringAt(externalDocs, "url") + ? `${stringAt(externalDocs, "description") ?? "External docs"}: ${stringAt(externalDocs, "url")}` + : null, + ].filter(isPresent); + return parts.length > 0 ? parts.join("\n\n") : undefined; +} + +function importOperationName(operation: UnknownRecord, method: string, path: string): string { + return ( + stringAt(operation, "summary") ?? + stringAt(operation, "operationId") ?? + firstLine(stringAt(operation, "description")) ?? + `${method.toUpperCase()} ${path}` + ); +} + +/** + * Some specs describe an operation without ever summarizing it, and the opening + * line is a far better name than the method and path. Paragraphs are left to the + * description, since a name that long is no easier to scan than the path. + */ +function firstLine(value: string | undefined): string | undefined { + const line = value?.split("\n").find((l) => l.trim().length > 0)?.trim(); + if (line == null || line.length > MAX_NAME_LENGTH) return undefined; + return line; +} + +function importOperationDescription({ + importState, + operation, + parameters, + bodyContentType, +}: { + importState: ImportState; + operation: UnknownRecord; + parameters: unknown[]; + bodyContentType: string | null; +}): string | undefined { + const parts: string[] = []; + const summary = stringAt(operation, "summary"); + const description = stringAt(operation, "description"); + const operationId = stringAt(operation, "operationId"); + + // Leads the description, since it changes whether the request should be used at all + if (operation.deprecated === true) { + parts.push("Deprecated."); + } + + if (description != null) { + parts.push(description); + } else if (summary != null) { + parts.push(summary); + } + + if (operationId != null) { + parts.push(`Operation ID: ${operationId}`); + } + + const parameterDescriptions = parameters + .map((p) => importState.resolve(p)) + .filter(isRecord) + .slice(0, MAX_DESCRIPTION_ITEMS) + .map((p) => { + const name = stringAt(p, "name") ?? "parameter"; + const location = stringAt(p, "in") ?? "unknown"; + const required = p.required === true ? ", required" : ""; + const description = stringAt(p, "description"); + return `- ${name} (${location}${required})${description ? `: ${description}` : ""}`; + }); + if (parameterDescriptions.length > 0) { + parts.push(["Parameters:", ...parameterDescriptions].join("\n")); + } + + const requestBody = importState.resolve(operation.requestBody); + if (isRecord(requestBody)) { + const content = toRecord(requestBody.content); + const contentTypes = Object.keys(content); + const bodyLines = [ + stringAt(requestBody, "description"), + bodyContentType ? `Selected content type: ${bodyContentType}` : null, + contentTypes.length > 0 ? `Available content types: ${contentTypes.join(", ")}` : null, + ].filter(isPresent); + if (bodyLines.length > 0) { + parts.push(["Request body:", ...bodyLines].join("\n")); + } + } + + const responseDescriptions = Object.entries(toRecord(operation.responses)) + .slice(0, MAX_DESCRIPTION_ITEMS) + .map(([status, response]) => { + const responseRecord = toRecord(importState.resolve(response)); + return `- ${status}: ${stringAt(responseRecord, "description") ?? ""}`.trimEnd(); + }); + if (responseDescriptions.length > 0) { + parts.push(["Responses:", ...responseDescriptions].join("\n")); + } + + const externalDocs = toRecord(operation.externalDocs); + if (stringAt(externalDocs, "url")) { + parts.push( + `${stringAt(externalDocs, "description") ?? "External docs"}: ${stringAt(externalDocs, "url")}`, + ); + } + + // Without this the parts these refs describe just come out blank + const unresolvedRefs = importState.unresolvedRefs().slice(0, MAX_DESCRIPTION_ITEMS); + if (unresolvedRefs.length > 0) { + parts.push( + [ + "Unresolved references (point outside this document, so the parts they describe were left empty):", + ...unresolvedRefs.map((ref) => `- ${ref}`), + ].join("\n"), + ); + } + + return parts.length > 0 ? parts.join("\n\n") : undefined; +} + +function findOrCreateFolderId({ + folderIdsByTag, + importState, + operation, + resources, + workspaceId, +}: { + folderIdsByTag: Map; + importState: ImportState; + operation: UnknownRecord; + resources: ImportResources; + workspaceId: string; +}): string | null { + const tag = toArray(operation.tags).find((t): t is string => typeof t === "string"); + if (tag == null) return null; + + const existingFolderId = folderIdsByTag.get(tag); + if (existingFolderId != null) return existingFolderId; + + const folder: ImportResources["folders"][0] = { + model: "folder", + id: importState.generateId("folder"), + workspaceId, + name: tag, + folderId: null, + sortPriority: importState.nextSortPriority(), + }; + resources.folders.push(folder); + folderIdsByTag.set(tag, folder.id); + return folder.id; +} + +function buildOperationUrl(baseUrl: string, path: string): string { + return joinUrlParts(baseUrl, path.replaceAll(/{([^}/]+)}/g, ":$1")); +} + +function importBaseUrl(spec: UnknownRecord): string { + const openApiServer = toArray(spec.servers) + .map((s) => toRecord(s)) + .map((s) => interpolateServerUrl(s)) + .find((url) => url.length > 0); + if (openApiServer != null) return openApiServer; + + const host = stringAt(spec, "host"); + if (host == null) return stringAt(spec, "basePath") ?? ""; + + const scheme = toArray(spec.schemes).find((s): s is string => typeof s === "string") ?? "https"; + return joinUrlParts(`${scheme}://${host}`, stringAt(spec, "basePath") ?? ""); +} + +function interpolateServerUrl(server: UnknownRecord): string { + let url = stringAt(server, "url") ?? ""; + for (const [name, variable] of Object.entries(toRecord(server.variables))) { + url = url.replaceAll(`{${name}}`, stringifyExampleValue(toRecord(variable).default)); + } + return url; +} + +function joinUrlParts(baseUrl: string, path: string): string { + if (baseUrl.length === 0) return path; + return `${trimTrailingSlashes(baseUrl)}/${trimLeadingSlashes(path)}`; +} + +function trimLeadingSlashes(value: string): string { + let index = 0; + while (value[index] === "/") index++; + return value.slice(index); +} + +function trimTrailingSlashes(value: string): string { + let index = value.length; + while (value[index - 1] === "/") index--; + return value.slice(0, index); +} + +function importUrlParameters({ + importState, + parameters, +}: { + importState: ImportState; + parameters: unknown[]; +}): HttpUrlParameter[] { + return parameters + .map((p) => importState.resolve(p)) + .filter(isRecord) + .filter((p) => stringAt(p, "in") === "query" || stringAt(p, "in") === "path") + .map((p) => ({ + enabled: p.required === true, + name: + stringAt(p, "in") === "path" + ? `:${stringAt(p, "name") ?? ""}` + : (stringAt(p, "name") ?? ""), + value: parameterExample(p, importState), + })) + .filter(({ name }) => name.length > 0); +} + +function importHeaderParameters({ + importState, + parameters, +}: { + importState: ImportState; + parameters: unknown[]; +}): HttpRequestHeader[] { + return parameters + .map((p) => importState.resolve(p)) + .filter(isRecord) + .filter((p) => stringAt(p, "in") === "header") + .map((p) => ({ + enabled: p.required === true, + name: stringAt(p, "name") ?? "", + value: parameterExample(p, importState), + })) + .filter(({ name }) => name.length > 0); +} + +function parameterExample(parameter: UnknownRecord, importState: ImportState): string { + const directExample = firstPresent(parameter.example, firstExampleValue(parameter.examples)); + if (directExample != null) return stringifyExampleValue(directExample); + return stringifyExampleValue(schemaToExample(importState.resolve(parameter.schema), importState)); +} + +function importBody({ + importState, + operation, + parameters, + spec, +}: { + importState: ImportState; + operation: UnknownRecord; + parameters: unknown[]; + spec: UnknownRecord; +}): { + headers: HttpRequestHeader[]; + body: Record; + bodyType: string | null; +} { + const openApiRequestBody = importState.resolve(operation.requestBody); + if (isRecord(openApiRequestBody)) { + return importBodyFromContent(importState, toRecord(openApiRequestBody.content)); + } + + const bodyParameter = parameters + .map((p) => importState.resolve(p)) + .find((p) => isRecord(p) && stringAt(p, "in") === "body"); + if (isRecord(bodyParameter)) { + const contentType = toArray(operation.consumes ?? spec.consumes).find( + (c): c is string => typeof c === "string", + ); + const bodyType = contentType ?? "application/json"; + return { + headers: [{ enabled: true, name: "Content-Type", value: bodyType }], + bodyType, + body: { + text: formatBodyText( + schemaToExample(importState.resolve(bodyParameter.schema), importState), + ), + }, + }; + } + + const formParameters = parameters + .map((p) => importState.resolve(p)) + .filter(isRecord) + .filter((p) => stringAt(p, "in") === "formData"); + if (formParameters.length > 0) { + const contentType = + toArray(operation.consumes ?? spec.consumes).find((c): c is string => typeof c === "string") ?? + (formParameters.some((p) => stringAt(p, "type") === "file") + ? "multipart/form-data" + : "application/x-www-form-urlencoded"); + return { + headers: [{ enabled: true, name: "Content-Type", value: contentType }], + bodyType: contentType, + body: { + form: formParameters.map((p) => ({ + enabled: p.required === true, + name: stringAt(p, "name") ?? "", + value: parameterExample(p, importState), + })), + }, + }; + } + + return { headers: [], body: {}, bodyType: null }; +} + +function importBodyFromContent(importState: ImportState, content: UnknownRecord) { + const contentType = chooseContentType(Object.keys(content)); + if (contentType == null) return { headers: [], body: {}, bodyType: null }; + + const mediaType = toRecord(content[contentType]); + const example = mediaTypeExample(mediaType, importState); + + if ( + contentType === "application/x-www-form-urlencoded" || + contentType === "multipart/form-data" + ) { + return { + headers: [{ enabled: true, name: "Content-Type", value: contentType }], + bodyType: contentType, + body: { + form: schemaToFormParameters(importState.resolve(mediaType.schema), importState), + }, + }; + } + + return { + headers: [{ enabled: true, name: "Content-Type", value: contentType }], + bodyType: contentType === "application/octet-stream" ? "binary" : contentType, + body: contentType === "application/octet-stream" ? {} : { text: formatBodyText(example) }, + }; +} + +function chooseContentType(contentTypes: string[]): string | null { + for (const preference of BODY_CONTENT_TYPE_PREFERENCE) { + const exact = contentTypes.find((c) => c.toLowerCase() === preference); + if (exact != null) return exact; + } + return contentTypes[0] ?? null; +} + +function mediaTypeExample(mediaType: UnknownRecord, importState: ImportState): unknown { + const directExample = firstPresent(mediaType.example, firstExampleValue(mediaType.examples)); + if (directExample != null) return directExample; + return schemaToExample(importState.resolve(mediaType.schema), importState); +} + +function schemaToFormParameters(schema: unknown, importState: ImportState) { + const resolvedSchema = toRecord(importState.resolve(schema)); + const required = toArray(resolvedSchema.required).filter( + (name): name is string => typeof name === "string", + ); + const properties = Object.entries(toRecord(resolvedSchema.properties)).slice( + 0, + MAX_EXAMPLE_PROPERTIES, + ); + + return properties.map(([name, property]) => { + const resolvedProperty = toRecord(importState.resolve(property)); + const example = schemaToExample(resolvedProperty, importState); + const base = { + enabled: required.includes(name), + name, + }; + if (stringAt(resolvedProperty, "format") === "binary") { + return { ...base, file: "" }; + } + return { ...base, value: stringifyExampleValue(example) }; + }); +} + +function schemaToExample( + schema: unknown, + importState: ImportState, + depth = 0, + visitedRefs = new Set(), +): unknown { + if (depth > MAX_EXAMPLE_DEPTH) return {}; + + const resolved = importState.resolve(schema, visitedRefs); + if (!isRecord(resolved)) return ""; + + const explicitExample = firstPresent( + resolved.example, + firstExampleValue(resolved.examples), + resolved.default, + ); + if (explicitExample != null) return explicitExample; + + const enumValues = toArray(resolved.enum); + if (enumValues.length > 0) return enumValues[0]; + + const allOf = toArray(resolved.allOf); + if (allOf.length > 0) { + return allOf.reduce((merged, childSchema) => { + const childExample = schemaToExample(childSchema, importState, depth + 1, visitedRefs); + return isRecord(childExample) ? { ...merged, ...childExample } : merged; + }, {}); + } + + const oneOf = toArray(resolved.oneOf); + const anyOf = toArray(resolved.anyOf); + if (oneOf.length > 0 || anyOf.length > 0) { + return schemaToExample(oneOf[0] ?? anyOf[0], importState, depth + 1, visitedRefs); + } + + const type = inferSchemaType(resolved); + if (type === "array") { + return [schemaToExample(resolved.items, importState, depth + 1, visitedRefs)]; + } + if (type === "object") { + const required = toArray(resolved.required).filter( + (name): name is string => typeof name === "string", + ); + const properties = Object.entries(toRecord(resolved.properties)).sort(([a], [b]) => { + const aRequired = required.includes(a); + const bRequired = required.includes(b); + return aRequired === bRequired ? 0 : aRequired ? -1 : 1; + }); + + return Object.fromEntries( + properties + .slice(0, MAX_EXAMPLE_PROPERTIES) + .map(([name, property]) => [ + name, + schemaToExample(property, importState, depth + 1, visitedRefs), + ]), + ); + } + if (type === "integer" || type === "number") return 0; + if (type === "boolean") return false; + if (stringAt(resolved, "format") === "date-time") return "2026-01-01T00:00:00Z"; + if (stringAt(resolved, "format") === "date") return "2026-01-01"; + return ""; +} + +function inferSchemaType(schema: UnknownRecord): string { + const rawType = schema.type; + if (typeof rawType === "string") return rawType; + if (Array.isArray(rawType)) { + const nonNullType = rawType.find((t) => t !== "null"); + if (typeof nonNullType === "string") return nonNullType; + } + if (isRecord(schema.properties) || isRecord(schema.additionalProperties)) return "object"; + if (schema.items != null) return "array"; + return "string"; +} + +function importAuthentication({ + importState, + operation, + spec, +}: { + importState: ImportState; + operation: UnknownRecord; + spec: UnknownRecord; +}): Pick { + const security = operation.security ?? spec.security; + if (!Array.isArray(security) || security.length === 0) { + return { authenticationType: null, authentication: {} }; + } + + const schemes = { + ...toRecord(toRecord(spec.components).securitySchemes), + ...toRecord(spec.securityDefinitions), + }; + for (const requirement of security) { + for (const [schemeName, rawScopes] of Object.entries(toRecord(requirement))) { + const scheme = toRecord(importState.resolve(schemes[schemeName])); + const type = stringAt(scheme, "type"); + if (type === "oauth2") { + const oauth2 = importOAuth2(scheme, rawScopes); + if (oauth2 != null) return oauth2; + continue; + } + if (type === "apiKey") { + return { authenticationType: "apikey", authentication: importApiKey(scheme, schemeName) }; + } + // Swagger 2.0 spells basic auth as its own type rather than an HTTP scheme + if (type === "basic" || (type === "http" && schemeIs(scheme, "basic"))) { + return { + authenticationType: "basic", + authentication: { username: "", password: "" }, + }; + } + if (type === "http" && schemeIs(scheme, "bearer")) { + return { + authenticationType: "bearer", + authentication: { token: "", prefix: "Bearer" }, + }; + } + } + } + + return { authenticationType: null, authentication: {} }; +} + +function schemeIs(scheme: UnknownRecord, name: string): boolean { + return stringAt(scheme, "scheme")?.toLowerCase() === name; +} + +/** + * The API key auth plugin can only write a header or a query parameter, so a + * cookie key becomes the Cookie header it would have ended up in, pre-filled + * with its name. Sending it as a header named after the cookie would just fail. + */ +function importApiKey(scheme: UnknownRecord, schemeName: string): Record { + const key = stringAt(scheme, "name") ?? schemeName; + const location = stringAt(scheme, "in"); + + if (location === "cookie") { + return { location: "header", key: "Cookie", value: `${key}=` }; + } + return { location: location === "query" ? "query" : "header", key, value: "" }; +} + +/** + * Maps an OpenAPI 3.x `flows` object or a Swagger 2.0 `flow` string onto the + * grant types the OAuth 2.0 auth plugin understands. Returns null when the + * scheme declares no flow this importer can map, so the caller keeps looking. + */ +function importOAuth2( + scheme: UnknownRecord, + rawScopes: unknown, +): Pick | null { + const scope = toArray(rawScopes) + .filter((s): s is string => typeof s === "string") + .join(" "); + + const flows = toRecord(scheme.flows); + const swagger2Flow = stringAt(scheme, "flow"); + const candidates: { grantType: string; flow: UnknownRecord }[] = [ + { grantType: "authorization_code", flow: toRecord(flows.authorizationCode) }, + { grantType: "client_credentials", flow: toRecord(flows.clientCredentials) }, + { grantType: "password", flow: toRecord(flows.password) }, + { grantType: "implicit", flow: toRecord(flows.implicit) }, + ]; + + // Swagger 2.0 puts the URLs on the scheme itself and names the flow differently + if (swagger2Flow != null) { + const grantType = { + accessCode: "authorization_code", + application: "client_credentials", + implicit: "implicit", + password: "password", + }[swagger2Flow]; + if (grantType == null) return null; + candidates.unshift({ grantType, flow: scheme }); + } + + for (const { grantType, flow } of candidates) { + const authorizationUrl = stringAt(flow, "authorizationUrl"); + const accessTokenUrl = stringAt(flow, "tokenUrl"); + if (authorizationUrl == null && accessTokenUrl == null) continue; + + const grantPatch = + grantType === "authorization_code" + ? { authorizationUrl, accessTokenUrl, clientSecret: "" } + : grantType === "implicit" + ? { authorizationUrl } + : grantType === "password" + ? { accessTokenUrl, clientSecret: "", username: "", password: "" } + : { accessTokenUrl, clientSecret: "" }; + + return { + authenticationType: "oauth2", + authentication: { + grantType, + clientId: "", + headerPrefix: "Bearer", + ...(scope.length > 0 ? { scope } : {}), + ...grantPatch, + }, + }; + } + + return null; +} + +function mergeHeaders(...headerGroups: HttpRequestHeader[][]): HttpRequestHeader[] { + const headers: HttpRequestHeader[] = []; + for (const header of headerGroups.flat()) { + const existing = headers.find((h) => h.name.toLowerCase() === header.name.toLowerCase()); + if (existing == null) { + headers.push(header); + } + } + return headers; +} + +function formatBodyText(example: unknown): string { + return typeof example === "string" ? example : JSON.stringify(example, null, 2); +} + +function stringifyExampleValue(value: unknown): string { + if (value == null) return ""; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + return JSON.stringify(value); +} + +function firstExampleValue(examples: unknown): unknown { + const firstExample = Object.values(toRecord(examples))[0]; + if (isRecord(firstExample) && "value" in firstExample) return firstExample.value; + return firstExample; +} + +function firstPresent(...values: unknown[]): unknown { + return values.find((value) => value !== undefined && value !== null); +} + +function stringAt(record: unknown, key: string): string | undefined { + const value = toRecord(record)[key]; + return typeof value === "string" ? value : undefined; +} + +function toArray(value: unknown): unknown[] { + return Array.isArray(value) ? value : []; +} + +function toRecord(value: unknown): UnknownRecord { + return isRecord(value) ? value : {}; +} + +function isRecord(value: unknown): value is UnknownRecord { + return value != null && typeof value === "object" && !Array.isArray(value); +} + +function isPresent(value: T | null | undefined): value is T { + return value != null && value !== ""; +} + +/** Recursively render all nested object properties */ +function convertTemplateSyntax(obj: T): T { + if (typeof obj === "string") { + // oxlint-disable-next-line no-template-curly-in-string -- Yaak template syntax + return obj.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") as T; + } + if (Array.isArray(obj) && obj != null) { + return obj.map(convertTemplateSyntax) as T; + } + if (typeof obj === "object" && obj != null) { + return Object.fromEntries( + Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)]), + ) as T; + } + return obj; +} + +function deleteUndefinedAttrs(obj: T): T { + if (Array.isArray(obj) && obj != null) { + return obj.map(deleteUndefinedAttrs) as T; + } + if (typeof obj === "object" && obj != null) { + return Object.fromEntries( + Object.entries(obj) + .filter(([, v]) => v !== undefined) + .map(([k, v]) => [k, deleteUndefinedAttrs(v)]), + ) as T; + } + return obj; +} + +class ImportState { + readonly #spec: UnknownRecord; + readonly #idCount: Partial> = {}; + #sortPriority = 0; + #unresolvedRefs = new Set(); + + constructor(spec: UnknownRecord) { + this.#spec = spec; + } + + generateId(model: string): string { + this.#idCount[model] = (this.#idCount[model] ?? -1) + 1; + return `GENERATE_ID::${model.toUpperCase()}_${this.#idCount[model]}`; + } + + nextSortPriority(): number { + return this.#sortPriority++; + } + + /** Starts collecting unresolved refs for a single operation */ + beginOperation(): void { + this.#unresolvedRefs = new Set(); + } + + /** Refs seen since `beginOperation` that point outside this document */ + unresolvedRefs(): string[] { + return [...this.#unresolvedRefs]; + } + + resolve(value: unknown, visitedRefs = new Set()): unknown { + if (!isRecord(value) || typeof value.$ref !== "string") return value; + if (visitedRefs.has(value.$ref)) return {}; + + const nextVisitedRefs = new Set(visitedRefs); + nextVisitedRefs.add(value.$ref); + + // Refs into other documents can't be followed without fetching them, so + // record them and let the operation description report what went missing + if (!value.$ref.startsWith("#/")) { + this.#unresolvedRefs.add(value.$ref); + return value; + } + + const resolved = value.$ref + .slice(2) + .split("/") + .map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~")) + .reduce((current, part) => toRecord(current)[part], this.#spec); + + return this.resolve(resolved, nextVisitedRefs); + } } diff --git a/plugins/importer-openapi/tests/__snapshots__/index.test.ts.snap b/plugins/importer-openapi/tests/__snapshots__/index.test.ts.snap new file mode 100644 index 00000000..10458055 --- /dev/null +++ b/plugins/importer-openapi/tests/__snapshots__/index.test.ts.snap @@ -0,0 +1,2789 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`importer-openapi > Snapshots real-world fixture apis-guru.yaml 1`] = ` +{ + "resources": { + "environments": [ + { + "id": "GENERATE_ID::ENVIRONMENT_0", + "model": "environment", + "name": "Global Variables", + "parentId": null, + "parentModel": "workspace", + "sortPriority": 0, + "variables": [ + { + "name": "baseUrl", + "value": "https://api.apis.guru/v2", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + ], + "folders": [ + { + "description": "Actions relating to APIs in the collection", + "folderId": null, + "id": "GENERATE_ID::FOLDER_0", + "model": "folder", + "name": "APIs", + "sortPriority": 1, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + ], + "grpcRequests": [], + "httpRequests": [ + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "List all APIs in the directory. +Returns links to the OpenAPI definitions for each API in the directory. +If API exist in multiple versions \`preferred\` one is explicitly marked. +Some basic info from the OpenAPI definition is cached inside each object. +This allows you to generate some simple views without needing to fetch the OpenAPI definition for each API. + + +Operation ID: listAPIs + +Responses: +- 200: OK", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [ + { + "enabled": true, + "name": "Accept", + "value": "application/json", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_0", + "method": "GET", + "model": "http_request", + "name": "List all APIs", + "sortPriority": 2, + "url": "\${[baseUrl]}/list.json", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Some basic metrics for the entire directory. +Just stunning numbers to put on a front page and are intended purely for WoW effect :) + + +Operation ID: getMetrics + +Responses: +- 200: OK", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [ + { + "enabled": true, + "name": "Accept", + "value": "application/json", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_1", + "method": "GET", + "model": "http_request", + "name": "Get basic metrics", + "sortPriority": 3, + "url": "\${[baseUrl]}/metrics.json", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "List all the providers in the directory + + +Operation ID: getProviders + +Responses: +- 200: OK", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [ + { + "enabled": true, + "name": "Accept", + "value": "application/json", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_2", + "method": "GET", + "model": "http_request", + "name": "List all providers", + "sortPriority": 4, + "url": "\${[baseUrl]}/providers.json", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns the API entry for one specific version of an API where there is no serviceName. + +Operation ID: getAPI + +Parameters: +- provider (path, required) +- api (path, required) + +Responses: +- 200: OK", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [ + { + "enabled": true, + "name": "Accept", + "value": "application/json", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_3", + "method": "GET", + "model": "http_request", + "name": "Retrieve one version of a particular API", + "sortPriority": 5, + "url": "\${[baseUrl]}/specs/:provider/:api.json", + "urlParameters": [ + { + "enabled": true, + "name": ":provider", + "value": "apis.guru", + }, + { + "enabled": true, + "name": ":api", + "value": "2.1.0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns the API entry for one specific version of an API where there is a serviceName. + +Operation ID: getServiceAPI + +Parameters: +- provider (path, required) +- service (path, required) +- api (path, required) + +Responses: +- 200: OK", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [ + { + "enabled": true, + "name": "Accept", + "value": "application/json", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_4", + "method": "GET", + "model": "http_request", + "name": "Retrieve one version of a particular API with a serviceName.", + "sortPriority": 6, + "url": "\${[baseUrl]}/specs/:provider/:service/:api.json", + "urlParameters": [ + { + "enabled": true, + "name": ":provider", + "value": "apis.guru", + }, + { + "enabled": true, + "name": ":service", + "value": "graph", + }, + { + "enabled": true, + "name": ":api", + "value": "2.1.0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "List all APIs in the directory for a particular providerName +Returns links to the individual API entry for each API. + + +Operation ID: getProvider + +Parameters: +- provider (path, required) + +Responses: +- 200: OK", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [ + { + "enabled": true, + "name": "Accept", + "value": "application/json", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_5", + "method": "GET", + "model": "http_request", + "name": "List all APIs for a particular provider", + "sortPriority": 7, + "url": "\${[baseUrl]}/:provider.json", + "urlParameters": [ + { + "enabled": true, + "name": ":provider", + "value": "apis.guru", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "List all serviceNames in the directory for a particular providerName + + +Operation ID: getServices + +Parameters: +- provider (path, required) + +Responses: +- 200: OK", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [ + { + "enabled": true, + "name": "Accept", + "value": "application/json", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_6", + "method": "GET", + "model": "http_request", + "name": "List all serviceNames for a particular provider", + "sortPriority": 8, + "url": "\${[baseUrl]}/:provider/services.json", + "urlParameters": [ + { + "enabled": true, + "name": ":provider", + "value": "apis.guru", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + ], + "websocketRequests": [], + "workspaces": [ + { + "description": "Wikipedia for Web APIs. Repository of API definitions in OpenAPI format. +**Warning**: If you want to be notified about changes in advance please join our [Slack channel](https://join.slack.com/t/mermade/shared_invite/zt-g78g7xir-MLE_CTCcXCdfJfG3CJe9qA). +Client sample: [[Demo]](https://apis.guru/simple-ui) [[Repo]](https://github.com/APIs-guru/simple-ui) + + +Contact: mike.ralphson@gmail.com + +License: CC0 1.0 (https://github.com/APIs-guru/openapi-directory#licenses)", + "id": "GENERATE_ID::WORKSPACE_0", + "model": "workspace", + "name": "APIs.guru", + }, + ], + }, +} +`; + +exports[`importer-openapi > Snapshots real-world fixture httpbin.yaml 1`] = ` +{ + "resources": { + "environments": [ + { + "id": "GENERATE_ID::ENVIRONMENT_0", + "model": "environment", + "name": "Global Variables", + "parentId": null, + "parentModel": "workspace", + "sortPriority": 0, + "variables": [ + { + "name": "baseUrl", + "value": "https://httpbin.org", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + ], + "folders": [ + { + "description": "Testing different HTTP verbs", + "folderId": null, + "id": "GENERATE_ID::FOLDER_0", + "model": "folder", + "name": "HTTP Methods", + "sortPriority": 1, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "description": "Auth methods", + "folderId": null, + "id": "GENERATE_ID::FOLDER_1", + "model": "folder", + "name": "Auth", + "sortPriority": 2, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "description": "Generates responses with given status code", + "folderId": null, + "id": "GENERATE_ID::FOLDER_2", + "model": "folder", + "name": "Status codes", + "sortPriority": 3, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "description": "Inspect the request data", + "folderId": null, + "id": "GENERATE_ID::FOLDER_3", + "model": "folder", + "name": "Request inspection", + "sortPriority": 4, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "description": "Inspect the response data like caching and headers", + "folderId": null, + "id": "GENERATE_ID::FOLDER_4", + "model": "folder", + "name": "Response inspection", + "sortPriority": 5, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "description": "Returns responses in different data formats", + "folderId": null, + "id": "GENERATE_ID::FOLDER_5", + "model": "folder", + "name": "Response formats", + "sortPriority": 6, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "description": "Generates random and dynamic data", + "folderId": null, + "id": "GENERATE_ID::FOLDER_6", + "model": "folder", + "name": "Dynamic data", + "sortPriority": 7, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "description": "Creates, reads and deletes Cookies", + "folderId": null, + "id": "GENERATE_ID::FOLDER_7", + "model": "folder", + "name": "Cookies", + "sortPriority": 8, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "description": "Returns different image formats", + "folderId": null, + "id": "GENERATE_ID::FOLDER_8", + "model": "folder", + "name": "Images", + "sortPriority": 9, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "description": "Returns different redirect responses", + "folderId": null, + "id": "GENERATE_ID::FOLDER_9", + "model": "folder", + "name": "Redirects", + "sortPriority": 10, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "description": "Returns anything that is passed to request", + "folderId": null, + "id": "GENERATE_ID::FOLDER_10", + "model": "folder", + "name": "Anything", + "sortPriority": 11, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + ], + "grpcRequests": [], + "httpRequests": [ + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Absolutely 302 Redirects n times. + +Parameters: +- n (path, required) + +Responses: +- 302: A redirection.", + "folderId": "GENERATE_ID::FOLDER_9", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_0", + "method": "GET", + "model": "http_request", + "name": "Absolutely 302 Redirects n times.", + "sortPriority": 12, + "url": "\${[baseUrl]}/absolute-redirect/:n", + "urlParameters": [ + { + "enabled": true, + "name": ":n", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns anything passed in request data. + +Responses: +- 200: Anything passed in request", + "folderId": "GENERATE_ID::FOLDER_10", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_1", + "method": "DELETE", + "model": "http_request", + "name": "Returns anything passed in request data. (DELETE /anything)", + "sortPriority": 13, + "url": "\${[baseUrl]}/anything", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns anything passed in request data. + +Responses: +- 200: Anything passed in request", + "folderId": "GENERATE_ID::FOLDER_10", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_2", + "method": "GET", + "model": "http_request", + "name": "Returns anything passed in request data. (GET /anything)", + "sortPriority": 14, + "url": "\${[baseUrl]}/anything", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns anything passed in request data. + +Responses: +- 200: Anything passed in request", + "folderId": "GENERATE_ID::FOLDER_10", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_3", + "method": "PATCH", + "model": "http_request", + "name": "Returns anything passed in request data. (PATCH /anything)", + "sortPriority": 15, + "url": "\${[baseUrl]}/anything", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns anything passed in request data. + +Responses: +- 200: Anything passed in request", + "folderId": "GENERATE_ID::FOLDER_10", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_4", + "method": "POST", + "model": "http_request", + "name": "Returns anything passed in request data. (POST /anything)", + "sortPriority": 16, + "url": "\${[baseUrl]}/anything", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns anything passed in request data. + +Responses: +- 200: Anything passed in request", + "folderId": "GENERATE_ID::FOLDER_10", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_5", + "method": "PUT", + "model": "http_request", + "name": "Returns anything passed in request data. (PUT /anything)", + "sortPriority": 17, + "url": "\${[baseUrl]}/anything", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns anything passed in request data. + +Responses: +- 200: Anything passed in request", + "folderId": "GENERATE_ID::FOLDER_10", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_6", + "method": "TRACE", + "model": "http_request", + "name": "Returns anything passed in request data. (TRACE /anything)", + "sortPriority": 18, + "url": "\${[baseUrl]}/anything", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns anything passed in request data. + +Parameters: +- anything (path, required): Automatically added + +Responses: +- 200: Anything passed in request", + "folderId": "GENERATE_ID::FOLDER_10", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_7", + "method": "DELETE", + "model": "http_request", + "name": "Returns anything passed in request data. (DELETE /anything/{anything})", + "sortPriority": 19, + "url": "\${[baseUrl]}/anything/:anything", + "urlParameters": [ + { + "enabled": true, + "name": ":anything", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns anything passed in request data. + +Parameters: +- anything (path, required): Automatically added + +Responses: +- 200: Anything passed in request", + "folderId": "GENERATE_ID::FOLDER_10", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_8", + "method": "GET", + "model": "http_request", + "name": "Returns anything passed in request data. (GET /anything/{anything})", + "sortPriority": 20, + "url": "\${[baseUrl]}/anything/:anything", + "urlParameters": [ + { + "enabled": true, + "name": ":anything", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns anything passed in request data. + +Parameters: +- anything (path, required): Automatically added + +Responses: +- 200: Anything passed in request", + "folderId": "GENERATE_ID::FOLDER_10", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_9", + "method": "PATCH", + "model": "http_request", + "name": "Returns anything passed in request data. (PATCH /anything/{anything})", + "sortPriority": 21, + "url": "\${[baseUrl]}/anything/:anything", + "urlParameters": [ + { + "enabled": true, + "name": ":anything", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns anything passed in request data. + +Parameters: +- anything (path, required): Automatically added + +Responses: +- 200: Anything passed in request", + "folderId": "GENERATE_ID::FOLDER_10", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_10", + "method": "POST", + "model": "http_request", + "name": "Returns anything passed in request data. (POST /anything/{anything})", + "sortPriority": 22, + "url": "\${[baseUrl]}/anything/:anything", + "urlParameters": [ + { + "enabled": true, + "name": ":anything", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns anything passed in request data. + +Parameters: +- anything (path, required): Automatically added + +Responses: +- 200: Anything passed in request", + "folderId": "GENERATE_ID::FOLDER_10", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_11", + "method": "PUT", + "model": "http_request", + "name": "Returns anything passed in request data. (PUT /anything/{anything})", + "sortPriority": 23, + "url": "\${[baseUrl]}/anything/:anything", + "urlParameters": [ + { + "enabled": true, + "name": ":anything", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns anything passed in request data. + +Parameters: +- anything (path, required): Automatically added + +Responses: +- 200: Anything passed in request", + "folderId": "GENERATE_ID::FOLDER_10", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_12", + "method": "TRACE", + "model": "http_request", + "name": "Returns anything passed in request data. (TRACE /anything/{anything})", + "sortPriority": 24, + "url": "\${[baseUrl]}/anything/:anything", + "urlParameters": [ + { + "enabled": true, + "name": ":anything", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Decodes base64url-encoded string. + +Parameters: +- value (path, required) + +Responses: +- 200: Decoded base64 content.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_13", + "method": "GET", + "model": "http_request", + "name": "Decodes base64url-encoded string.", + "sortPriority": 25, + "url": "\${[baseUrl]}/base64/:value", + "urlParameters": [ + { + "enabled": true, + "name": ":value", + "value": "SFRUUEJJTiBpcyBhd2Vzb21l", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Prompts the user for authorization using HTTP Basic Auth. + +Parameters: +- user (path, required) +- passwd (path, required) + +Responses: +- 200: Sucessful authentication. +- 401: Unsuccessful authentication.", + "folderId": "GENERATE_ID::FOLDER_1", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_14", + "method": "GET", + "model": "http_request", + "name": "Prompts the user for authorization using HTTP Basic Auth. (GET /basic-auth/{user}/{passwd})", + "sortPriority": 26, + "url": "\${[baseUrl]}/basic-auth/:user/:passwd", + "urlParameters": [ + { + "enabled": true, + "name": ":user", + "value": "", + }, + { + "enabled": true, + "name": ":passwd", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Prompts the user for authorization using bearer authentication. + +Parameters: +- Authorization (header) + +Responses: +- 200: Sucessful authentication. +- 401: Unsuccessful authentication.", + "folderId": "GENERATE_ID::FOLDER_1", + "headers": [ + { + "enabled": false, + "name": "Authorization", + "value": "", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_15", + "method": "GET", + "model": "http_request", + "name": "Prompts the user for authorization using bearer authentication.", + "sortPriority": 27, + "url": "\${[baseUrl]}/bearer", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns Brotli-encoded data. + +Responses: +- 200: Brotli-encoded data.", + "folderId": "GENERATE_ID::FOLDER_5", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_16", + "method": "GET", + "model": "http_request", + "name": "Returns Brotli-encoded data.", + "sortPriority": 28, + "url": "\${[baseUrl]}/brotli", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns n random bytes generated with given seed + +Parameters: +- n (path, required) + +Responses: +- 200: Bytes.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_17", + "method": "GET", + "model": "http_request", + "name": "Returns n random bytes generated with given seed", + "sortPriority": 29, + "url": "\${[baseUrl]}/bytes/:n", + "urlParameters": [ + { + "enabled": true, + "name": ":n", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a 304 if an If-Modified-Since header or If-None-Match is present. Returns the same as a GET otherwise. + +Parameters: +- If-Modified-Since (header) +- If-None-Match (header) + +Responses: +- 200: Cached response +- 304: Modified", + "folderId": "GENERATE_ID::FOLDER_4", + "headers": [ + { + "enabled": false, + "name": "If-Modified-Since", + "value": "", + }, + { + "enabled": false, + "name": "If-None-Match", + "value": "", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_18", + "method": "GET", + "model": "http_request", + "name": "Returns a 304 if an If-Modified-Since header or If-None-Match is present. Returns the same as a GET otherwise.", + "sortPriority": 30, + "url": "\${[baseUrl]}/cache", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Sets a Cache-Control header for n seconds. + +Parameters: +- value (path, required) + +Responses: +- 200: Cache control set", + "folderId": "GENERATE_ID::FOLDER_4", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_19", + "method": "GET", + "model": "http_request", + "name": "Sets a Cache-Control header for n seconds.", + "sortPriority": 31, + "url": "\${[baseUrl]}/cache/:value", + "urlParameters": [ + { + "enabled": true, + "name": ":value", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns cookie data. + +Responses: +- 200: Set cookies.", + "folderId": "GENERATE_ID::FOLDER_7", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_20", + "method": "GET", + "model": "http_request", + "name": "Returns cookie data.", + "sortPriority": 32, + "url": "\${[baseUrl]}/cookies", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Deletes cookie(s) as provided by the query string and redirects to cookie list. + +Parameters: +- freeform (query) + +Responses: +- 200: Redirect to cookie list", + "folderId": "GENERATE_ID::FOLDER_7", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_21", + "method": "GET", + "model": "http_request", + "name": "Deletes cookie(s) as provided by the query string and redirects to cookie list.", + "sortPriority": 33, + "url": "\${[baseUrl]}/cookies/delete", + "urlParameters": [ + { + "enabled": false, + "name": "freeform", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Sets cookie(s) as provided by the query string and redirects to cookie list. + +Parameters: +- freeform (query) + +Responses: +- 200: Redirect to cookie list", + "folderId": "GENERATE_ID::FOLDER_7", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_22", + "method": "GET", + "model": "http_request", + "name": "Sets cookie(s) as provided by the query string and redirects to cookie list.", + "sortPriority": 34, + "url": "\${[baseUrl]}/cookies/set", + "urlParameters": [ + { + "enabled": false, + "name": "freeform", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Sets a cookie and redirects to cookie list. + +Parameters: +- name (path, required) +- value (path, required) + +Responses: +- 200: Set cookies and redirects to cookie list.", + "folderId": "GENERATE_ID::FOLDER_7", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_23", + "method": "GET", + "model": "http_request", + "name": "Sets a cookie and redirects to cookie list.", + "sortPriority": 35, + "url": "\${[baseUrl]}/cookies/set/:name/:value", + "urlParameters": [ + { + "enabled": true, + "name": ":name", + "value": "", + }, + { + "enabled": true, + "name": ":value", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns Deflate-encoded data. + +Responses: +- 200: Defalte-encoded data.", + "folderId": "GENERATE_ID::FOLDER_5", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_24", + "method": "GET", + "model": "http_request", + "name": "Returns Deflate-encoded data.", + "sortPriority": 36, + "url": "\${[baseUrl]}/deflate", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a delayed response (max of 10 seconds). + +Parameters: +- delay (path, required) + +Responses: +- 200: A delayed response.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_25", + "method": "DELETE", + "model": "http_request", + "name": "Returns a delayed response (max of 10 seconds). (DELETE /delay/{delay})", + "sortPriority": 37, + "url": "\${[baseUrl]}/delay/:delay", + "urlParameters": [ + { + "enabled": true, + "name": ":delay", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a delayed response (max of 10 seconds). + +Parameters: +- delay (path, required) + +Responses: +- 200: A delayed response.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_26", + "method": "GET", + "model": "http_request", + "name": "Returns a delayed response (max of 10 seconds). (GET /delay/{delay})", + "sortPriority": 38, + "url": "\${[baseUrl]}/delay/:delay", + "urlParameters": [ + { + "enabled": true, + "name": ":delay", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a delayed response (max of 10 seconds). + +Parameters: +- delay (path, required) + +Responses: +- 200: A delayed response.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_27", + "method": "PATCH", + "model": "http_request", + "name": "Returns a delayed response (max of 10 seconds). (PATCH /delay/{delay})", + "sortPriority": 39, + "url": "\${[baseUrl]}/delay/:delay", + "urlParameters": [ + { + "enabled": true, + "name": ":delay", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a delayed response (max of 10 seconds). + +Parameters: +- delay (path, required) + +Responses: +- 200: A delayed response.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_28", + "method": "POST", + "model": "http_request", + "name": "Returns a delayed response (max of 10 seconds). (POST /delay/{delay})", + "sortPriority": 40, + "url": "\${[baseUrl]}/delay/:delay", + "urlParameters": [ + { + "enabled": true, + "name": ":delay", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a delayed response (max of 10 seconds). + +Parameters: +- delay (path, required) + +Responses: +- 200: A delayed response.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_29", + "method": "PUT", + "model": "http_request", + "name": "Returns a delayed response (max of 10 seconds). (PUT /delay/{delay})", + "sortPriority": 41, + "url": "\${[baseUrl]}/delay/:delay", + "urlParameters": [ + { + "enabled": true, + "name": ":delay", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a delayed response (max of 10 seconds). + +Parameters: +- delay (path, required) + +Responses: +- 200: A delayed response.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_30", + "method": "TRACE", + "model": "http_request", + "name": "Returns a delayed response (max of 10 seconds). (TRACE /delay/{delay})", + "sortPriority": 42, + "url": "\${[baseUrl]}/delay/:delay", + "urlParameters": [ + { + "enabled": true, + "name": ":delay", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "The request's DELETE parameters. + +Responses: +- 200: The request's DELETE parameters.", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_31", + "method": "DELETE", + "model": "http_request", + "name": "The request's DELETE parameters.", + "sortPriority": 43, + "url": "\${[baseUrl]}/delete", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns page denied by robots.txt rules. + +Responses: +- 200: Denied message", + "folderId": "GENERATE_ID::FOLDER_5", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_32", + "method": "GET", + "model": "http_request", + "name": "Returns page denied by robots.txt rules.", + "sortPriority": 44, + "url": "\${[baseUrl]}/deny", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Prompts the user for authorization using Digest Auth. + +Parameters: +- qop (path, required): auth or auth-int +- user (path, required) +- passwd (path, required) + +Responses: +- 200: Sucessful authentication. +- 401: Unsuccessful authentication.", + "folderId": "GENERATE_ID::FOLDER_1", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_33", + "method": "GET", + "model": "http_request", + "name": "Prompts the user for authorization using Digest Auth.", + "sortPriority": 45, + "url": "\${[baseUrl]}/digest-auth/:qop/:user/:passwd", + "urlParameters": [ + { + "enabled": true, + "name": ":qop", + "value": "", + }, + { + "enabled": true, + "name": ":user", + "value": "", + }, + { + "enabled": true, + "name": ":passwd", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Prompts the user for authorization using Digest Auth + Algorithm. + +Parameters: +- qop (path, required): auth or auth-int +- user (path, required) +- passwd (path, required) +- algorithm (path, required): MD5, SHA-256, SHA-512 + +Responses: +- 200: Sucessful authentication. +- 401: Unsuccessful authentication.", + "folderId": "GENERATE_ID::FOLDER_1", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_34", + "method": "GET", + "model": "http_request", + "name": "Prompts the user for authorization using Digest Auth + Algorithm. (GET /digest-auth/{qop}/{user}/{passwd}/{algorithm})", + "sortPriority": 46, + "url": "\${[baseUrl]}/digest-auth/:qop/:user/:passwd/:algorithm", + "urlParameters": [ + { + "enabled": true, + "name": ":qop", + "value": "", + }, + { + "enabled": true, + "name": ":user", + "value": "", + }, + { + "enabled": true, + "name": ":passwd", + "value": "", + }, + { + "enabled": true, + "name": ":algorithm", + "value": "MD5", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "allow settings the stale_after argument. + + +Parameters: +- qop (path, required): auth or auth-int +- user (path, required) +- passwd (path, required) +- algorithm (path, required): MD5, SHA-256, SHA-512 +- stale_after (path, required) + +Responses: +- 200: Sucessful authentication. +- 401: Unsuccessful authentication.", + "folderId": "GENERATE_ID::FOLDER_1", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_35", + "method": "GET", + "model": "http_request", + "name": "Prompts the user for authorization using Digest Auth + Algorithm. (GET /digest-auth/{qop}/{user}/{passwd}/{algorithm}/{stale_after})", + "sortPriority": 47, + "url": "\${[baseUrl]}/digest-auth/:qop/:user/:passwd/:algorithm/:stale_after", + "urlParameters": [ + { + "enabled": true, + "name": ":qop", + "value": "", + }, + { + "enabled": true, + "name": ":user", + "value": "", + }, + { + "enabled": true, + "name": ":passwd", + "value": "", + }, + { + "enabled": true, + "name": ":algorithm", + "value": "MD5", + }, + { + "enabled": true, + "name": ":stale_after", + "value": "never", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Drips data over a duration after an optional initial delay. + +Parameters: +- duration (query): The amount of time (in seconds) over which to drip each byte +- numbytes (query): The number of bytes to respond with +- code (query): The response code that will be returned +- delay (query): The amount of time (in seconds) to delay before responding + +Responses: +- 200: A dripped response.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_36", + "method": "GET", + "model": "http_request", + "name": "Drips data over a duration after an optional initial delay.", + "sortPriority": 48, + "url": "\${[baseUrl]}/drip", + "urlParameters": [ + { + "enabled": false, + "name": "duration", + "value": "2", + }, + { + "enabled": false, + "name": "numbytes", + "value": "10", + }, + { + "enabled": false, + "name": "code", + "value": "200", + }, + { + "enabled": false, + "name": "delay", + "value": "2", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a UTF-8 encoded body. + +Responses: +- 200: Encoded UTF-8 content.", + "folderId": "GENERATE_ID::FOLDER_5", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_37", + "method": "GET", + "model": "http_request", + "name": "Returns a UTF-8 encoded body.", + "sortPriority": 49, + "url": "\${[baseUrl]}/encoding/utf8", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Assumes the resource has the given etag and responds to If-None-Match and If-Match headers appropriately. + +Parameters: +- If-None-Match (header) +- If-Match (header) +- etag (path, required): Automatically added + +Responses: +- 200: Normal response +- 412: match", + "folderId": "GENERATE_ID::FOLDER_4", + "headers": [ + { + "enabled": false, + "name": "If-None-Match", + "value": "", + }, + { + "enabled": false, + "name": "If-Match", + "value": "", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_38", + "method": "GET", + "model": "http_request", + "name": "Assumes the resource has the given etag and responds to If-None-Match and If-Match headers appropriately.", + "sortPriority": 50, + "url": "\${[baseUrl]}/etag/:etag", + "urlParameters": [ + { + "enabled": true, + "name": ":etag", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "The request's query parameters. + +Responses: +- 200: The request's query parameters.", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_39", + "method": "GET", + "model": "http_request", + "name": "The request's query parameters.", + "sortPriority": 51, + "url": "\${[baseUrl]}/get", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns GZip-encoded data. + +Responses: +- 200: GZip-encoded data.", + "folderId": "GENERATE_ID::FOLDER_5", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_40", + "method": "GET", + "model": "http_request", + "name": "Returns GZip-encoded data.", + "sortPriority": 52, + "url": "\${[baseUrl]}/gzip", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Return the incoming request's HTTP headers. + +Responses: +- 200: The request's headers.", + "folderId": "GENERATE_ID::FOLDER_3", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_41", + "method": "GET", + "model": "http_request", + "name": "Return the incoming request's HTTP headers.", + "sortPriority": 53, + "url": "\${[baseUrl]}/headers", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Prompts the user for authorization using HTTP Basic Auth. + +Parameters: +- user (path, required) +- passwd (path, required) + +Responses: +- 200: Sucessful authentication. +- 404: Unsuccessful authentication.", + "folderId": "GENERATE_ID::FOLDER_1", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_42", + "method": "GET", + "model": "http_request", + "name": "Prompts the user for authorization using HTTP Basic Auth. (GET /hidden-basic-auth/{user}/{passwd})", + "sortPriority": 54, + "url": "\${[baseUrl]}/hidden-basic-auth/:user/:passwd", + "urlParameters": [ + { + "enabled": true, + "name": ":user", + "value": "", + }, + { + "enabled": true, + "name": ":passwd", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a simple HTML document. + +Responses: +- 200: An HTML page.", + "folderId": "GENERATE_ID::FOLDER_5", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_43", + "method": "GET", + "model": "http_request", + "name": "Returns a simple HTML document.", + "sortPriority": 55, + "url": "\${[baseUrl]}/html", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a simple image of the type suggest by the Accept header. + +Responses: +- 200: An image.", + "folderId": "GENERATE_ID::FOLDER_8", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_44", + "method": "GET", + "model": "http_request", + "name": "Returns a simple image of the type suggest by the Accept header.", + "sortPriority": 56, + "url": "\${[baseUrl]}/image", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a simple JPEG image. + +Responses: +- 200: A JPEG image.", + "folderId": "GENERATE_ID::FOLDER_8", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_45", + "method": "GET", + "model": "http_request", + "name": "Returns a simple JPEG image.", + "sortPriority": 57, + "url": "\${[baseUrl]}/image/jpeg", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a simple PNG image. + +Responses: +- 200: A PNG image.", + "folderId": "GENERATE_ID::FOLDER_8", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_46", + "method": "GET", + "model": "http_request", + "name": "Returns a simple PNG image.", + "sortPriority": 58, + "url": "\${[baseUrl]}/image/png", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a simple SVG image. + +Responses: +- 200: An SVG image.", + "folderId": "GENERATE_ID::FOLDER_8", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_47", + "method": "GET", + "model": "http_request", + "name": "Returns a simple SVG image.", + "sortPriority": 59, + "url": "\${[baseUrl]}/image/svg", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a simple WEBP image. + +Responses: +- 200: A WEBP image.", + "folderId": "GENERATE_ID::FOLDER_8", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_48", + "method": "GET", + "model": "http_request", + "name": "Returns a simple WEBP image.", + "sortPriority": 60, + "url": "\${[baseUrl]}/image/webp", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns the requester's IP Address. + +Responses: +- 200: The Requester's IP Address.", + "folderId": "GENERATE_ID::FOLDER_3", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_49", + "method": "GET", + "model": "http_request", + "name": "Returns the requester's IP Address.", + "sortPriority": 61, + "url": "\${[baseUrl]}/ip", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a simple JSON document. + +Responses: +- 200: An JSON document.", + "folderId": "GENERATE_ID::FOLDER_5", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_50", + "method": "GET", + "model": "http_request", + "name": "Returns a simple JSON document.", + "sortPriority": 62, + "url": "\${[baseUrl]}/json", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Generate a page containing n links to other pages which do the same. + +Parameters: +- n (path, required) +- offset (path, required) + +Responses: +- 200: HTML links.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_51", + "method": "GET", + "model": "http_request", + "name": "Generate a page containing n links to other pages which do the same.", + "sortPriority": 63, + "url": "\${[baseUrl]}/links/:n/:offset", + "urlParameters": [ + { + "enabled": true, + "name": ":n", + "value": "0", + }, + { + "enabled": true, + "name": ":offset", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "The request's PATCH parameters. + +Responses: +- 200: The request's PATCH parameters.", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_52", + "method": "PATCH", + "model": "http_request", + "name": "The request's PATCH parameters.", + "sortPriority": 64, + "url": "\${[baseUrl]}/patch", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "The request's POST parameters. + +Responses: +- 200: The request's POST parameters.", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_53", + "method": "POST", + "model": "http_request", + "name": "The request's POST parameters.", + "sortPriority": 65, + "url": "\${[baseUrl]}/post", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "The request's PUT parameters. + +Responses: +- 200: The request's PUT parameters.", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_54", + "method": "PUT", + "model": "http_request", + "name": "The request's PUT parameters.", + "sortPriority": 66, + "url": "\${[baseUrl]}/put", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Streams n random bytes generated with given seed, at given chunk size per packet. + +Parameters: +- numbytes (path, required) + +Responses: +- 200: Bytes.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_55", + "method": "GET", + "model": "http_request", + "name": "Streams n random bytes generated with given seed, at given chunk size per packet. (GET /range/{numbytes})", + "sortPriority": 67, + "url": "\${[baseUrl]}/range/:numbytes", + "urlParameters": [ + { + "enabled": true, + "name": ":numbytes", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "302/3XX Redirects to the given URL. + +Responses: +- 302: A redirection.", + "folderId": "GENERATE_ID::FOLDER_9", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_56", + "method": "DELETE", + "model": "http_request", + "name": "302/3XX Redirects to the given URL. (DELETE /redirect-to)", + "sortPriority": 68, + "url": "\${[baseUrl]}/redirect-to", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "302/3XX Redirects to the given URL. + +Parameters: +- url (query, required) +- status_code (query) + +Responses: +- 302: A redirection.", + "folderId": "GENERATE_ID::FOLDER_9", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_57", + "method": "GET", + "model": "http_request", + "name": "302/3XX Redirects to the given URL. (GET /redirect-to)", + "sortPriority": 69, + "url": "\${[baseUrl]}/redirect-to", + "urlParameters": [ + { + "enabled": true, + "name": "url", + "value": "", + }, + { + "enabled": false, + "name": "status_code", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "302/3XX Redirects to the given URL. + +Responses: +- 302: A redirection.", + "folderId": "GENERATE_ID::FOLDER_9", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_58", + "method": "PATCH", + "model": "http_request", + "name": "302/3XX Redirects to the given URL. (PATCH /redirect-to)", + "sortPriority": 70, + "url": "\${[baseUrl]}/redirect-to", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": { + "form": [ + { + "enabled": false, + "name": "status_code", + "value": "0", + }, + { + "enabled": true, + "name": "url", + "value": "", + }, + ], + }, + "bodyType": "application/x-www-form-urlencoded", + "description": "302/3XX Redirects to the given URL. + +Request body: +Selected content type: application/x-www-form-urlencoded +Available content types: application/x-www-form-urlencoded + +Responses: +- 302: A redirection.", + "folderId": "GENERATE_ID::FOLDER_9", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/x-www-form-urlencoded", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_59", + "method": "POST", + "model": "http_request", + "name": "302/3XX Redirects to the given URL. (POST /redirect-to)", + "sortPriority": 71, + "url": "\${[baseUrl]}/redirect-to", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": { + "form": [ + { + "enabled": false, + "name": "status_code", + "value": "0", + }, + { + "enabled": true, + "name": "url", + "value": "", + }, + ], + }, + "bodyType": "application/x-www-form-urlencoded", + "description": "302/3XX Redirects to the given URL. + +Request body: +Selected content type: application/x-www-form-urlencoded +Available content types: application/x-www-form-urlencoded + +Responses: +- 302: A redirection.", + "folderId": "GENERATE_ID::FOLDER_9", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/x-www-form-urlencoded", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_60", + "method": "PUT", + "model": "http_request", + "name": "302/3XX Redirects to the given URL. (PUT /redirect-to)", + "sortPriority": 72, + "url": "\${[baseUrl]}/redirect-to", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "302/3XX Redirects to the given URL. + +Responses: +- 302: A redirection.", + "folderId": "GENERATE_ID::FOLDER_9", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_61", + "method": "TRACE", + "model": "http_request", + "name": "302/3XX Redirects to the given URL. (TRACE /redirect-to)", + "sortPriority": 73, + "url": "\${[baseUrl]}/redirect-to", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "302 Redirects n times. + +Parameters: +- n (path, required) + +Responses: +- 302: A redirection.", + "folderId": "GENERATE_ID::FOLDER_9", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_62", + "method": "GET", + "model": "http_request", + "name": "302 Redirects n times.", + "sortPriority": 74, + "url": "\${[baseUrl]}/redirect/:n", + "urlParameters": [ + { + "enabled": true, + "name": ":n", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Relatively 302 Redirects n times. + +Parameters: +- n (path, required) + +Responses: +- 302: A redirection.", + "folderId": "GENERATE_ID::FOLDER_9", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_63", + "method": "GET", + "model": "http_request", + "name": "Relatively 302 Redirects n times.", + "sortPriority": 75, + "url": "\${[baseUrl]}/relative-redirect/:n", + "urlParameters": [ + { + "enabled": true, + "name": ":n", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a set of response headers from the query string. + +Parameters: +- freeform (query) + +Responses: +- 200: Response headers", + "folderId": "GENERATE_ID::FOLDER_4", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_64", + "method": "GET", + "model": "http_request", + "name": "Returns a set of response headers from the query string. (GET /response-headers)", + "sortPriority": 76, + "url": "\${[baseUrl]}/response-headers", + "urlParameters": [ + { + "enabled": false, + "name": "freeform", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a set of response headers from the query string. + +Parameters: +- freeform (query) + +Responses: +- 200: Response headers", + "folderId": "GENERATE_ID::FOLDER_4", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_65", + "method": "POST", + "model": "http_request", + "name": "Returns a set of response headers from the query string. (POST /response-headers)", + "sortPriority": 77, + "url": "\${[baseUrl]}/response-headers", + "urlParameters": [ + { + "enabled": false, + "name": "freeform", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns some robots.txt rules. + +Responses: +- 200: Robots file", + "folderId": "GENERATE_ID::FOLDER_5", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_66", + "method": "GET", + "model": "http_request", + "name": "Returns some robots.txt rules.", + "sortPriority": 78, + "url": "\${[baseUrl]}/robots.txt", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Return status code or random status code if more than one are given + +Parameters: +- codes (path, required) + +Responses: +- 100: Informational responses +- 200: Success +- 300: Redirection +- 400: Client Errors +- 500: Server Errors", + "folderId": "GENERATE_ID::FOLDER_2", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_67", + "method": "DELETE", + "model": "http_request", + "name": "Return status code or random status code if more than one are given (DELETE /status/{codes})", + "sortPriority": 79, + "url": "\${[baseUrl]}/status/:codes", + "urlParameters": [ + { + "enabled": true, + "name": ":codes", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Return status code or random status code if more than one are given + +Parameters: +- codes (path, required) + +Responses: +- 100: Informational responses +- 200: Success +- 300: Redirection +- 400: Client Errors +- 500: Server Errors", + "folderId": "GENERATE_ID::FOLDER_2", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_68", + "method": "GET", + "model": "http_request", + "name": "Return status code or random status code if more than one are given (GET /status/{codes})", + "sortPriority": 80, + "url": "\${[baseUrl]}/status/:codes", + "urlParameters": [ + { + "enabled": true, + "name": ":codes", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Return status code or random status code if more than one are given + +Parameters: +- codes (path, required) + +Responses: +- 100: Informational responses +- 200: Success +- 300: Redirection +- 400: Client Errors +- 500: Server Errors", + "folderId": "GENERATE_ID::FOLDER_2", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_69", + "method": "PATCH", + "model": "http_request", + "name": "Return status code or random status code if more than one are given (PATCH /status/{codes})", + "sortPriority": 81, + "url": "\${[baseUrl]}/status/:codes", + "urlParameters": [ + { + "enabled": true, + "name": ":codes", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Return status code or random status code if more than one are given + +Parameters: +- codes (path, required) + +Responses: +- 100: Informational responses +- 200: Success +- 300: Redirection +- 400: Client Errors +- 500: Server Errors", + "folderId": "GENERATE_ID::FOLDER_2", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_70", + "method": "POST", + "model": "http_request", + "name": "Return status code or random status code if more than one are given (POST /status/{codes})", + "sortPriority": 82, + "url": "\${[baseUrl]}/status/:codes", + "urlParameters": [ + { + "enabled": true, + "name": ":codes", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Return status code or random status code if more than one are given + +Parameters: +- codes (path, required) + +Responses: +- 100: Informational responses +- 200: Success +- 300: Redirection +- 400: Client Errors +- 500: Server Errors", + "folderId": "GENERATE_ID::FOLDER_2", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_71", + "method": "PUT", + "model": "http_request", + "name": "Return status code or random status code if more than one are given (PUT /status/{codes})", + "sortPriority": 83, + "url": "\${[baseUrl]}/status/:codes", + "urlParameters": [ + { + "enabled": true, + "name": ":codes", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Return status code or random status code if more than one are given + +Parameters: +- codes (path, required) + +Responses: +- 100: Informational responses +- 200: Success +- 300: Redirection +- 400: Client Errors +- 500: Server Errors", + "folderId": "GENERATE_ID::FOLDER_2", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_72", + "method": "TRACE", + "model": "http_request", + "name": "Return status code or random status code if more than one are given (TRACE /status/{codes})", + "sortPriority": 84, + "url": "\${[baseUrl]}/status/:codes", + "urlParameters": [ + { + "enabled": true, + "name": ":codes", + "value": "", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Streams n random bytes generated with given seed, at given chunk size per packet. + +Parameters: +- n (path, required) + +Responses: +- 200: Bytes.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_73", + "method": "GET", + "model": "http_request", + "name": "Streams n random bytes generated with given seed, at given chunk size per packet. (GET /stream-bytes/{n})", + "sortPriority": 85, + "url": "\${[baseUrl]}/stream-bytes/:n", + "urlParameters": [ + { + "enabled": true, + "name": ":n", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Stream n JSON responses + +Parameters: +- n (path, required) + +Responses: +- 200: Streamed JSON responses.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_74", + "method": "GET", + "model": "http_request", + "name": "Stream n JSON responses", + "sortPriority": 86, + "url": "\${[baseUrl]}/stream/:n", + "urlParameters": [ + { + "enabled": true, + "name": ":n", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Return the incoming requests's User-Agent header. + +Responses: +- 200: The request's User-Agent header.", + "folderId": "GENERATE_ID::FOLDER_3", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_75", + "method": "GET", + "model": "http_request", + "name": "Return the incoming requests's User-Agent header.", + "sortPriority": 87, + "url": "\${[baseUrl]}/user-agent", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Return a UUID4. + +Responses: +- 200: A UUID4.", + "folderId": "GENERATE_ID::FOLDER_6", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_76", + "method": "GET", + "model": "http_request", + "name": "Return a UUID4.", + "sortPriority": 88, + "url": "\${[baseUrl]}/uuid", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Returns a simple XML document. + +Responses: +- 200: An XML document.", + "folderId": "GENERATE_ID::FOLDER_5", + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_77", + "method": "GET", + "model": "http_request", + "name": "Returns a simple XML document.", + "sortPriority": 89, + "url": "\${[baseUrl]}/xml", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + ], + "websocketRequests": [], + "workspaces": [ + { + "description": "A simple HTTP Request & Response Service.

Run locally: $ docker run -p 80:80 kennethreitz/httpbin + +Contact: me@kennethreitz.org", + "id": "GENERATE_ID::WORKSPACE_0", + "model": "workspace", + "name": "httpbin.org", + }, + ], + }, +} +`; + +exports[`importer-openapi > Snapshots real-world fixture nasa-apod.yaml 1`] = ` +{ + "resources": { + "environments": [ + { + "id": "GENERATE_ID::ENVIRONMENT_0", + "model": "environment", + "name": "Global Variables", + "parentId": null, + "parentModel": "workspace", + "sortPriority": 0, + "variables": [ + { + "name": "baseUrl", + "value": "https://api.nasa.gov/planetary", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + ], + "folders": [ + { + "description": "An example tag + +Here's a link: https://example.com", + "folderId": null, + "id": "GENERATE_ID::FOLDER_0", + "model": "folder", + "name": "request tag", + "sortPriority": 1, + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + ], + "grpcRequests": [], + "httpRequests": [ + { + "authentication": { + "key": "api_key", + "location": "query", + "value": "", + }, + "authenticationType": "apikey", + "body": {}, + "bodyType": null, + "description": "Returns the picture of the day + +Parameters: +- date (query): The date of the APOD image to retrieve +- hd (query): Retrieve the URL for the high resolution image + +Responses: +- 200: successful operation +- 400: Date must be between Jun 16, 1995 and Mar 28, 2019.", + "folderId": "GENERATE_ID::FOLDER_0", + "headers": [ + { + "enabled": true, + "name": "Accept", + "value": "application/json", + }, + ], + "id": "GENERATE_ID::HTTP_REQUEST_0", + "method": "GET", + "model": "http_request", + "name": "Returns images", + "sortPriority": 2, + "url": "\${[baseUrl]}/apod", + "urlParameters": [ + { + "enabled": false, + "name": "date", + "value": "", + }, + { + "enabled": false, + "name": "hd", + "value": "false", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + ], + "websocketRequests": [], + "workspaces": [ + { + "description": "This endpoint structures the APOD imagery and associated metadata so that it can be repurposed for other applications. In addition, if the concept_tags parameter is set to True, then keywords derived from the image explanation are returned. These keywords could be used as auto-generated hashtags for twitter or instagram feeds; but generally help with discoverability of relevant imagery + +Contact: evan.t.yates@nasa.gov + +License: Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html)", + "id": "GENERATE_ID::WORKSPACE_0", + "model": "workspace", + "name": "APOD", + }, + ], + }, +} +`; + +exports[`importer-openapi > Snapshots real-world fixture xkcd.yaml 1`] = ` +{ + "resources": { + "environments": [ + { + "id": "GENERATE_ID::ENVIRONMENT_0", + "model": "environment", + "name": "Global Variables", + "parentId": null, + "parentModel": "workspace", + "sortPriority": 0, + "variables": [ + { + "name": "baseUrl", + "value": "http://xkcd.com/", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + ], + "folders": [], + "grpcRequests": [], + "httpRequests": [ + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Fetch current comic and metadata. + + +Responses: +- 200: OK", + "folderId": null, + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_0", + "method": "GET", + "model": "http_request", + "name": "Fetch current comic and metadata.", + "sortPriority": 1, + "url": "\${[baseUrl]}/info.0.json", + "urlParameters": [], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + { + "authentication": {}, + "authenticationType": null, + "body": {}, + "bodyType": null, + "description": "Fetch comics and metadata by comic id. + + +Parameters: +- comicId (path, required) + +Responses: +- 200: OK", + "folderId": null, + "headers": [], + "id": "GENERATE_ID::HTTP_REQUEST_1", + "method": "GET", + "model": "http_request", + "name": "Fetch comics and metadata by comic id.", + "sortPriority": 2, + "url": "\${[baseUrl]}/:comicId/info.0.json", + "urlParameters": [ + { + "enabled": true, + "name": ":comicId", + "value": "0", + }, + ], + "workspaceId": "GENERATE_ID::WORKSPACE_0", + }, + ], + "websocketRequests": [], + "workspaces": [ + { + "description": "Webcomic of romance, sarcasm, math, and language.", + "id": "GENERATE_ID::WORKSPACE_0", + "model": "workspace", + "name": "XKCD", + }, + ], + }, +} +`; diff --git a/plugins/importer-openapi/tests/fixtures/real-world/SOURCES.md b/plugins/importer-openapi/tests/fixtures/real-world/SOURCES.md new file mode 100644 index 00000000..f4bd7af6 --- /dev/null +++ b/plugins/importer-openapi/tests/fixtures/real-world/SOURCES.md @@ -0,0 +1,8 @@ +# Real-World OpenAPI Fixtures + +These fixtures were copied from the public APIs.guru OpenAPI directory: + +- `apis-guru.yaml`: https://api.apis.guru/v2/specs/apis.guru/2.2.0/openapi.yaml +- `httpbin.yaml`: https://api.apis.guru/v2/specs/httpbin.org/0.9.2/openapi.yaml +- `nasa-apod.yaml`: https://api.apis.guru/v2/specs/nasa.gov/apod/1.0.0/openapi.yaml +- `xkcd.yaml`: https://api.apis.guru/v2/specs/xkcd.com/1.0.0/openapi.yaml diff --git a/plugins/importer-openapi/tests/fixtures/real-world/apis-guru.yaml b/plugins/importer-openapi/tests/fixtures/real-world/apis-guru.yaml new file mode 100644 index 00000000..4dc5db11 --- /dev/null +++ b/plugins/importer-openapi/tests/fixtures/real-world/apis-guru.yaml @@ -0,0 +1,399 @@ +openapi: 3.0.0 +servers: + - url: https://api.apis.guru/v2 +info: + contact: + email: mike.ralphson@gmail.com + name: APIs.guru + url: https://APIs.guru + description: | + Wikipedia for Web APIs. Repository of API definitions in OpenAPI format. + **Warning**: If you want to be notified about changes in advance please join our [Slack channel](https://join.slack.com/t/mermade/shared_invite/zt-g78g7xir-MLE_CTCcXCdfJfG3CJe9qA). + Client sample: [[Demo]](https://apis.guru/simple-ui) [[Repo]](https://github.com/APIs-guru/simple-ui) + license: + name: CC0 1.0 + url: https://github.com/APIs-guru/openapi-directory#licenses + title: APIs.guru + version: 2.2.0 + x-apisguru-categories: + - open_data + - developer_tools + x-logo: + url: https://api.apis.guru/v2/cache/logo/https_apis.guru_branding_logo_vertical.svg + x-origin: + - format: openapi + url: https://api.apis.guru/v2/openapi.yaml + version: "3.0" + x-providerName: apis.guru + x-tags: + - API + - Catalog + - Directory + - REST + - Swagger + - OpenAPI +externalDocs: + url: https://github.com/APIs-guru/openapi-directory/blob/master/API.md +security: [] +tags: + - description: Actions relating to APIs in the collection + name: APIs +paths: + /list.json: + get: + description: | + List all APIs in the directory. + Returns links to the OpenAPI definitions for each API in the directory. + If API exist in multiple versions `preferred` one is explicitly marked. + Some basic info from the OpenAPI definition is cached inside each object. + This allows you to generate some simple views without needing to fetch the OpenAPI definition for each API. + operationId: listAPIs + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/APIs" + description: OK + summary: List all APIs + tags: + - APIs + /metrics.json: + get: + description: | + Some basic metrics for the entire directory. + Just stunning numbers to put on a front page and are intended purely for WoW effect :) + operationId: getMetrics + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Metrics" + description: OK + summary: Get basic metrics + tags: + - APIs + /providers.json: + get: + description: | + List all the providers in the directory + operationId: getProviders + responses: + "200": + content: + application/json: + schema: + properties: + data: + items: + minLength: 1 + type: string + minItems: 1 + type: array + type: object + description: OK + summary: List all providers + tags: + - APIs + "/specs/{provider}/{api}.json": + get: + description: Returns the API entry for one specific version of an API where there is no serviceName. + operationId: getAPI + parameters: + - $ref: "#/components/parameters/provider" + - $ref: "#/components/parameters/api" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/API" + description: OK + summary: Retrieve one version of a particular API + tags: + - APIs + "/specs/{provider}/{service}/{api}.json": + get: + description: Returns the API entry for one specific version of an API where there is a serviceName. + operationId: getServiceAPI + parameters: + - $ref: "#/components/parameters/provider" + - in: path + name: service + required: true + schema: + example: graph + maxLength: 255 + minLength: 1 + type: string + - $ref: "#/components/parameters/api" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/API" + description: OK + summary: Retrieve one version of a particular API with a serviceName. + tags: + - APIs + "/{provider}.json": + get: + description: | + List all APIs in the directory for a particular providerName + Returns links to the individual API entry for each API. + operationId: getProvider + parameters: + - $ref: "#/components/parameters/provider" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/APIs" + description: OK + summary: List all APIs for a particular provider + tags: + - APIs + "/{provider}/services.json": + get: + description: | + List all serviceNames in the directory for a particular providerName + operationId: getServices + parameters: + - $ref: "#/components/parameters/provider" + responses: + "200": + content: + application/json: + schema: + properties: + data: + items: + minLength: 0 + type: string + minItems: 1 + type: array + type: object + description: OK + summary: List all serviceNames for a particular provider + tags: + - APIs +components: + parameters: + api: + in: path + name: api + required: true + schema: + example: 2.1.0 + maxLength: 255 + minLength: 1 + type: string + provider: + in: path + name: provider + required: true + schema: + example: apis.guru + maxLength: 255 + minLength: 1 + type: string + schemas: + API: + additionalProperties: false + description: Meta information about API + properties: + added: + description: Timestamp when the API was first added to the directory + format: date-time + type: string + preferred: + description: Recommended version + type: string + versions: + additionalProperties: + $ref: "#/components/schemas/ApiVersion" + description: List of supported versions of the API + minProperties: 1 + type: object + required: + - added + - preferred + - versions + type: object + APIs: + additionalProperties: + $ref: "#/components/schemas/API" + description: | + List of API details. + It is a JSON object with API IDs(`[:]`) as keys. + example: + googleapis.com:drive: + added: 2015-02-22T20:00:45.000Z + preferred: v3 + versions: + v2: + added: 2015-02-22T20:00:45.000Z + info: + title: Drive + version: v2 + x-apiClientRegistration: + url: https://console.developers.google.com + x-logo: + url: https://api.apis.guru/v2/cache/logo/https_www.gstatic.com_images_icons_material_product_2x_drive_32dp.png + x-origin: + format: google + url: https://www.googleapis.com/discovery/v1/apis/drive/v2/rest + version: v1 + x-preferred: false + x-providerName: googleapis.com + x-serviceName: drive + swaggerUrl: https://api.apis.guru/v2/specs/googleapis.com/drive/v2/swagger.json + swaggerYamlUrl: https://api.apis.guru/v2/specs/googleapis.com/drive/v2/swagger.yaml + updated: 2016-06-17T00:21:44.000Z + v3: + added: 2015-12-12T00:25:13.000Z + info: + title: Drive + version: v3 + x-apiClientRegistration: + url: https://console.developers.google.com + x-logo: + url: https://api.apis.guru/v2/cache/logo/https_www.gstatic.com_images_icons_material_product_2x_drive_32dp.png + x-origin: + format: google + url: https://www.googleapis.com/discovery/v1/apis/drive/v3/rest + version: v1 + x-preferred: true + x-providerName: googleapis.com + x-serviceName: drive + swaggerUrl: https://api.apis.guru/v2/specs/googleapis.com/drive/v3/swagger.json + swaggerYamlUrl: https://api.apis.guru/v2/specs/googleapis.com/drive/v3/swagger.yaml + updated: 2016-06-17T00:21:44.000Z + minProperties: 1 + type: object + ApiVersion: + additionalProperties: false + properties: + added: + description: Timestamp when the version was added + format: date-time + type: string + externalDocs: + description: Copy of `externalDocs` section from OpenAPI definition + minProperties: 1 + type: object + info: + description: Copy of `info` section from OpenAPI definition + minProperties: 1 + type: object + link: + description: Link to the individual API entry for this API + format: url + type: string + openapiVer: + description: The value of the `openapi` or `swagger` property of the source definition + type: string + swaggerUrl: + description: URL to OpenAPI definition in JSON format + format: url + type: string + swaggerYamlUrl: + description: URL to OpenAPI definition in YAML format + format: url + type: string + updated: + description: Timestamp when the version was updated + format: date-time + type: string + required: + - added + - updated + - swaggerUrl + - swaggerYamlUrl + - info + - openapiVer + type: object + Metrics: + additionalProperties: false + description: List of basic metrics + example: + datasets: [] + fixedPct: 22 + fixes: 81119 + invalid: 598 + issues: 28 + numAPIs: 2501 + numDrivers: 10 + numEndpoints: 106448 + numProviders: 659 + numSpecs: 3329 + stars: 2429 + thisWeek: + added: 45 + updated: 171 + unofficial: 25 + unreachable: 123 + properties: + datasets: + description: Data used for charting etc + items: {} + type: array + fixedPct: + description: Percentage of all APIs where auto fixes have been applied + type: integer + fixes: + description: Total number of fixes applied across all APIs + type: integer + invalid: + description: Number of newly invalid APIs + type: integer + issues: + description: Open GitHub issues on our main repo + type: integer + numAPIs: + description: Number of unique APIs + minimum: 1 + type: integer + numDrivers: + description: Number of methods of API retrieval + type: integer + numEndpoints: + description: Total number of endpoints inside all definitions + minimum: 1 + type: integer + numProviders: + description: Number of API providers in directory + type: integer + numSpecs: + description: Number of API definitions including different versions of the same API + minimum: 1 + type: integer + stars: + description: GitHub stars for our main repo + type: integer + thisWeek: + description: Summary totals for the last 7 days + properties: + added: + description: APIs added in the last week + type: integer + updated: + description: APIs updated in the last week + type: integer + type: object + unofficial: + description: Number of unofficial APIs + type: integer + unreachable: + description: Number of unreachable (4XX,5XX status) APIs + type: integer + required: + - numSpecs + - numAPIs + - numEndpoints + type: object +x-optic-standard: "@febf8ac6-ee67-4565-b45a-5c85a469dca7/Fz6KU3_wMIO5iJ6_VUZ30" +x-optic-url: https://app.useoptic.com/organizations/febf8ac6-ee67-4565-b45a-5c85a469dca7/apis/_0fKWqUvhs9ssYNkq1k-c diff --git a/plugins/importer-openapi/tests/fixtures/real-world/httpbin.yaml b/plugins/importer-openapi/tests/fixtures/real-world/httpbin.yaml new file mode 100644 index 00000000..25e52a25 --- /dev/null +++ b/plugins/importer-openapi/tests/fixtures/real-world/httpbin.yaml @@ -0,0 +1,1121 @@ +openapi: 3.0.0 +servers: + - url: https://httpbin.org +info: + contact: + email: me@kennethreitz.org + url: https://kennethreitz.org + x-responsibleDeveloper: Kenneth Reitz + x-responsibleOrganization: Kenneth Reitz + description: "A simple HTTP Request & Response Service.

Run locally: $ docker run -p 80:80 kennethreitz/httpbin" + title: httpbin.org + version: 0.9.2 + x-apisguru-categories: + - developer_tools + x-origin: + - format: swagger + url: http://httpbin.org/spec.json + version: "2.0" + x-providerName: httpbin.org + x-logo: + url: https://api.apis.guru/v2/cache/logo/https_apis.guru_assets_images_no-logo.svg +tags: + - description: Testing different HTTP verbs + name: HTTP Methods + - description: Auth methods + name: Auth + - description: Generates responses with given status code + name: Status codes + - description: Inspect the request data + name: Request inspection + - description: Inspect the response data like caching and headers + name: Response inspection + - description: Returns responses in different data formats + name: Response formats + - description: Generates random and dynamic data + name: Dynamic data + - description: Creates, reads and deletes Cookies + name: Cookies + - description: Returns different image formats + name: Images + - description: Returns different redirect responses + name: Redirects + - description: Returns anything that is passed to request + name: Anything +paths: + "/absolute-redirect/{n}": + get: + parameters: + - in: path + name: n + required: true + schema: + type: integer + responses: + "302": + description: A redirection. + summary: Absolutely 302 Redirects n times. + tags: + - Redirects + /anything: + delete: + responses: + "200": + description: Anything passed in request + summary: Returns anything passed in request data. + tags: + - Anything + get: + responses: + "200": + description: Anything passed in request + summary: Returns anything passed in request data. + tags: + - Anything + patch: + responses: + "200": + description: Anything passed in request + summary: Returns anything passed in request data. + tags: + - Anything + post: + responses: + "200": + description: Anything passed in request + summary: Returns anything passed in request data. + tags: + - Anything + put: + responses: + "200": + description: Anything passed in request + summary: Returns anything passed in request data. + tags: + - Anything + trace: + responses: + "200": + description: Anything passed in request + summary: Returns anything passed in request data. + tags: + - Anything + "/anything/{anything}": + delete: + parameters: + - description: Automatically added + in: path + name: anything + required: true + schema: + type: string + responses: + "200": + description: Anything passed in request + summary: Returns anything passed in request data. + tags: + - Anything + get: + parameters: + - description: Automatically added + in: path + name: anything + required: true + schema: + type: string + responses: + "200": + description: Anything passed in request + summary: Returns anything passed in request data. + tags: + - Anything + patch: + parameters: + - description: Automatically added + in: path + name: anything + required: true + schema: + type: string + responses: + "200": + description: Anything passed in request + summary: Returns anything passed in request data. + tags: + - Anything + post: + parameters: + - description: Automatically added + in: path + name: anything + required: true + schema: + type: string + responses: + "200": + description: Anything passed in request + summary: Returns anything passed in request data. + tags: + - Anything + put: + parameters: + - description: Automatically added + in: path + name: anything + required: true + schema: + type: string + responses: + "200": + description: Anything passed in request + summary: Returns anything passed in request data. + tags: + - Anything + trace: + parameters: + - description: Automatically added + in: path + name: anything + required: true + schema: + type: string + responses: + "200": + description: Anything passed in request + summary: Returns anything passed in request data. + tags: + - Anything + "/base64/{value}": + get: + parameters: + - in: path + name: value + required: true + schema: + default: SFRUUEJJTiBpcyBhd2Vzb21l + type: string + responses: + "200": + description: Decoded base64 content. + summary: Decodes base64url-encoded string. + tags: + - Dynamic data + "/basic-auth/{user}/{passwd}": + get: + parameters: + - in: path + name: user + required: true + schema: + type: string + - in: path + name: passwd + required: true + schema: + type: string + responses: + "200": + description: Sucessful authentication. + "401": + description: Unsuccessful authentication. + summary: Prompts the user for authorization using HTTP Basic Auth. + tags: + - Auth + /bearer: + get: + parameters: + - in: header + name: Authorization + schema: + type: string + responses: + "200": + description: Sucessful authentication. + "401": + description: Unsuccessful authentication. + summary: Prompts the user for authorization using bearer authentication. + tags: + - Auth + /brotli: + get: + responses: + "200": + description: Brotli-encoded data. + summary: Returns Brotli-encoded data. + tags: + - Response formats + "/bytes/{n}": + get: + parameters: + - in: path + name: n + required: true + schema: + type: integer + responses: + "200": + description: Bytes. + summary: Returns n random bytes generated with given seed + tags: + - Dynamic data + /cache: + get: + parameters: + - in: header + name: If-Modified-Since + schema: + type: string + - in: header + name: If-None-Match + schema: + type: string + responses: + "200": + description: Cached response + "304": + description: Modified + summary: Returns a 304 if an If-Modified-Since header or If-None-Match is present. Returns the same as a GET otherwise. + tags: + - Response inspection + "/cache/{value}": + get: + parameters: + - in: path + name: value + required: true + schema: + type: integer + responses: + "200": + description: Cache control set + summary: Sets a Cache-Control header for n seconds. + tags: + - Response inspection + /cookies: + get: + responses: + "200": + description: Set cookies. + summary: Returns cookie data. + tags: + - Cookies + /cookies/delete: + get: + parameters: + - allowEmptyValue: true + explode: true + in: query + name: freeform + schema: + additionalProperties: + type: string + type: string + style: form + responses: + "200": + description: Redirect to cookie list + summary: Deletes cookie(s) as provided by the query string and redirects to cookie list. + tags: + - Cookies + /cookies/set: + get: + parameters: + - allowEmptyValue: true + explode: true + in: query + name: freeform + schema: + additionalProperties: + type: string + type: string + style: form + responses: + "200": + description: Redirect to cookie list + summary: Sets cookie(s) as provided by the query string and redirects to cookie list. + tags: + - Cookies + "/cookies/set/{name}/{value}": + get: + parameters: + - in: path + name: name + required: true + schema: + type: string + - in: path + name: value + required: true + schema: + type: string + responses: + "200": + description: Set cookies and redirects to cookie list. + summary: Sets a cookie and redirects to cookie list. + tags: + - Cookies + /deflate: + get: + responses: + "200": + description: Defalte-encoded data. + summary: Returns Deflate-encoded data. + tags: + - Response formats + "/delay/{delay}": + delete: + parameters: + - in: path + name: delay + required: true + schema: + type: integer + responses: + "200": + description: A delayed response. + summary: Returns a delayed response (max of 10 seconds). + tags: + - Dynamic data + get: + parameters: + - in: path + name: delay + required: true + schema: + type: integer + responses: + "200": + description: A delayed response. + summary: Returns a delayed response (max of 10 seconds). + tags: + - Dynamic data + patch: + parameters: + - in: path + name: delay + required: true + schema: + type: integer + responses: + "200": + description: A delayed response. + summary: Returns a delayed response (max of 10 seconds). + tags: + - Dynamic data + post: + parameters: + - in: path + name: delay + required: true + schema: + type: integer + responses: + "200": + description: A delayed response. + summary: Returns a delayed response (max of 10 seconds). + tags: + - Dynamic data + put: + parameters: + - in: path + name: delay + required: true + schema: + type: integer + responses: + "200": + description: A delayed response. + summary: Returns a delayed response (max of 10 seconds). + tags: + - Dynamic data + trace: + parameters: + - in: path + name: delay + required: true + schema: + type: integer + responses: + "200": + description: A delayed response. + summary: Returns a delayed response (max of 10 seconds). + tags: + - Dynamic data + /delete: + delete: + responses: + "200": + description: The request's DELETE parameters. + summary: The request's DELETE parameters. + tags: + - HTTP Methods + /deny: + get: + responses: + "200": + description: Denied message + summary: Returns page denied by robots.txt rules. + tags: + - Response formats + "/digest-auth/{qop}/{user}/{passwd}": + get: + parameters: + - description: auth or auth-int + in: path + name: qop + required: true + schema: + type: string + - in: path + name: user + required: true + schema: + type: string + - in: path + name: passwd + required: true + schema: + type: string + responses: + "200": + description: Sucessful authentication. + "401": + description: Unsuccessful authentication. + summary: Prompts the user for authorization using Digest Auth. + tags: + - Auth + "/digest-auth/{qop}/{user}/{passwd}/{algorithm}": + get: + parameters: + - description: auth or auth-int + in: path + name: qop + required: true + schema: + type: string + - in: path + name: user + required: true + schema: + type: string + - in: path + name: passwd + required: true + schema: + type: string + - description: MD5, SHA-256, SHA-512 + in: path + name: algorithm + required: true + schema: + default: MD5 + type: string + responses: + "200": + description: Sucessful authentication. + "401": + description: Unsuccessful authentication. + summary: Prompts the user for authorization using Digest Auth + Algorithm. + tags: + - Auth + "/digest-auth/{qop}/{user}/{passwd}/{algorithm}/{stale_after}": + get: + description: | + allow settings the stale_after argument. + parameters: + - description: auth or auth-int + in: path + name: qop + required: true + schema: + type: string + - in: path + name: user + required: true + schema: + type: string + - in: path + name: passwd + required: true + schema: + type: string + - description: MD5, SHA-256, SHA-512 + in: path + name: algorithm + required: true + schema: + default: MD5 + type: string + - in: path + name: stale_after + required: true + schema: + default: never + type: string + responses: + "200": + description: Sucessful authentication. + "401": + description: Unsuccessful authentication. + summary: Prompts the user for authorization using Digest Auth + Algorithm. + tags: + - Auth + /drip: + get: + parameters: + - description: The amount of time (in seconds) over which to drip each byte + in: query + name: duration + required: false + schema: + default: 2 + type: number + - description: The number of bytes to respond with + in: query + name: numbytes + required: false + schema: + default: 10 + type: integer + - description: The response code that will be returned + in: query + name: code + required: false + schema: + default: 200 + type: integer + - description: The amount of time (in seconds) to delay before responding + in: query + name: delay + required: false + schema: + default: 2 + type: number + responses: + "200": + description: A dripped response. + summary: Drips data over a duration after an optional initial delay. + tags: + - Dynamic data + /encoding/utf8: + get: + responses: + "200": + description: Encoded UTF-8 content. + summary: Returns a UTF-8 encoded body. + tags: + - Response formats + "/etag/{etag}": + get: + parameters: + - in: header + name: If-None-Match + schema: + type: string + - in: header + name: If-Match + schema: + type: string + - description: Automatically added + in: path + name: etag + required: true + schema: + type: string + responses: + "200": + description: Normal response + "412": + description: match + summary: Assumes the resource has the given etag and responds to If-None-Match and If-Match headers appropriately. + tags: + - Response inspection + /get: + get: + responses: + "200": + description: The request's query parameters. + summary: The request's query parameters. + tags: + - HTTP Methods + /gzip: + get: + responses: + "200": + description: GZip-encoded data. + summary: Returns GZip-encoded data. + tags: + - Response formats + /headers: + get: + responses: + "200": + description: The request's headers. + summary: Return the incoming request's HTTP headers. + tags: + - Request inspection + "/hidden-basic-auth/{user}/{passwd}": + get: + parameters: + - in: path + name: user + required: true + schema: + type: string + - in: path + name: passwd + required: true + schema: + type: string + responses: + "200": + description: Sucessful authentication. + "404": + description: Unsuccessful authentication. + summary: Prompts the user for authorization using HTTP Basic Auth. + tags: + - Auth + /html: + get: + responses: + "200": + description: An HTML page. + summary: Returns a simple HTML document. + tags: + - Response formats + /image: + get: + responses: + "200": + description: An image. + summary: Returns a simple image of the type suggest by the Accept header. + tags: + - Images + /image/jpeg: + get: + responses: + "200": + description: A JPEG image. + summary: Returns a simple JPEG image. + tags: + - Images + /image/png: + get: + responses: + "200": + description: A PNG image. + summary: Returns a simple PNG image. + tags: + - Images + /image/svg: + get: + responses: + "200": + description: An SVG image. + summary: Returns a simple SVG image. + tags: + - Images + /image/webp: + get: + responses: + "200": + description: A WEBP image. + summary: Returns a simple WEBP image. + tags: + - Images + /ip: + get: + responses: + "200": + description: The Requester's IP Address. + summary: Returns the requester's IP Address. + tags: + - Request inspection + /json: + get: + responses: + "200": + description: An JSON document. + summary: Returns a simple JSON document. + tags: + - Response formats + "/links/{n}/{offset}": + get: + parameters: + - in: path + name: n + required: true + schema: + type: integer + - in: path + name: offset + required: true + schema: + type: integer + responses: + "200": + description: HTML links. + summary: Generate a page containing n links to other pages which do the same. + tags: + - Dynamic data + /patch: + patch: + responses: + "200": + description: The request's PATCH parameters. + summary: The request's PATCH parameters. + tags: + - HTTP Methods + /post: + post: + responses: + "200": + description: The request's POST parameters. + summary: The request's POST parameters. + tags: + - HTTP Methods + /put: + put: + responses: + "200": + description: The request's PUT parameters. + summary: The request's PUT parameters. + tags: + - HTTP Methods + "/range/{numbytes}": + get: + parameters: + - in: path + name: numbytes + required: true + schema: + type: integer + responses: + "200": + description: Bytes. + summary: Streams n random bytes generated with given seed, at given chunk size per packet. + tags: + - Dynamic data + /redirect-to: + delete: + responses: + "302": + description: A redirection. + summary: 302/3XX Redirects to the given URL. + tags: + - Redirects + get: + parameters: + - in: query + name: url + required: true + schema: + type: string + - in: query + name: status_code + schema: + type: integer + responses: + "302": + description: A redirection. + summary: 302/3XX Redirects to the given URL. + tags: + - Redirects + patch: + responses: + "302": + description: A redirection. + summary: 302/3XX Redirects to the given URL. + tags: + - Redirects + post: + requestBody: + $ref: "#/components/requestBodies/postRedirectTo" + responses: + "302": + description: A redirection. + summary: 302/3XX Redirects to the given URL. + tags: + - Redirects + put: + requestBody: + $ref: "#/components/requestBodies/postRedirectTo" + responses: + "302": + description: A redirection. + summary: 302/3XX Redirects to the given URL. + tags: + - Redirects + trace: + responses: + "302": + description: A redirection. + summary: 302/3XX Redirects to the given URL. + tags: + - Redirects + "/redirect/{n}": + get: + parameters: + - in: path + name: n + required: true + schema: + type: integer + responses: + "302": + description: A redirection. + summary: 302 Redirects n times. + tags: + - Redirects + "/relative-redirect/{n}": + get: + parameters: + - in: path + name: n + required: true + schema: + type: integer + responses: + "302": + description: A redirection. + summary: Relatively 302 Redirects n times. + tags: + - Redirects + /response-headers: + get: + parameters: + - allowEmptyValue: true + explode: true + in: query + name: freeform + schema: + additionalProperties: + type: string + type: string + style: form + responses: + "200": + description: Response headers + summary: Returns a set of response headers from the query string. + tags: + - Response inspection + post: + parameters: + - allowEmptyValue: true + explode: true + in: query + name: freeform + schema: + additionalProperties: + type: string + type: string + style: form + responses: + "200": + description: Response headers + summary: Returns a set of response headers from the query string. + tags: + - Response inspection + /robots.txt: + get: + responses: + "200": + description: Robots file + summary: Returns some robots.txt rules. + tags: + - Response formats + "/status/{codes}": + delete: + parameters: + - in: path + name: codes + required: true + schema: + type: string + responses: + "100": + description: Informational responses + "200": + description: Success + "300": + description: Redirection + "400": + description: Client Errors + "500": + description: Server Errors + summary: Return status code or random status code if more than one are given + tags: + - Status codes + get: + parameters: + - in: path + name: codes + required: true + schema: + type: string + responses: + "100": + description: Informational responses + "200": + description: Success + "300": + description: Redirection + "400": + description: Client Errors + "500": + description: Server Errors + summary: Return status code or random status code if more than one are given + tags: + - Status codes + patch: + parameters: + - in: path + name: codes + required: true + schema: + type: string + responses: + "100": + description: Informational responses + "200": + description: Success + "300": + description: Redirection + "400": + description: Client Errors + "500": + description: Server Errors + summary: Return status code or random status code if more than one are given + tags: + - Status codes + post: + parameters: + - in: path + name: codes + required: true + schema: + type: string + responses: + "100": + description: Informational responses + "200": + description: Success + "300": + description: Redirection + "400": + description: Client Errors + "500": + description: Server Errors + summary: Return status code or random status code if more than one are given + tags: + - Status codes + put: + parameters: + - in: path + name: codes + required: true + schema: + type: string + responses: + "100": + description: Informational responses + "200": + description: Success + "300": + description: Redirection + "400": + description: Client Errors + "500": + description: Server Errors + summary: Return status code or random status code if more than one are given + tags: + - Status codes + trace: + parameters: + - in: path + name: codes + required: true + schema: + type: string + responses: + "100": + description: Informational responses + "200": + description: Success + "300": + description: Redirection + "400": + description: Client Errors + "500": + description: Server Errors + summary: Return status code or random status code if more than one are given + tags: + - Status codes + "/stream-bytes/{n}": + get: + parameters: + - in: path + name: n + required: true + schema: + type: integer + responses: + "200": + description: Bytes. + summary: Streams n random bytes generated with given seed, at given chunk size per packet. + tags: + - Dynamic data + "/stream/{n}": + get: + parameters: + - in: path + name: n + required: true + schema: + type: integer + responses: + "200": + description: Streamed JSON responses. + summary: Stream n JSON responses + tags: + - Dynamic data + /user-agent: + get: + responses: + "200": + description: The request's User-Agent header. + summary: Return the incoming requests's User-Agent header. + tags: + - Request inspection + /uuid: + get: + responses: + "200": + description: A UUID4. + summary: Return a UUID4. + tags: + - Dynamic data + /xml: + get: + responses: + "200": + description: An XML document. + summary: Returns a simple XML document. + tags: + - Response formats +components: + requestBodies: + postRedirectTo: + content: + application/x-www-form-urlencoded: + schema: + properties: + status_code: + type: integer + url: + type: string + required: + - url + type: object + required: true +x-protocol: https diff --git a/plugins/importer-openapi/tests/fixtures/real-world/nasa-apod.yaml b/plugins/importer-openapi/tests/fixtures/real-world/nasa-apod.yaml new file mode 100644 index 00000000..172317fc --- /dev/null +++ b/plugins/importer-openapi/tests/fixtures/real-world/nasa-apod.yaml @@ -0,0 +1,69 @@ +openapi: 3.0.0 +servers: + - url: https://api.nasa.gov/planetary + - url: http://api.nasa.gov/planetary +info: + contact: + email: evan.t.yates@nasa.gov + description: This endpoint structures the APOD imagery and associated metadata so that it can be repurposed for other applications. In addition, if the concept_tags parameter is set to True, then keywords derived from the image explanation are returned. These keywords could be used as auto-generated hashtags for twitter or instagram feeds; but generally help with discoverability of relevant imagery + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + title: APOD + version: 1.0.0 + x-apisguru-categories: + - media + - open_data + x-origin: + - format: swagger + url: https://raw.githubusercontent.com/nasa/api-docs/gh-pages/assets/json/APOD + version: "2.0" + x-providerName: nasa.gov + x-serviceName: apod + x-logo: + url: https://api.apis.guru/v2/cache/logo/https_apis.guru_assets_images_no-logo.svg +tags: + - description: An example tag + externalDocs: + description: Here's a link + url: https://example.com + name: request tag +paths: + /apod: + get: + description: Returns the picture of the day + parameters: + - description: The date of the APOD image to retrieve + in: query + name: date + required: false + schema: + type: string + - description: Retrieve the URL for the high resolution image + in: query + name: hd + required: false + schema: + type: boolean + responses: + "200": + content: + application/json: + schema: + items: + x-thing: ok + type: array + description: successful operation + "400": + description: Date must be between Jun 16, 1995 and Mar 28, 2019. + security: + - api_key: [] + summary: Returns images + tags: + - request tag +components: + securitySchemes: + api_key: + in: query + name: api_key + type: apiKey diff --git a/plugins/importer-openapi/tests/fixtures/real-world/xkcd.yaml b/plugins/importer-openapi/tests/fixtures/real-world/xkcd.yaml new file mode 100644 index 00000000..49016335 --- /dev/null +++ b/plugins/importer-openapi/tests/fixtures/real-world/xkcd.yaml @@ -0,0 +1,78 @@ +openapi: 3.0.0 +servers: + - url: http://xkcd.com/ +info: + description: Webcomic of romance, sarcasm, math, and language. + title: XKCD + version: 1.0.0 + x-apisguru-categories: + - media + x-logo: + url: https://api.apis.guru/v2/cache/logo/http_imgs.xkcd.com_static_terrible_small_logo.png + x-origin: + - format: openapi + url: https://raw.githubusercontent.com/APIs-guru/unofficial_openapi_specs/master/xkcd.com/1.0.0/openapi.yaml + version: "3.0" + x-providerName: xkcd.com + x-tags: + - humor + - comics + x-unofficialSpec: true +externalDocs: + url: https://xkcd.com/json.html +paths: + /info.0.json: + get: + description: | + Fetch current comic and metadata. + responses: + "200": + content: + "*/*": + schema: + $ref: "#/components/schemas/comic" + description: OK + "/{comicId}/info.0.json": + get: + description: | + Fetch comics and metadata by comic id. + parameters: + - in: path + name: comicId + required: true + schema: + type: number + responses: + "200": + content: + "*/*": + schema: + $ref: "#/components/schemas/comic" + description: OK +components: + schemas: + comic: + properties: + alt: + type: string + day: + type: string + img: + type: string + link: + type: string + month: + type: string + news: + type: string + num: + type: number + safe_title: + type: string + title: + type: string + transcript: + type: string + year: + type: string + type: object diff --git a/plugins/importer-openapi/tests/index.test.ts b/plugins/importer-openapi/tests/index.test.ts index efb29c9a..68ca24fb 100644 --- a/plugins/importer-openapi/tests/index.test.ts +++ b/plugins/importer-openapi/tests/index.test.ts @@ -5,7 +5,13 @@ import { convertOpenApi } from "../src"; describe("importer-openapi", () => { const p = path.join(__dirname, "fixtures"); - const fixtures = fs.readdirSync(p); + const fixtures = fs.readdirSync(p).filter((fixture) => { + return fs.statSync(path.join(p, fixture)).isFile(); + }); + const realWorldFixturesPath = path.join(p, "real-world"); + const realWorldFixtures = fs + .readdirSync(realWorldFixturesPath) + .filter((fixture) => fixture.endsWith(".yaml")); test("Maps operation description to request description", async () => { const imported = await convertOpenApi( @@ -25,7 +31,195 @@ describe("importer-openapi", () => { expect(imported?.resources.httpRequests).toEqual([ expect.objectContaining({ - description: "Lijst van klanten", + description: expect.stringContaining("Lijst van klanten"), + }), + ]); + }); + + test("Imports requests directly from OpenAPI details", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.0.0", + info: { title: "Native Import Test", version: "1.0.0" }, + servers: [ + { url: "https://api.example.com/{version}", variables: { version: { default: "v1" } } }, + ], + tags: [{ name: "accounts", description: "Account operations" }], + paths: { + "/accounts/{accountId}/members": { + parameters: [ + { + name: "accountId", + in: "path", + required: true, + description: "Account identifier", + schema: { type: "string", example: "acct_123" }, + }, + ], + post: { + tags: ["accounts"], + summary: "Create member", + operationId: "createMember", + parameters: [ + { + name: "include", + in: "query", + description: "Related resources to include", + schema: { type: "string", enum: ["roles"] }, + }, + { + name: "X-Trace-Id", + in: "header", + schema: { type: "string", example: "trace-123" }, + }, + ], + security: [{ tokenAuth: [] }], + requestBody: { + description: "Member payload", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/MemberInput" }, + }, + }, + }, + responses: { + "201": { description: "Created" }, + }, + }, + }, + }, + components: { + securitySchemes: { + tokenAuth: { type: "http", scheme: "bearer" }, + }, + schemas: { + MemberInput: { + type: "object", + required: ["email"], + properties: { + email: { type: "string", example: "me@example.com" }, + admin: { type: "boolean", default: false }, + primaryContact: { $ref: "#/components/schemas/Contact" }, + secondaryContact: { $ref: "#/components/schemas/Contact" }, + }, + }, + Contact: { + type: "object", + properties: { + name: { type: "string", example: "Taylor" }, + }, + }, + }, + }, + }), + ); + + expect(imported?.resources.folders).toEqual([ + expect.objectContaining({ name: "accounts", description: "Account operations" }), + ]); + expect(imported?.resources.environments).toEqual([ + expect.objectContaining({ + name: "Global Variables", + variables: [{ name: "baseUrl", value: "https://api.example.com/v1" }], + }), + ]); + expect(imported?.resources.httpRequests).toEqual([ + expect.objectContaining({ + name: "Create member", + method: "POST", + url: "${[baseUrl]}/accounts/:accountId/members", + authenticationType: "bearer", + authentication: { token: "", prefix: "Bearer" }, + bodyType: "application/json", + body: { + text: JSON.stringify( + { + email: "me@example.com", + admin: false, + primaryContact: { name: "Taylor" }, + secondaryContact: { name: "Taylor" }, + }, + null, + 2, + ), + }, + headers: expect.arrayContaining([ + { enabled: false, name: "X-Trace-Id", value: "trace-123" }, + { enabled: true, name: "Content-Type", value: "application/json" }, + ]), + urlParameters: [ + { enabled: true, name: ":accountId", value: "acct_123" }, + { enabled: false, name: "include", value: "roles" }, + ], + description: expect.stringContaining("Operation ID: createMember"), + }), + ]); + expect(imported?.resources.httpRequests[0]?.description).toContain("Member payload"); + expect(imported?.resources.httpRequests[0]?.description).toContain("201: Created"); + }); + + test("Handles large schemas without the Postman converter path", async () => { + const paths: Record = {}; + for (let i = 0; i < 500; i++) { + paths[`/zones/{zoneId}/resources/${i}`] = { + get: { + tags: ["zones"], + summary: `Read resource ${i}`, + parameters: [ + { name: "zoneId", in: "path", required: true, schema: { type: "string" } }, + { name: "page", in: "query", schema: { type: "integer", default: 1 } }, + ], + responses: { + "200": { + description: "OK", + content: { + "application/json": { schema: { $ref: "#/components/schemas/Resource" } }, + }, + }, + }, + }, + }; + } + + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.1.0", + info: { title: "Large API", version: "1.0.0" }, + servers: [{ url: "https://api.example.com/client/v4" }], + tags: [{ name: "zones" }], + paths, + components: { + schemas: { + Resource: { + type: "object", + properties: { + id: { type: "string" }, + name: { type: "string" }, + metadata: { $ref: "#/components/schemas/Metadata" }, + }, + }, + Metadata: { + type: "object", + properties: { + createdOn: { type: "string", format: "date-time" }, + tags: { type: "array", items: { type: "string" } }, + }, + }, + }, + }, + }), + ); + + expect(imported?.resources.httpRequests.length).toBe(500); + expect(imported?.resources.httpRequests[499]).toEqual( + expect.objectContaining({ + name: "Read resource 499", + url: "${[baseUrl]}/zones/:zoneId/resources/499", + }), + ); + expect(imported?.resources.environments).toEqual([ + expect.objectContaining({ + variables: [{ name: "baseUrl", value: "https://api.example.com/client/v4" }], }), ]); }); @@ -35,6 +229,340 @@ describe("importer-openapi", () => { expect(imported).toBeUndefined(); }); + test("Prefers operation and path servers over the spec base URL", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.0.0", + info: { title: "Servers Test", version: "1.0.0" }, + servers: [{ url: "https://root.example.com" }], + paths: { + "/root": { get: { responses: {} } }, + "/path-level": { + servers: [{ url: "https://path.example.com" }], + get: { responses: {} }, + }, + "/operation-level": { + servers: [{ url: "https://path.example.com" }], + get: { servers: [{ url: "https://operation.example.com" }], responses: {} }, + }, + }, + }), + ); + + expect(imported?.resources.httpRequests.map((r) => r.url)).toEqual([ + "${[baseUrl]}/root", + "https://path.example.com/path-level", + "https://operation.example.com/operation-level", + ]); + }); + + test("Imports OpenAPI 3 OAuth2 flows", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.0.0", + info: { title: "OAuth Test", version: "1.0.0" }, + paths: { + "/a": { get: { security: [{ oauth: ["read", "write"] }], responses: {} } }, + "/b": { get: { security: [{ implicitOauth: [] }], responses: {} } }, + }, + components: { + securitySchemes: { + oauth: { + type: "oauth2", + flows: { + clientCredentials: { tokenUrl: "https://example.com/token", scopes: {} }, + }, + }, + implicitOauth: { + type: "oauth2", + flows: { + implicit: { authorizationUrl: "https://example.com/authorize", scopes: {} }, + }, + }, + }, + }, + }), + ); + + expect(imported?.resources.httpRequests[0]).toEqual( + expect.objectContaining({ + authenticationType: "oauth2", + authentication: { + grantType: "client_credentials", + clientId: "", + clientSecret: "", + headerPrefix: "Bearer", + scope: "read write", + accessTokenUrl: "https://example.com/token", + }, + }), + ); + expect(imported?.resources.httpRequests[1]).toEqual( + expect.objectContaining({ + authenticationType: "oauth2", + authentication: { + grantType: "implicit", + clientId: "", + headerPrefix: "Bearer", + authorizationUrl: "https://example.com/authorize", + }, + }), + ); + }); + + test("Imports Swagger 2 OAuth2 flows and produces", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + swagger: "2.0", + info: { title: "Swagger OAuth Test", version: "1.0.0" }, + host: "example.com", + produces: ["application/json"], + paths: { "/a": { get: { security: [{ oauth: ["admin"] }], responses: {} } } }, + securityDefinitions: { + oauth: { + type: "oauth2", + flow: "accessCode", + authorizationUrl: "https://example.com/authorize", + tokenUrl: "https://example.com/token", + scopes: { admin: "Admin access" }, + }, + }, + }), + ); + + expect(imported?.resources.httpRequests[0]).toEqual( + expect.objectContaining({ + authenticationType: "oauth2", + authentication: { + grantType: "authorization_code", + clientId: "", + clientSecret: "", + headerPrefix: "Bearer", + scope: "admin", + authorizationUrl: "https://example.com/authorize", + accessTokenUrl: "https://example.com/token", + }, + headers: [{ enabled: true, name: "Accept", value: "application/json" }], + }), + ); + }); + + test("Names operations that only carry a description", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.0.0", + info: { title: "Naming Test", version: "1.0.0" }, + paths: { + "/a": { get: { description: "Fetch the current comic.\nMore detail here.\n" } }, + "/b": { get: { description: `${"x".repeat(101)}` } }, + "/c": { get: { summary: "Explicit summary", description: "Ignored" } }, + }, + }), + ); + + expect(imported?.resources.httpRequests.map((r) => r.name)).toEqual([ + "Fetch the current comic.", + // Too long to read as a name, so the route is clearer + "GET /b", + "Explicit summary", + ]); + }); + + test("Disambiguates requests that would share a name", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.0.0", + info: { title: "Duplicate Test", version: "1.0.0" }, + paths: { + "/anything": { + get: { summary: "Returns anything" }, + post: { summary: "Returns anything" }, + }, + "/unique": { get: { summary: "Stands alone" } }, + }, + }), + ); + + expect(imported?.resources.httpRequests.map((r) => r.name)).toEqual([ + "Returns anything (GET /anything)", + "Returns anything (POST /anything)", + "Stands alone", + ]); + }); + + test("Flags deprecated operations", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.0.0", + info: { title: "Deprecated Test", version: "1.0.0" }, + paths: { + "/old": { get: { summary: "Old", deprecated: true, description: "Use /new instead." } }, + "/new": { get: { summary: "New" } }, + }, + }), + ); + + expect(imported?.resources.httpRequests[0]?.description).toBe( + "Deprecated.\n\nUse /new instead.", + ); + expect(imported?.resources.httpRequests[1]?.description).toBe("New"); + }); + + test("Derives an Accept header from OpenAPI 3 responses", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.0.0", + info: { title: "Accept Test", version: "1.0.0" }, + paths: { + "/prefers-json": { + get: { + responses: { + "200": { + description: "ok", + content: { "application/xml": {}, "application/json": {} }, + }, + }, + }, + }, + // Only failures describe content, so there is nothing to accept + "/errors-only": { + get: { + responses: { "500": { description: "nope", content: { "application/json": {} } } }, + }, + }, + }, + }), + ); + + expect(imported?.resources.httpRequests[0]?.headers).toEqual([ + { enabled: true, name: "Accept", value: "application/json" }, + ]); + expect(imported?.resources.httpRequests[1]?.headers).toEqual([]); + }); + + test("Lets an operation override a path-level parameter", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.0.0", + info: { title: "Override Test", version: "1.0.0" }, + paths: { + "/a": { + parameters: [ + { name: "page", in: "query", required: false, schema: { example: "path-level" } }, + { name: "keep", in: "query", required: true, schema: { example: "untouched" } }, + ], + get: { + parameters: [ + // Same name and location as above, so it replaces rather than adds + { name: "page", in: "query", required: true, schema: { example: "operation" } }, + // Same name but a different location, so it is its own parameter + { name: "page", in: "header", schema: { example: "header-level" } }, + ], + responses: {}, + }, + }, + }, + }), + ); + + expect(imported?.resources.httpRequests[0]?.urlParameters).toEqual([ + { enabled: true, name: "page", value: "operation" }, + { enabled: true, name: "keep", value: "untouched" }, + ]); + expect(imported?.resources.httpRequests[0]?.headers).toEqual([ + { enabled: false, name: "page", value: "header-level" }, + ]); + }); + + test("Prefers operation-level consumes for Swagger bodies", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + swagger: "2.0", + info: { title: "Consumes Test", version: "1.0.0" }, + host: "example.com", + consumes: ["application/json"], + paths: { + "/a": { + post: { + consumes: ["application/xml"], + parameters: [{ name: "body", in: "body", schema: { type: "object" } }], + responses: {}, + }, + }, + }, + }), + ); + + expect(imported?.resources.httpRequests[0]).toEqual( + expect.objectContaining({ + bodyType: "application/xml", + headers: expect.arrayContaining([ + { enabled: true, name: "Content-Type", value: "application/xml" }, + ]), + }), + ); + }); + + test("Imports Swagger 2 basic auth and cookie API keys", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + swagger: "2.0", + info: { title: "Auth Test", version: "1.0.0" }, + host: "example.com", + paths: { + "/a": { get: { security: [{ basicAuth: [] }], responses: {} } }, + "/b": { get: { security: [{ cookieKey: [] }], responses: {} } }, + }, + securityDefinitions: { + basicAuth: { type: "basic" }, + cookieKey: { type: "apiKey", in: "cookie", name: "session" }, + }, + }), + ); + + expect(imported?.resources.httpRequests[0]).toEqual( + expect.objectContaining({ + authenticationType: "basic", + authentication: { username: "", password: "" }, + }), + ); + // The auth plugin has no cookie location, so it becomes the Cookie header + expect(imported?.resources.httpRequests[1]).toEqual( + expect.objectContaining({ + authenticationType: "apikey", + authentication: { location: "header", key: "Cookie", value: "session=" }, + }), + ); + }); + + test("Reports references that point outside the document", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.0.0", + info: { title: "External Ref Test", version: "1.0.0" }, + paths: { + "/a": { + post: { + requestBody: { + content: { + "application/json": { schema: { $ref: "./shared.yaml#/components/schemas/Foo" } }, + }, + }, + responses: {}, + }, + }, + "/b": { get: { responses: {} } }, + }, + }), + ); + + expect(imported?.resources.httpRequests[0]?.description).toContain( + "./shared.yaml#/components/schemas/Foo", + ); + // The report is per-operation, so an unrelated request stays clean + expect(imported?.resources.httpRequests[1]?.description).toBeUndefined(); + }); + for (const fixture of fixtures) { test(`Imports ${fixture}`, async () => { const contents = fs.readFileSync(path.join(p, fixture), "utf-8"); @@ -46,7 +574,15 @@ describe("importer-openapi", () => { }), ]); expect(imported?.resources.httpRequests.length).toBe(19); - expect(imported?.resources.folders.length).toBe(7); + expect(imported?.resources.folders.map((f) => f.name)).toEqual(["pet", "store", "user"]); + }); + } + + for (const fixture of realWorldFixtures) { + test(`Snapshots real-world fixture ${fixture}`, async () => { + const contents = fs.readFileSync(path.join(realWorldFixturesPath, fixture), "utf-8"); + const imported = await convertOpenApi(contents); + expect(imported).toMatchSnapshot(); }); } });