diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a036fc15..22991625 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,14 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Install deps and run tests - run: npm cit + - name: Install deps + run: npm ci + + - name: Run tests + run: npm test + + - name: Run benchmarks + run: npm run bench lint: runs-on: ubuntu-latest diff --git a/package-lock.json b/package-lock.json index c8375012..6a0f84ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "oas", "workspaces": [ "./packages/*" ], @@ -13897,9 +13898,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -20621,7 +20622,6 @@ "jsonpath-plus": "^10.0.0", "jsonpointer": "^5.0.0", "memoizee": "^0.4.16", - "oas-normalize": "file:../oas-normalize", "openapi-types": "^12.1.1", "path-to-regexp": "^8.1.0", "remove-undefined-objects": "^5.0.0" diff --git a/package.json b/package.json index 0df6e734..779d7e8f 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "scripts": { "alex": "alex .", "attw": "npx lerna run attw --stream", + "bench": "vitest bench", "build": "npx lerna run build --stream", "clean": "npx lerna clean", "lint": "npm run lint:types && npm run lint:js && npm run prettier", diff --git a/packages/oas-normalize/README.md b/packages/oas-normalize/README.md index 30c33b0a..6b5f7678 100644 --- a/packages/oas-normalize/README.md +++ b/packages/oas-normalize/README.md @@ -5,7 +5,7 @@

- Tooling for converting, validating, and parsing OpenAPI, Swagger, and Postman API definitions + Tooling for converting, validating, and parsing OpenAPI, Swagger, and Postman API definitions.

@@ -27,126 +27,154 @@ npm install oas-normalize ## Usage -```javascript +```ts import OASNormalize from 'oas-normalize'; -// const { default: OASNormalize } = require('oas-normalize'); // If you're using CJS. const oas = new OASNormalize( 'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/petstore-expanded.yaml', - // ...or a string, path, JSON blob, whatever you've got. + // ...or a JSON object, YAML, a file path, stringified JSON, whatever you have. ); -oas +await oas .validate() - .then(definition => { - // Definition will always be JSON, and valid. - console.log(definition); + .then(() => { + // The API definition is valid! }) .catch(err => { - console.log(err); + console.error(err); }); ``` -### `#bundle()` +> [!WARNING] +> Support for Postman collections is experimental. If you've supplied a Postman collection to the library, it will **always** be converted to OpenAPI, using [`@readme/postman-to-openapi`](https://npm.im/@readme/postman-to-openapi) before doing any bundling, validating, etc. -> **Note** -> -> Because Postman collections don't support `$ref` pointers, this method will automatically upconvert a Postman collection to OpenAPI if supplied one. +### `.load()` + +Load and retrive the API definition that `oas-normalize` was initialized with. Every method of `oas-normalize` utilizes this internally however if you would like to retrieve the original API _definition_ supplied (for example if all you had was a URL, a file path, or a buffer), you can use `.load()` to automatically resolve and return its contents. + +```ts +const file = await oas.load(); +console.log(file); +``` + +### `.bundle()` Bundle up the given API definition, resolving any external `$ref` pointers in the process. -```js -await oas.bundle().then(definition => { - console.log(definition); -}); +```ts +const definition = await oas.bundle(); +console.log(definition); ``` -### `#deref()` +### `.convert()` -> **Note** -> -> Because Postman collections don't support `$ref` pointers, this method will automatically upconvert a Postman collection to OpenAPI if supplied one. +Convert a given API definition into an OpenAPI definition JSON object. + +```ts +await oas + .convert() + .then(definition => { + // Definition will always be an OpenAPI JSON object, regardless if a + // Swagger definition, Postman collection, or even YAML was supplied. + console.log(definition); + }) + .catch(err => { + console.error(err); + }); +``` + +### `.deref()` Dereference the given API definition, resolving all `$ref` pointers in the process. -```js -await oas.deref().then(definition => { - console.log(definition); -}); +```ts +const definition = await oas.bundle(); +console.log(definition); ``` -### `#validate({ convertToLatest?: boolean })` +### `.validate()` -Validate and optionally convert to OpenAPI, a given API definition. This supports Swagger 2.0, OpenAPI 3.x API definitions as well as Postman 2.x collections. +Validate a given API definition. This supports Swagger 2.0 and OpenAPI 3.x API definitions, as well as Postman 2.x collections. -Please note that if you've supplied a Postman collection to the library it will **always** be converted to OpenAPI, using [@readme/postman-to-openapi](https://npm.im/@readme/postman-to-openapi), and we will only validate resulting OpenAPI definition. - -```js -await oas.validate().then(definition => { - console.log(definition); -}); +```ts +try { + await oas.validate(); + // The API definition is valid! +} catch (err) { + console.error(err); +} ``` -#### Options +#### Error Handling - -| Option | Type | Description | -| :--- | :--- | :--- | -| `convertToLatest` | Boolean | By default `#validate` will not upconvert Swagger API definitions to OpenAPI so if you wish for this to happen, supply `true`. | - +All thrown validation error messages that direct the user to the line(s) where their errors are present: -#### Error Handling +``` +OpenAPI schema validation failed. -For validation errors, when available, you'll get back an object: - -```js -{ - "details": [ - // Ajv pathing errors. For example: - /* { - "instancePath": "/components/securitySchemes/tlsAuth", - "schemaPath": "#/properties/securitySchemes/patternProperties/%5E%5Ba-zA-Z0-9%5C.%5C-_%5D%2B%24/oneOf", - "keyword": "oneOf", - "params": { "passingSchemas": null }, - "message": "must match exactly one schema in oneOf" - }, */ - ] -} +REQUIRED must have required property 'url' + + 7 | }, + 8 | "servers": [ +> 9 | { + | ^ ☹️ url is missing here! + 10 | "urll": "http://petstore.swagger.io/v2" + 11 | } + 12 | ], ``` -`message` is almost always there, but `path` is less dependable. +However if you would like to programatically access this information the `SyntaxError` error that is thrown contains a `details` array of [AJV](https://npm.im/ajv) errors: + +```json +[ + { + "instancePath": "/servers/0", + "schemaPath": "#/required", + "keyword": "required", + "params": { "missingProperty": "url" }, + "message": "must have required property 'url'", + }, + { + "instancePath": "/servers/0", + "schemaPath": "#/additionalProperties", + "keyword": "additionalProperties", + "params": { "additionalProperty": "urll" }, + "message": "must NOT have additional properties", + }, +]; +``` -### `#version()` +### `.version()` Load and retrieve version information about a supplied API definition. -```js -await oas.version().then(({ specification, version }) => { - console.log(specification); // openapi - console.log(version); // 3.1.0 -}); +```ts +const { specification, version } = await oas.version(); + +console.log(specification); // openapi +console.log(version); // 3.1.0 ``` ### Options ##### Enable local paths -For security reasons, you need to opt into allowing fetching by a local path. To enable it supply the `enablePaths` option to the class instance: +For security reasons, you need to opt into allowing fetching by a local path. To enable this supply the `enablePaths` option to the class instance: -```js +```ts const oas = new OASNormalize('./petstore.json', { enablePaths: true }); ``` ##### Colorized errors -If you wish errors from `.validate()` to be styled and colorized, supply `colorizeErrors: true` to your instance of `OASNormalize`: +If you wish errors from `.validate()` to be styled and colorized, supply `colorizeErrors: true` to the class instance: -```js +```ts const oas = new OASNormalize('https://example.com/petstore.json', { colorizeErrors: true, }); ``` -Error messages will look like such: +When enabled thrown validation error messages will now resemble the following: diff --git a/packages/oas-normalize/package.json b/packages/oas-normalize/package.json index f939daf8..88403456 100644 --- a/packages/oas-normalize/package.json +++ b/packages/oas-normalize/package.json @@ -53,6 +53,7 @@ }, "scripts": { "attw": "attw --pack --format table-flipped", + "bench": "echo 'Please run benchmarks from the root!' && exit 1", "build": "tsup", "lint": "npm run lint:types && npm run lint:js", "lint:js": "eslint . --ext .js,.ts --ignore-path ../../.gitignore", diff --git a/packages/oas-normalize/src/index.ts b/packages/oas-normalize/src/index.ts index df958a92..b95ba34d 100644 --- a/packages/oas-normalize/src/index.ts +++ b/packages/oas-normalize/src/index.ts @@ -12,6 +12,7 @@ import * as utils from './lib/utils.js'; export default class OASNormalize { cache: { bundle?: OpenAPI.Document | false; + convert?: OpenAPI.Document | false; deref?: OpenAPI.Document | false; load?: Record | false; }; @@ -40,15 +41,16 @@ export default class OASNormalize { } /** - * @private + * Load and return the API definition that `oas-normalize` was initialized with. + * */ async load(): Promise> { - if (this.cache.load) return Promise.resolve(this.cache.load); + if (this.cache.load) return this.cache.load; const resolve = (obj: Parameters[0]) => { const ret = utils.stringToJSON(obj); this.cache.load = ret; - return Promise.resolve(ret); + return ret; }; switch (this.type) { @@ -67,24 +69,21 @@ export default class OASNormalize { case 'path': // Load a local file if (!this.opts.enablePaths) { - return Promise.reject(new Error('Use `opts.enablePaths` to enable accessing local files.')); + throw new Error('Use `opts.enablePaths` to enable accessing local files.'); } const contents = fs.readFileSync(this.file).toString(); if (!contents.trim()) { - return Promise.reject(new Error('No file contents found.')); + throw new Error('No file contents found.'); } return resolve(contents); default: - return Promise.reject(new Error('Could not load this file.')); + throw new Error('Could not load this file.'); } } - /** - * @private - */ - static async convertPostmanToOpenAPI(schema: any) { + private static async convertPostmanToOpenAPI(schema: any) { return postmanToOpenAPI(JSON.stringify(schema), undefined, { outputFormat: 'json', replaceVars: true }).then( JSON.parse, ); @@ -95,7 +94,7 @@ export default class OASNormalize { * */ async bundle() { - if (this.cache.bundle) return Promise.resolve(this.cache.bundle); + if (this.cache.bundle) return this.cache.bundle; return this.load() .then(schema => { @@ -120,7 +119,7 @@ export default class OASNormalize { * */ async deref() { - if (this.cache.deref) return Promise.resolve(this.cache.deref); + if (this.cache.deref) return this.cache.deref; return this.load() .then(schema => { @@ -141,19 +140,47 @@ export default class OASNormalize { } /** - * Validate, and potentially convert to OpenAPI, a given API definition. + * Convert a given API definition to OpenAPI if it is not already. + * + */ + async convert(): Promise { + if (this.cache.convert) return this.cache.convert; + + return this.load() + .then(async schema => { + // If we have a Postman collection we need to convert it to OpenAPI. + return utils.isPostman(schema) ? OASNormalize.convertPostmanToOpenAPI(schema) : schema; + }) + .then(async schema => { + if (!utils.isSwagger(schema) && !utils.isOpenAPI(schema)) { + throw new Error('The supplied API definition is unsupported.'); + } else if (utils.isOpenAPI(schema)) { + return schema; + } + + const baseVersion = parseInt(schema.swagger, 10); + if (baseVersion === 1) { + throw new Error('Swagger v1.2 is unsupported.'); + } + + return converter + .convertObj(schema, { anchors: true }) + .then((options: { openapi: OpenAPI.Document }) => options.openapi); + }); + } + + /** + * Validate a given API definition. + * + * If supplied a Postman collection it will be converted to OpenAPI first and then run through + * standard OpenAPI validation. * */ async validate( opts: { - /** - * Automatically convert the supplied API definition to the latest version of OpenAPI. - */ - convertToLatest?: boolean; parser?: openapiParser.Options; - } = { convertToLatest: false }, - ): Promise { - const convertToLatest = opts.convertToLatest; + } = {}, + ): Promise { const parserOptions = opts.parser || {}; if (!parserOptions.validate) { parserOptions.validate = {}; @@ -163,44 +190,34 @@ export default class OASNormalize { return this.load() .then(async schema => { - if (!utils.isPostman(schema)) { - return schema; - } - - return OASNormalize.convertPostmanToOpenAPI(schema); + // Because we don't have something akin to `openapi-parser` for Postman collections we just + // always convert them to OpenAPI. + return utils.isPostman(schema) ? OASNormalize.convertPostmanToOpenAPI(schema) : schema; }) .then(async schema => { if (!utils.isSwagger(schema) && !utils.isOpenAPI(schema)) { - return Promise.reject(new Error('The supplied API definition is unsupported.')); + throw new Error('The supplied API definition is unsupported.'); } else if (utils.isSwagger(schema)) { const baseVersion = parseInt(schema.swagger, 10); if (baseVersion === 1) { - return Promise.reject(new Error('Swagger v1.2 is unsupported.')); + throw new Error('Swagger v1.2 is unsupported.'); } } /** - * `openapiParser.validate()` dereferences schemas at the same time as validation and does - * not give us an option to disable this. Since all we already have a dereferencing method - * on this library and our `validate()` method here just needs to tell us if the definition - * is valid or not we need to clone it before passing it over to `openapi-parser` so as to - * not run into pass-by-reference problems. + * `openapiParser.validate()` dereferences schemas at the same time as validation, mutating + * the supplied parameter in the process, and does not give us an option to disable this. + * As we already have a dereferencing method on this library, and this method just needs to + * tell us if the API definition is valid or not, we need to clone the schema before + * supplying it to `openapi-parser`. */ // eslint-disable-next-line try-catch-failsafe/json-parse const clonedSchema = JSON.parse(JSON.stringify(schema)); - return openapiParser - .validate(clonedSchema, parserOptions) - .then(() => { - if (!convertToLatest || utils.isOpenAPI(schema)) { - return schema; - } - - return converter - .convertObj(schema, { anchors: true }) - .then((options: { openapi: OpenAPI.Document }) => options.openapi); - }) - .catch(err => Promise.reject(err)); + return openapiParser.validate(clonedSchema, parserOptions).then(() => { + // The API definition, whatever its format or specification, is valid. + return true; + }); }); } diff --git a/packages/oas-normalize/test/__fixtures__/postman/petstore.collection.yaml b/packages/oas-normalize/test/__fixtures__/postman/petstore.collection.yaml new file mode 100644 index 00000000..05bf5a90 --- /dev/null +++ b/packages/oas-normalize/test/__fixtures__/postman/petstore.collection.yaml @@ -0,0 +1,1793 @@ +info: + _postman_id: 0b2e8577-2899-4229-bb1c-4cb031108c2f + name: Swagger Petstore + description: |- + This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. + + Contact Support: + Email: apiteam@swagger.io + schema: https://schema.getpostman.com/json/collection/v2.1.0/collection.json +item: + - name: pet + item: + - name: '{pet Id}' + item: + - name: Find pet by ID + request: + auth: + type: apikey + apikey: + - key: key + value: api_key + type: string + - key: value + value: '' + type: string + - key: in + value: header + type: string + method: GET + header: + - key: Accept + value: application/xml + url: + raw: '{{baseUrl}}/pet/:petId' + host: + - '{{baseUrl}}' + path: + - pet + - ':petId' + variable: + - key: petId + value: '-53776022' + description: '(Required) ID of pet to return' + description: Returns a single pet + response: + - name: successful operation + originalRequest: + method: GET + header: + - description: 'Added as a part of security scheme: apikey' + key: api_key + value: '' + url: + raw: '{{baseUrl}}/pet/:petId' + host: + - '{{baseUrl}}' + path: + - pet + - ':petId' + variable: + - key: petId + value: '-53776022' + description: '(Required) ID of pet to return' + status: OK + code: 200 + _postman_previewlanguage: json + header: + - key: Content-Type + value: application/json + cookie: [] + body: |- + { + "name": "doggie", + "photoUrls": [ + "https://example.com/photo.png", + "https://example.com/photo.png" + ], + "id": 25, + "category": { + "id": -66648423, + "name": "sint proident voluptate nostrud" + }, + "tags": [ + { + "id": -36713801, + "name": "cupidatat laboris" + }, + { + "id": 88956444, + "name": "sed" + } + ], + "status": "available" + } + - name: Invalid ID supplied + originalRequest: + method: GET + header: + - description: 'Added as a part of security scheme: apikey' + key: api_key + value: '' + url: + raw: '{{baseUrl}}/pet/:petId' + host: + - '{{baseUrl}}' + path: + - pet + - ':petId' + variable: + - key: petId + value: '-53776022' + description: '(Required) ID of pet to return' + status: Bad Request + code: 400 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Pet not found + originalRequest: + method: GET + header: + - description: 'Added as a part of security scheme: apikey' + key: api_key + value: '' + url: + raw: '{{baseUrl}}/pet/:petId' + host: + - '{{baseUrl}}' + path: + - pet + - ':petId' + variable: + - key: petId + value: '-53776022' + description: '(Required) ID of pet to return' + status: Not Found + code: 404 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: successful response + originalRequest: + method: GET + header: + - description: 'Added as a part of security scheme: apikey' + key: api_key + value: '' + url: + raw: '{{baseUrl}}/pet/:petId' + host: + - '{{baseUrl}}' + path: + - pet + - ':petId' + variable: + - key: petId + value: '-53776022' + description: '(Required) ID of pet to return' + status: Internal Server Error + code: 500 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Updates a pet in the store with form data + request: + auth: + type: oauth2 + oauth2: + - key: scope + value: write:pets read:pets + type: string + - key: authUrl + value: http://petstore.swagger.io/oauth/dialog + type: string + - key: grant_type + value: implicit + type: string + method: POST + header: + - key: Content-Type + value: application/x-www-form-urlencoded + body: + mode: urlencoded + urlencoded: + - key: name + value: dolore magna + description: Updated name of the pet + - key: status + value: et incididunt + description: Updated status of the pet + url: + raw: '{{baseUrl}}/pet/:petId' + host: + - '{{baseUrl}}' + path: + - pet + - ':petId' + variable: + - key: petId + value: '-53776022' + description: '(Required) ID of pet that needs to be updated' + response: + - name: Invalid input + originalRequest: + method: POST + header: + - description: 'Added as a part of security scheme: oauth2' + key: Authorization + value: '' + body: + mode: urlencoded + urlencoded: + - key: name + value: dolore magna + description: Updated name of the pet + - key: status + value: et incididunt + description: Updated status of the pet + url: + raw: '{{baseUrl}}/pet/:petId' + host: + - '{{baseUrl}}' + path: + - pet + - ':petId' + variable: + - key: petId + value: '-53776022' + description: '(Required) ID of pet that needs to be updated' + status: Method Not Allowed + code: 405 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Deletes a pet + request: + auth: + type: oauth2 + oauth2: + - key: scope + value: write:pets read:pets + type: string + - key: authUrl + value: http://petstore.swagger.io/oauth/dialog + type: string + - key: grant_type + value: implicit + type: string + method: DELETE + header: + - key: api_key + value: dolore est + url: + raw: '{{baseUrl}}/pet/:petId' + host: + - '{{baseUrl}}' + path: + - pet + - ':petId' + variable: + - key: petId + value: '-53776022' + description: '(Required) Pet id to delete' + response: + - name: Invalid ID supplied + originalRequest: + method: DELETE + header: + - description: 'Added as a part of security scheme: oauth2' + key: Authorization + value: '' + - key: api_key + value: dolore est + url: + raw: '{{baseUrl}}/pet/:petId' + host: + - '{{baseUrl}}' + path: + - pet + - ':petId' + variable: + - key: petId + value: '-53776022' + description: '(Required) Pet id to delete' + status: Bad Request + code: 400 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Pet not found + originalRequest: + method: DELETE + header: + - description: 'Added as a part of security scheme: oauth2' + key: Authorization + value: '' + - key: api_key + value: dolore est + url: + raw: '{{baseUrl}}/pet/:petId' + host: + - '{{baseUrl}}' + path: + - pet + - ':petId' + variable: + - key: petId + value: '-53776022' + description: '(Required) Pet id to delete' + status: Not Found + code: 404 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Uploads an image + request: + auth: + type: oauth2 + oauth2: + - key: scope + value: write:pets read:pets + type: string + - key: authUrl + value: http://petstore.swagger.io/oauth/dialog + type: string + - key: grant_type + value: implicit + type: string + method: POST + header: + - key: Content-Type + value: multipart/form-data + - key: Accept + value: application/json + body: + mode: formdata + formdata: + - key: additionalMetadata + value: voluptate cillum + description: Additional data to pass to server + type: text + - key: file + description: file to upload + type: file + src: [] + url: + raw: '{{baseUrl}}/pet/:petId/uploadImage' + host: + - '{{baseUrl}}' + path: + - pet + - ':petId' + - uploadImage + variable: + - key: petId + value: '-53776022' + description: '(Required) ID of pet to update' + response: + - name: successful operation + originalRequest: + method: POST + header: + - description: 'Added as a part of security scheme: oauth2' + key: Authorization + value: '' + body: + mode: formdata + formdata: + - key: additionalMetadata + value: voluptate cillum + description: Additional data to pass to server + type: text + - key: file + description: file to upload + type: file + src: [] + url: + raw: '{{baseUrl}}/pet/:petId/uploadImage' + host: + - '{{baseUrl}}' + path: + - pet + - ':petId' + - uploadImage + variable: + - key: petId + value: '-53776022' + description: '(Required) ID of pet to update' + status: OK + code: 200 + _postman_previewlanguage: json + header: + - key: Content-Type + value: application/json + cookie: [] + body: |- + { + "code": -15164975, + "type": "Excepteur", + "message": "ut Lorem dolor officia incididunt" + } + - name: Add a new pet to the store + request: + auth: + type: oauth2 + oauth2: + - key: scope + value: write:pets read:pets + type: string + - key: authUrl + value: http://petstore.swagger.io/oauth/dialog + type: string + - key: grant_type + value: implicit + type: string + method: POST + header: + - key: Content-Type + value: application/json + body: + mode: raw + raw: |- + { + "name": "doggie", + "photoUrls": [ + "https://example.com/photo.png", + "https://example.com/photo.png" + ], + "category": { + "id": -98206889, + "name": "ut" + }, + "tags": [ + { + "id": -66696067, + "name": "laboris dolore pariatur qui" + }, + { + "id": -22260355, + "name": "qui reprehenderit aliqua sint velit" + } + ], + "status": "available" + } + options: + raw: + language: json + url: + raw: '{{baseUrl}}/pet' + host: + - '{{baseUrl}}' + path: + - pet + response: + - name: Invalid input + originalRequest: + method: POST + header: + - description: 'Added as a part of security scheme: oauth2' + key: Authorization + value: '' + url: + raw: '{{baseUrl}}/pet' + host: + - '{{baseUrl}}' + path: + - pet + status: Method Not Allowed + code: 405 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Update an existing pet + request: + auth: + type: oauth2 + oauth2: + - key: scope + value: write:pets read:pets + type: string + - key: authUrl + value: http://petstore.swagger.io/oauth/dialog + type: string + - key: grant_type + value: implicit + type: string + method: PUT + header: + - key: Content-Type + value: application/json + body: + mode: raw + raw: |- + { + "name": "doggie", + "photoUrls": [ + "https://example.com/photo.png", + "https://example.com/photo.png" + ], + "category": { + "id": 45108667, + "name": "elit" + }, + "tags": [ + { + "id": -90871361, + "name": "tempor ullamco" + }, + { + "id": 41541088, + "name": "commodo irure in dolor fugiat" + } + ], + "status": "sold" + } + options: + raw: + language: json + url: + raw: '{{baseUrl}}/pet' + host: + - '{{baseUrl}}' + path: + - pet + response: + - name: Invalid ID supplied + originalRequest: + method: PUT + header: + - description: 'Added as a part of security scheme: oauth2' + key: Authorization + value: '' + url: + raw: '{{baseUrl}}/pet' + host: + - '{{baseUrl}}' + path: + - pet + status: Bad Request + code: 400 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Pet not found + originalRequest: + method: PUT + header: + - description: 'Added as a part of security scheme: oauth2' + key: Authorization + value: '' + url: + raw: '{{baseUrl}}/pet' + host: + - '{{baseUrl}}' + path: + - pet + status: Not Found + code: 404 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Validation exception + originalRequest: + method: PUT + header: + - description: 'Added as a part of security scheme: oauth2' + key: Authorization + value: '' + url: + raw: '{{baseUrl}}/pet' + host: + - '{{baseUrl}}' + path: + - pet + status: Method Not Allowed + code: 405 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Finds Pets by status + request: + auth: + type: oauth2 + oauth2: + - key: scope + value: write:pets read:pets + type: string + - key: authUrl + value: http://petstore.swagger.io/oauth/dialog + type: string + - key: grant_type + value: implicit + type: string + method: GET + header: + - key: Accept + value: application/xml + url: + raw: '{{baseUrl}}/pet/findByStatus?status=available&status=available' + host: + - '{{baseUrl}}' + path: + - pet + - findByStatus + query: + - key: status + value: available + description: '(Required) Status values that need to be considered for filter' + - key: status + value: available + description: '(Required) Status values that need to be considered for filter' + description: Multiple status values can be provided with comma separated strings + response: + - name: successful operation + originalRequest: + method: GET + header: + - description: 'Added as a part of security scheme: oauth2' + key: Authorization + value: '' + url: + raw: '{{baseUrl}}/pet/findByStatus?status=available&status=available' + host: + - '{{baseUrl}}' + path: + - pet + - findByStatus + query: + - key: status + value: available + - key: status + value: available + status: OK + code: 200 + _postman_previewlanguage: json + header: + - key: Content-Type + value: application/json + cookie: [] + body: |- + [ + { + "name": "doggie", + "photoUrls": [ + "https://example.com/photo.png", + "https://example.com/photo.png" + ], + "id": 25, + "category": { + "id": -10731015, + "name": "consectetur proident" + }, + "tags": [ + { + "id": -87509155, + "name": "dolor nulla Ut" + }, + { + "id": -19458248, + "name": "elit" + } + ], + "status": "pending" + }, + { + "name": "doggie", + "photoUrls": [ + "https://example.com/photo.png", + "https://example.com/photo.png" + ], + "id": 25, + "category": { + "id": 38511362, + "name": "a" + }, + "tags": [ + { + "id": 26246913, + "name": "pariatur" + }, + { + "id": 65442833, + "name": "dolor irure consectetur" + } + ], + "status": "pending" + } + ] + - name: Invalid status value + originalRequest: + method: GET + header: + - description: 'Added as a part of security scheme: oauth2' + key: Authorization + value: '' + url: + raw: '{{baseUrl}}/pet/findByStatus?status=available&status=available' + host: + - '{{baseUrl}}' + path: + - pet + - findByStatus + query: + - key: status + value: available + - key: status + value: available + status: Bad Request + code: 400 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Finds Pets by tags + request: + auth: + type: oauth2 + oauth2: + - key: scope + value: write:pets read:pets + type: string + - key: authUrl + value: http://petstore.swagger.io/oauth/dialog + type: string + - key: grant_type + value: implicit + type: string + method: GET + header: + - key: Accept + value: application/xml + url: + raw: '{{baseUrl}}/pet/findByTags?tags=enim nostrud consequat&tags=culpa quis + dolor laboris' + host: + - '{{baseUrl}}' + path: + - pet + - findByTags + query: + - key: tags + value: enim nostrud consequat + description: '(Required) Tags to filter by' + - key: tags + value: culpa quis dolor laboris + description: '(Required) Tags to filter by' + description: Muliple tags can be provided with comma separated strings. Use + tag1, tag2, tag3 for testing. + response: + - name: successful operation + originalRequest: + method: GET + header: + - description: 'Added as a part of security scheme: oauth2' + key: Authorization + value: '' + url: + raw: '{{baseUrl}}/pet/findByTags?tags=sed occaecat anim veniam ex&tags=anim + reprehenderit magna' + host: + - '{{baseUrl}}' + path: + - pet + - findByTags + query: + - key: tags + value: sed occaecat anim veniam ex + - key: tags + value: anim reprehenderit magna + status: OK + code: 200 + _postman_previewlanguage: json + header: + - key: Content-Type + value: application/json + cookie: [] + body: |- + [ + { + "name": "doggie", + "photoUrls": [ + "https://example.com/photo.png", + "https://example.com/photo.png" + ], + "id": 25, + "category": { + "id": -10731015, + "name": "consectetur proident" + }, + "tags": [ + { + "id": -87509155, + "name": "dolor nulla Ut" + }, + { + "id": -19458248, + "name": "elit" + } + ], + "status": "pending" + }, + { + "name": "doggie", + "photoUrls": [ + "https://example.com/photo.png", + "https://example.com/photo.png" + ], + "id": 25, + "category": { + "id": 38511362, + "name": "a" + }, + "tags": [ + { + "id": 26246913, + "name": "pariatur" + }, + { + "id": 65442833, + "name": "dolor irure consectetur" + } + ], + "status": "pending" + } + ] + - name: Invalid tag value + originalRequest: + method: GET + header: + - description: 'Added as a part of security scheme: oauth2' + key: Authorization + value: '' + url: + raw: '{{baseUrl}}/pet/findByTags?tags=sed occaecat anim veniam ex&tags=anim + reprehenderit magna' + host: + - '{{baseUrl}}' + path: + - pet + - findByTags + query: + - key: tags + value: sed occaecat anim veniam ex + - key: tags + value: anim reprehenderit magna + status: Bad Request + code: 400 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: store + item: + - name: order + item: + - name: '{order Id}' + item: + - name: Find purchase order by ID + request: + method: GET + header: + - key: Accept + value: application/xml + url: + raw: '{{baseUrl}}/store/order/:orderId' + host: + - '{{baseUrl}}' + path: + - store + - order + - ':orderId' + variable: + - key: orderId + value: '7' + description: '(Required) ID of pet that needs to be fetched' + description: For valid response try integer IDs with value >= 1 and <= 10. + Other values will generated exceptions + response: + - name: successful operation + originalRequest: + method: GET + header: [] + url: + raw: '{{baseUrl}}/store/order/:orderId' + host: + - '{{baseUrl}}' + path: + - store + - order + - ':orderId' + variable: + - key: orderId + value: '7' + description: '(Required) ID of pet that needs to be fetched' + status: OK + code: 200 + _postman_previewlanguage: json + header: + - key: Content-Type + value: application/json + cookie: [] + body: |- + { + "id": -15329310, + "petId": -50620843, + "quantity": 78361352, + "shipDate": "2018-08-08T02:36:45.934Z", + "status": "delivered", + "complete": false + } + - name: Invalid ID supplied + originalRequest: + method: GET + header: [] + url: + raw: '{{baseUrl}}/store/order/:orderId' + host: + - '{{baseUrl}}' + path: + - store + - order + - ':orderId' + variable: + - key: orderId + value: '7' + description: '(Required) ID of pet that needs to be fetched' + status: Bad Request + code: 400 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Order not found + originalRequest: + method: GET + header: [] + url: + raw: '{{baseUrl}}/store/order/:orderId' + host: + - '{{baseUrl}}' + path: + - store + - order + - ':orderId' + variable: + - key: orderId + value: '7' + description: '(Required) ID of pet that needs to be fetched' + status: Not Found + code: 404 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Delete purchase order by ID + request: + method: DELETE + header: [] + url: + raw: '{{baseUrl}}/store/order/:orderId' + host: + - '{{baseUrl}}' + path: + - store + - order + - ':orderId' + variable: + - key: orderId + value: '32526146' + description: '(Required) ID of the order that needs to be deleted' + description: For valid response try integer IDs with positive integer value. + Negative or non-integer values will generate API errors + response: + - name: Invalid ID supplied + originalRequest: + method: DELETE + header: [] + url: + raw: '{{baseUrl}}/store/order/:orderId' + host: + - '{{baseUrl}}' + path: + - store + - order + - ':orderId' + variable: + - key: orderId + value: '32526146' + description: '(Required) ID of the order that needs to be deleted' + status: Bad Request + code: 400 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Order not found + originalRequest: + method: DELETE + header: [] + url: + raw: '{{baseUrl}}/store/order/:orderId' + host: + - '{{baseUrl}}' + path: + - store + - order + - ':orderId' + variable: + - key: orderId + value: '32526146' + description: '(Required) ID of the order that needs to be deleted' + status: Not Found + code: 404 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Place an order for a pet + request: + method: POST + header: + - key: Content-Type + value: application/json + - key: Accept + value: application/xml + body: + mode: raw + raw: |- + { + "id": -15329310, + "petId": -50620843, + "quantity": 78361352, + "shipDate": "2018-08-08T02:36:45.934Z", + "status": "delivered", + "complete": false + } + options: + raw: + language: json + url: + raw: '{{baseUrl}}/store/order' + host: + - '{{baseUrl}}' + path: + - store + - order + response: + - name: successful operation + originalRequest: + method: POST + header: [] + body: + mode: raw + raw: |- + { + "id": -15329310, + "petId": -50620843, + "quantity": 78361352, + "shipDate": "2018-08-08T02:36:45.934Z", + "status": "delivered", + "complete": false + } + options: + raw: + language: json + url: + raw: '{{baseUrl}}/store/order' + host: + - '{{baseUrl}}' + path: + - store + - order + status: OK + code: 200 + _postman_previewlanguage: json + header: + - key: Content-Type + value: application/json + cookie: [] + body: |- + { + "id": -15329310, + "petId": -50620843, + "quantity": 78361352, + "shipDate": "2018-08-08T02:36:45.934Z", + "status": "delivered", + "complete": false + } + - name: Invalid Order + originalRequest: + method: POST + header: [] + body: + mode: raw + raw: |- + { + "id": -15329310, + "petId": -50620843, + "quantity": 78361352, + "shipDate": "2018-08-08T02:36:45.934Z", + "status": "delivered", + "complete": false + } + options: + raw: + language: json + url: + raw: '{{baseUrl}}/store/order' + host: + - '{{baseUrl}}' + path: + - store + - order + status: Bad Request + code: 400 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Returns pet inventories by status + request: + auth: + type: apikey + apikey: + - key: key + value: api_key + type: string + - key: value + value: '' + type: string + - key: in + value: header + type: string + method: GET + header: + - key: Accept + value: application/json + url: + raw: '{{baseUrl}}/store/inventory' + host: + - '{{baseUrl}}' + path: + - store + - inventory + description: Returns a map of status codes to quantities + response: + - name: successful operation + originalRequest: + method: GET + header: + - description: 'Added as a part of security scheme: apikey' + key: api_key + value: '' + url: + raw: '{{baseUrl}}/store/inventory' + host: + - '{{baseUrl}}' + path: + - store + - inventory + status: OK + code: 200 + _postman_previewlanguage: json + header: + - key: Content-Type + value: application/json + cookie: [] + body: |- + { + "dolor_4": -59651882, + "dolor__10": 76938793 + } + - name: user + item: + - name: '{username}' + item: + - name: Get user by user name + request: + method: GET + header: + - key: Accept + value: application/xml + url: + raw: '{{baseUrl}}/user/:username' + host: + - '{{baseUrl}}' + path: + - user + - ':username' + variable: + - key: username + value: dolore est + description: '(Required) The name that needs to be fetched. Use user1 + for testing. ' + response: + - name: successful operation + originalRequest: + method: GET + header: [] + url: + raw: '{{baseUrl}}/user/:username' + host: + - '{{baseUrl}}' + path: + - user + - ':username' + variable: + - key: username + value: dolore est + description: '(Required) The name that needs to be fetched. Use user1 + for testing. ' + status: OK + code: 200 + _postman_previewlanguage: json + header: + - key: Content-Type + value: application/json + cookie: [] + body: |- + { + "id": -70409270, + "username": "do ea nisi", + "firstName": "occaecat dolor in dolore", + "lastName": "laborum sit ullamco", + "email": "nostrud Ut", + "password": "officia occaecat enim l", + "phone": "in elit officia sint", + "userStatus": 86291576 + } + - name: Invalid username supplied + originalRequest: + method: GET + header: [] + url: + raw: '{{baseUrl}}/user/:username' + host: + - '{{baseUrl}}' + path: + - user + - ':username' + variable: + - key: username + value: dolore est + description: '(Required) The name that needs to be fetched. Use user1 + for testing. ' + status: Bad Request + code: 400 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: User not found + originalRequest: + method: GET + header: [] + url: + raw: '{{baseUrl}}/user/:username' + host: + - '{{baseUrl}}' + path: + - user + - ':username' + variable: + - key: username + value: dolore est + description: '(Required) The name that needs to be fetched. Use user1 + for testing. ' + status: Not Found + code: 404 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Updated user + request: + method: PUT + header: + - key: Content-Type + value: application/json + body: + mode: raw + raw: |- + { + "id": -70409270, + "username": "do ea nisi", + "firstName": "occaecat dolor in dolore", + "lastName": "laborum sit ullamco", + "email": "nostrud Ut", + "password": "officia occaecat enim l", + "phone": "in elit officia sint", + "userStatus": 86291576 + } + options: + raw: + language: json + url: + raw: '{{baseUrl}}/user/:username' + host: + - '{{baseUrl}}' + path: + - user + - ':username' + variable: + - key: username + value: dolore est + description: '(Required) name that need to be updated' + description: This can only be done by the logged in user. + response: + - name: Invalid user supplied + originalRequest: + method: PUT + header: [] + body: + mode: raw + raw: |- + { + "id": -70409270, + "username": "do ea nisi", + "firstName": "occaecat dolor in dolore", + "lastName": "laborum sit ullamco", + "email": "nostrud Ut", + "password": "officia occaecat enim l", + "phone": "in elit officia sint", + "userStatus": 86291576 + } + options: + raw: + language: json + url: + raw: '{{baseUrl}}/user/:username' + host: + - '{{baseUrl}}' + path: + - user + - ':username' + variable: + - key: username + value: dolore est + description: '(Required) name that need to be updated' + status: Bad Request + code: 400 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: User not found + originalRequest: + method: PUT + header: [] + body: + mode: raw + raw: |- + { + "id": -70409270, + "username": "do ea nisi", + "firstName": "occaecat dolor in dolore", + "lastName": "laborum sit ullamco", + "email": "nostrud Ut", + "password": "officia occaecat enim l", + "phone": "in elit officia sint", + "userStatus": 86291576 + } + options: + raw: + language: json + url: + raw: '{{baseUrl}}/user/:username' + host: + - '{{baseUrl}}' + path: + - user + - ':username' + variable: + - key: username + value: dolore est + description: '(Required) name that need to be updated' + status: Not Found + code: 404 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Delete user + request: + method: DELETE + header: [] + url: + raw: '{{baseUrl}}/user/:username' + host: + - '{{baseUrl}}' + path: + - user + - ':username' + variable: + - key: username + value: dolore est + description: '(Required) The name that needs to be deleted' + description: This can only be done by the logged in user. + response: + - name: Invalid username supplied + originalRequest: + method: DELETE + header: [] + url: + raw: '{{baseUrl}}/user/:username' + host: + - '{{baseUrl}}' + path: + - user + - ':username' + variable: + - key: username + value: dolore est + description: '(Required) The name that needs to be deleted' + status: Bad Request + code: 400 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: User not found + originalRequest: + method: DELETE + header: [] + url: + raw: '{{baseUrl}}/user/:username' + host: + - '{{baseUrl}}' + path: + - user + - ':username' + variable: + - key: username + value: dolore est + description: '(Required) The name that needs to be deleted' + status: Not Found + code: 404 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Create user + request: + method: POST + header: + - key: Content-Type + value: application/json + body: + mode: raw + raw: |- + { + "id": -70409270, + "username": "do ea nisi", + "firstName": "occaecat dolor in dolore", + "lastName": "laborum sit ullamco", + "email": "nostrud Ut", + "password": "officia occaecat enim l", + "phone": "in elit officia sint", + "userStatus": 86291576 + } + options: + raw: + language: json + url: + raw: '{{baseUrl}}/user' + host: + - '{{baseUrl}}' + path: + - user + description: This can only be done by the logged in user. + response: + - name: successful operation + originalRequest: + method: POST + header: [] + body: + mode: raw + raw: |- + { + "id": -70409270, + "username": "do ea nisi", + "firstName": "occaecat dolor in dolore", + "lastName": "laborum sit ullamco", + "email": "nostrud Ut", + "password": "officia occaecat enim l", + "phone": "in elit officia sint", + "userStatus": 86291576 + } + options: + raw: + language: json + url: + raw: '{{baseUrl}}/user' + host: + - '{{baseUrl}}' + path: + - user + status: Internal Server Error + code: 500 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Creates list of users with given input array + request: + method: POST + header: + - key: Content-Type + value: application/json + body: + mode: raw + raw: |- + [ + { + "id": -95496632, + "username": "reprehenderit Duis", + "firstName": "ipsum enim", + "lastName": "sit incididunt quis su", + "email": "esse in nostrud", + "password": "dolore", + "phone": "ea tempor sed eiusmod", + "userStatus": 23456761 + }, + { + "id": -17610454, + "username": "occaecat in laboris voluptate", + "firstName": "consectetur fugiat", + "lastName": "est consectet", + "email": "ea qui", + "password": "dolor aliquip", + "phone": "nulla eu veniam", + "userStatus": -56036685 + } + ] + options: + raw: + language: json + url: + raw: '{{baseUrl}}/user/createWithArray' + host: + - '{{baseUrl}}' + path: + - user + - createWithArray + response: + - name: successful operation + originalRequest: + method: POST + header: [] + url: + raw: '{{baseUrl}}/user/createWithArray' + host: + - '{{baseUrl}}' + path: + - user + - createWithArray + status: Internal Server Error + code: 500 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Creates list of users with given input array + request: + method: POST + header: + - key: Content-Type + value: application/json + body: + mode: raw + raw: |- + [ + { + "id": -17453134, + "username": "est veniam proident", + "firstName": "adipisic", + "lastName": "ea nisi ut anim", + "email": "culpa voluptate laborum ut", + "password": "veniam id enim voluptate", + "phone": "enim sed", + "userStatus": 31329378 + }, + { + "id": -97425969, + "username": "velit sint ea ad reprehend", + "firstName": "reprehenderit do aliquip", + "lastName": "anim mollit", + "email": "minim qui laborum in", + "password": "exercitation quis", + "phone": "irure", + "userStatus": -62781043 + } + ] + options: + raw: + language: json + url: + raw: '{{baseUrl}}/user/createWithList' + host: + - '{{baseUrl}}' + path: + - user + - createWithList + response: + - name: successful operation + originalRequest: + method: POST + header: [] + url: + raw: '{{baseUrl}}/user/createWithList' + host: + - '{{baseUrl}}' + path: + - user + - createWithList + status: Internal Server Error + code: 500 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Logs user into the system + request: + method: GET + header: + - key: Accept + value: application/xml + url: + raw: '{{baseUrl}}/user/login?username=dolore est&password=dolore est' + host: + - '{{baseUrl}}' + path: + - user + - login + query: + - key: username + value: dolore est + description: '(Required) The user name for login' + - key: password + value: dolore est + description: '(Required) The password for login in clear text' + response: + - name: successful operation + originalRequest: + method: GET + header: [] + url: + raw: '{{baseUrl}}/user/login?username=dolore est&password=dolore est' + host: + - '{{baseUrl}}' + path: + - user + - login + query: + - key: username + value: dolore est + - key: password + value: dolore est + status: OK + code: 200 + _postman_previewlanguage: json + header: + - key: X-Rate-Limit + value: '-71676539' + description: calls per hour allowed by the user + - key: X-Expires-After + value: '1966-01-28T06:32:02.771Z' + description: date in UTC when token expires + - key: Content-Type + value: application/json + cookie: [] + body: '"dolore est"' + - name: Invalid username/password supplied + originalRequest: + method: GET + header: [] + url: + raw: '{{baseUrl}}/user/login?username=dolore est&password=dolore est' + host: + - '{{baseUrl}}' + path: + - user + - login + query: + - key: username + value: dolore est + - key: password + value: dolore est + status: Bad Request + code: 400 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' + - name: Logs out current logged in user session + request: + method: GET + header: [] + url: + raw: '{{baseUrl}}/user/logout' + host: + - '{{baseUrl}}' + path: + - user + - logout + response: + - name: successful operation + originalRequest: + method: GET + header: [] + url: + raw: '{{baseUrl}}/user/logout' + host: + - '{{baseUrl}}' + path: + - user + - logout + status: Internal Server Error + code: 500 + _postman_previewlanguage: text + header: + - key: Content-Type + value: text/plain + cookie: [] + body: '' +variable: + - key: baseUrl + value: http://petstore.swagger.io/v2 + type: string diff --git a/packages/oas-normalize/test/__snapshots__/index.test.ts.snap b/packages/oas-normalize/test/__snapshots__/index.test.ts.snap index 8075a6d6..2e2e3dc1 100644 --- a/packages/oas-normalize/test/__snapshots__/index.test.ts.snap +++ b/packages/oas-normalize/test/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`#validate > OpenAPI 3.0 support > should validate a JSON path as expected 1`] = ` +exports[`#convert > OpenAPI 3.0 support > should validate a JSON path as expected 1`] = ` { "components": { "requestBodies": { @@ -1072,7 +1072,7 @@ exports[`#validate > OpenAPI 3.0 support > should validate a JSON path as expect } `; -exports[`#validate > OpenAPI 3.0 support > should validate a URL hosting JSON as expected 1`] = ` +exports[`#convert > OpenAPI 3.0 support > should validate a URL hosting JSON as expected 1`] = ` { "components": { "requestBodies": { @@ -2144,7 +2144,7 @@ exports[`#validate > OpenAPI 3.0 support > should validate a URL hosting JSON as } `; -exports[`#validate > OpenAPI 3.0 support > should validate a URL hosting YAML as expected 1`] = ` +exports[`#convert > OpenAPI 3.0 support > should validate a URL hosting YAML as expected 1`] = ` { "components": { "requestBodies": { @@ -3216,7 +3216,7 @@ exports[`#validate > OpenAPI 3.0 support > should validate a URL hosting YAML as } `; -exports[`#validate > OpenAPI 3.0 support > should validate a YAML path as expected 1`] = ` +exports[`#convert > OpenAPI 3.0 support > should validate a YAML path as expected 1`] = ` { "components": { "requestBodies": { @@ -4288,7 +4288,7 @@ exports[`#validate > OpenAPI 3.0 support > should validate a YAML path as expect } `; -exports[`#validate > OpenAPI 3.1 support > should validate a JSON path as expected 1`] = ` +exports[`#convert > OpenAPI 3.1 support > should validate a JSON path as expected 1`] = ` { "components": { "requestBodies": { @@ -5355,7 +5355,7 @@ exports[`#validate > OpenAPI 3.1 support > should validate a JSON path as expect } `; -exports[`#validate > OpenAPI 3.1 support > should validate a URL hosting JSON as expected 1`] = ` +exports[`#convert > OpenAPI 3.1 support > should validate a URL hosting JSON as expected 1`] = ` { "components": { "requestBodies": { @@ -6422,7 +6422,7 @@ exports[`#validate > OpenAPI 3.1 support > should validate a URL hosting JSON as } `; -exports[`#validate > OpenAPI 3.1 support > should validate a URL hosting YAML as expected 1`] = ` +exports[`#convert > OpenAPI 3.1 support > should validate a URL hosting YAML as expected 1`] = ` { "components": { "requestBodies": { @@ -7489,7 +7489,7 @@ exports[`#validate > OpenAPI 3.1 support > should validate a URL hosting YAML as } `; -exports[`#validate > OpenAPI 3.1 support > should validate a YAML path as expected 1`] = ` +exports[`#convert > OpenAPI 3.1 support > should validate a YAML path as expected 1`] = ` { "components": { "requestBodies": { @@ -8556,7 +8556,7 @@ exports[`#validate > OpenAPI 3.1 support > should validate a YAML path as expect } `; -exports[`#validate > Postman support > should support converting a Postman collection to OpenAPI (validating it in the process) 1`] = ` +exports[`#convert > Postman support > should support converting a Postman collection to OpenAPI (validating it in the process) 1`] = ` { "components": { "securitySchemes": { @@ -10252,7 +10252,7 @@ Contact Support: } `; -exports[`#validate > Swagger 2.0 support > should validate a JSON path as expected 1`] = ` +exports[`#convert > Swagger 2.0 support > should validate a JSON path as expected 1`] = ` { "components": { "requestBodies": { @@ -11317,7 +11317,7 @@ exports[`#validate > Swagger 2.0 support > should validate a JSON path as expect } `; -exports[`#validate > Swagger 2.0 support > should validate a URL hosting JSON as expected 1`] = ` +exports[`#convert > Swagger 2.0 support > should validate a URL hosting JSON as expected 1`] = ` { "components": { "requestBodies": { @@ -12382,7 +12382,7 @@ exports[`#validate > Swagger 2.0 support > should validate a URL hosting JSON as } `; -exports[`#validate > Swagger 2.0 support > should validate a URL hosting YAML as expected 1`] = ` +exports[`#convert > Swagger 2.0 support > should validate a URL hosting YAML as expected 1`] = ` { "components": { "requestBodies": { @@ -13447,7 +13447,7 @@ exports[`#validate > Swagger 2.0 support > should validate a URL hosting YAML as } `; -exports[`#validate > Swagger 2.0 support > should validate a YAML path as expected 1`] = ` +exports[`#convert > Swagger 2.0 support > should validate a YAML path as expected 1`] = ` { "components": { "requestBodies": { @@ -14513,7 +14513,7 @@ exports[`#validate > Swagger 2.0 support > should validate a YAML path as expect `; exports[`#validate > should error out, and show all errors, when a definition has lots of problems 1`] = ` -[SyntaxError: OpenAPI schema validation failed. +"OpenAPI schema validation failed. REQUIRED must have required property 'url' @@ -14533,5 +14533,38 @@ ADDITIONAL PROPERTY must NOT have additional properties | ^^^^^^^ 😲 tagss is not expected to be here! 27 | "pet" 28 | ], - 29 | "summary": "Finds Pets by status",] + 29 | "summary": "Finds Pets by status"," +`; + +exports[`#validate > should error out, and show all errors, when a definition has lots of problems 2`] = ` +[ + { + "instancePath": "/servers/0", + "keyword": "required", + "message": "must have required property 'url'", + "params": { + "missingProperty": "url", + }, + "schemaPath": "#/required", + }, + { + "instancePath": "/servers/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "urll", + }, + "schemaPath": "#/additionalProperties", + }, + { + "instancePath": "/paths/~1pet~1findByStatus/get", + "isIdentifierLocation": true, + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tagss", + }, + "schemaPath": "#/additionalProperties", + }, +] `; diff --git a/packages/oas-normalize/test/benchmarks/convert.bench.ts b/packages/oas-normalize/test/benchmarks/convert.bench.ts new file mode 100644 index 00000000..82f77039 --- /dev/null +++ b/packages/oas-normalize/test/benchmarks/convert.bench.ts @@ -0,0 +1,60 @@ +/* eslint-disable unicorn/prefer-module */ +/* eslint-disable vitest/consistent-test-it */ +import fs from 'node:fs/promises'; + +import swaggerJSON from '@readme/oas-examples/2.0/json/petstore.json'; +import petstore30JSON from '@readme/oas-examples/3.0/json/petstore.json'; +import petstore31JSON from '@readme/oas-examples/3.1/json/petstore.json'; +import { bench, describe } from 'vitest'; + +import OASNormalize from '../../src/index.js'; +import postmanJSON from '../__fixtures__/postman/petstore.collection.json'; + +describe('JSON', () => { + bench('OpenAPI 3.1', async () => { + const normalized = new OASNormalize(JSON.stringify(petstore30JSON)); + await normalized.convert(); + }); + + bench('OpenAPI 3.0', async () => { + const normalized = new OASNormalize(JSON.stringify(petstore31JSON)); + await normalized.convert(); + }); + + bench('Swagger 2.0', async () => { + const normalized = new OASNormalize(JSON.stringify(swaggerJSON)); + await normalized.convert(); + }); + + bench('Postman', async () => { + const normalized = new OASNormalize(JSON.stringify(postmanJSON)); + await normalized.convert(); + }); +}); + +describe('YAML', async () => { + const swaggerYAML = await fs.readFile(require.resolve('@readme/oas-examples/2.0/yaml/petstore.yaml'), 'utf8'); + const petstore30YAML = await fs.readFile(require.resolve('@readme/oas-examples/3.0/yaml/petstore.yaml'), 'utf8'); + const petstore31YAML = await fs.readFile(require.resolve('@readme/oas-examples/3.1/yaml/petstore.yaml'), 'utf8'); + const postmanYAML = await fs.readFile(require.resolve('../__fixtures__/postman/petstore.collection.yaml'), 'utf8'); + + bench('OpenAPI 3.1', async () => { + const normalized = new OASNormalize(petstore31YAML); + await normalized.convert(); + }); + + bench('OpenAPI 3.0', async () => { + const normalized = new OASNormalize(petstore30YAML); + await normalized.convert(); + }); + + bench('Swagger 2.0', async () => { + const normalized = new OASNormalize(swaggerYAML); + await normalized.convert(); + }); + + bench('Postman', async () => { + const normalized = new OASNormalize(postmanYAML); + await normalized.convert(); + }); +}); diff --git a/packages/oas-normalize/test/benchmarks/validate.bench.ts b/packages/oas-normalize/test/benchmarks/validate.bench.ts new file mode 100644 index 00000000..7c9423d7 --- /dev/null +++ b/packages/oas-normalize/test/benchmarks/validate.bench.ts @@ -0,0 +1,60 @@ +/* eslint-disable unicorn/prefer-module */ +/* eslint-disable vitest/consistent-test-it */ +import fs from 'node:fs/promises'; + +import swaggerJSON from '@readme/oas-examples/2.0/json/petstore.json'; +import petstore30JSON from '@readme/oas-examples/3.0/json/petstore.json'; +import petstore31JSON from '@readme/oas-examples/3.1/json/petstore.json'; +import { bench, describe } from 'vitest'; + +import OASNormalize from '../../src/index.js'; +import postmanJSON from '../__fixtures__/postman/petstore.collection.json'; + +describe('JSON', () => { + bench('OpenAPI 3.1', async () => { + const normalized = new OASNormalize(JSON.stringify(petstore30JSON)); + await normalized.validate(); + }); + + bench('OpenAPI 3.0', async () => { + const normalized = new OASNormalize(JSON.stringify(petstore31JSON)); + await normalized.validate(); + }); + + bench('Swagger 2.0', async () => { + const normalized = new OASNormalize(JSON.stringify(swaggerJSON)); + await normalized.validate(); + }); + + bench('Postman', async () => { + const normalized = new OASNormalize(JSON.stringify(postmanJSON)); + await normalized.validate(); + }); +}); + +describe('YAML', async () => { + const swaggerYAML = await fs.readFile(require.resolve('@readme/oas-examples/2.0/yaml/petstore.yaml'), 'utf8'); + const petstore30YAML = await fs.readFile(require.resolve('@readme/oas-examples/3.0/yaml/petstore.yaml'), 'utf8'); + const petstore31YAML = await fs.readFile(require.resolve('@readme/oas-examples/3.1/yaml/petstore.yaml'), 'utf8'); + const postmanYAML = await fs.readFile(require.resolve('../__fixtures__/postman/petstore.collection.yaml'), 'utf8'); + + bench('OpenAPI 3.1', async () => { + const normalized = new OASNormalize(petstore31YAML); + await normalized.validate(); + }); + + bench('OpenAPI 3.0', async () => { + const normalized = new OASNormalize(petstore30YAML); + await normalized.validate(); + }); + + bench('Swagger 2.0', async () => { + const normalized = new OASNormalize(swaggerYAML); + await normalized.validate(); + }); + + bench('Postman', async () => { + const normalized = new OASNormalize(postmanYAML); + await normalized.validate(); + }); +}); diff --git a/packages/oas-normalize/test/index.test.ts b/packages/oas-normalize/test/index.test.ts index c9da9c2d..2ad11ec7 100644 --- a/packages/oas-normalize/test/index.test.ts +++ b/packages/oas-normalize/test/index.test.ts @@ -5,7 +5,7 @@ import fs from 'node:fs'; import path from 'node:path'; import nock from 'nock'; -import { describe, afterEach, beforeAll, beforeEach, it, expect } from 'vitest'; +import { describe, afterEach, beforeAll, beforeEach, it, expect, assert } from 'vitest'; import OASNormalize from '../src/index.js'; import { getAPIDefinitionType, isAPIDefinition, isOpenAPI, isPostman, isSwagger } from '../src/lib/utils.js'; @@ -174,6 +174,67 @@ describe('#bundle', () => { }); }); +describe('#convert', () => { + describe.each([ + ['Swagger 2.0', '2.0'], + ['OpenAPI 3.0', '3.0'], + ['OpenAPI 3.1', '3.1'], + ])('%s support', (_, version) => { + it.runIf(version === '3.1')( + 'should not attempt to upconvert an OpenAPI definition if we dont need to', + async () => { + const webhooks = await import('@readme/oas-examples/3.1/json/webhooks.json').then(r => r.default); + const o = new OASNormalize(structuredClone(webhooks)); + + await expect(o.convert()).resolves.toStrictEqual(webhooks); + }, + ); + + it('should validate a URL hosting JSON as expected', async () => { + const json = await import(`@readme/oas-examples/${version}/json/petstore.json`).then(r => r.default); + + nock('http://example.com').get(`/api-${version}.json`).reply(200, structuredClone(json)); + const o = new OASNormalize(`http://example.com/api-${version}.json`); + + await expect(o.convert()).resolves.toMatchSnapshot(); + }); + + it('should validate a JSON path as expected', async () => { + const o = new OASNormalize(require.resolve(`@readme/oas-examples/${version}/json/petstore.json`), { + enablePaths: true, + }); + + await expect(o.convert()).resolves.toMatchSnapshot(); + }); + + it('should validate a URL hosting YAML as expected', async () => { + const yaml = fs.readFileSync(require.resolve(`@readme/oas-examples/${version}/yaml/petstore.yaml`), 'utf8'); + nock('http://example.com').get(`/api-${version}.yaml`).reply(200, yaml); + const o = new OASNormalize(`http://example.com/api-${version}.yaml`); + + await expect(o.convert()).resolves.toMatchSnapshot(); + }); + + it('should validate a YAML path as expected', async () => { + const o = new OASNormalize(require.resolve(`@readme/oas-examples/${version}/yaml/petstore.yaml`), { + enablePaths: true, + }); + + await expect(o.convert()).resolves.toMatchSnapshot(); + }); + }); + + describe('Postman support', () => { + it('should support converting a Postman collection to OpenAPI (validating it in the process)', async () => { + const o = new OASNormalize(require.resolve('./__fixtures__/postman/petstore.collection.json'), { + enablePaths: true, + }); + + await expect(o.convert()).resolves.toMatchSnapshot(); + }); + }); +}); + describe('#deref', () => { it('should dereference a definition', async () => { const openapi = await import('@readme/oas-examples/3.0/json/petstore.json').then(r => r.default); @@ -222,14 +283,14 @@ describe('#validate', () => { const swagger = await import('@readme/oas-examples/2.0/json/petstore.json').then(r => r.default); const o = new OASNormalize(structuredClone(swagger)); - await expect(o.validate()).resolves.toStrictEqual(swagger); + await expect(o.validate()).resolves.toBe(true); }); it('should not attempt to upconvert an OpenAPI definition if we dont need to', async () => { const webhooks = await import('@readme/oas-examples/3.1/json/webhooks.json').then(r => r.default); const o = new OASNormalize(structuredClone(webhooks)); - await expect(o.validate({ convertToLatest: true })).resolves.toStrictEqual(webhooks); + await expect(o.validate()).resolves.toBe(true); }); it('should error out on a definition a missing component', async () => { @@ -271,13 +332,22 @@ describe('#validate', () => { ); }); + /* eslint-disable vitest/no-conditional-expect */ it('should error out, and show all errors, when a definition has lots of problems', async () => { const o = new OASNormalize(require.resolve('./__fixtures__/invalid/openapi-very-invalid.json'), { enablePaths: true, }); - await expect(o.validate()).rejects.toMatchSnapshot(); + try { + await o.validate(); + assert.fail(); + } catch (err) { + expect(err).toBeInstanceOf(SyntaxError); + expect(err.message).toMatchSnapshot(); + expect(err.details).toMatchSnapshot(); + } }); + /* eslint-enable vitest/no-conditional-expect */ it('should error out for empty file', async () => { const o = new OASNormalize(require.resolve('./__fixtures__/invalid/empty.json'), { @@ -310,7 +380,7 @@ describe('#validate', () => { nock('http://example.com').get(`/api-${version}.json`).reply(200, structuredClone(json)); const o = new OASNormalize(`http://example.com/api-${version}.json`); - await expect(o.validate({ convertToLatest: true })).resolves.toMatchSnapshot(); + await expect(o.validate()).resolves.toBe(true); }); it('should validate a JSON path as expected', async () => { @@ -318,7 +388,7 @@ describe('#validate', () => { enablePaths: true, }); - await expect(o.validate({ convertToLatest: true })).resolves.toMatchSnapshot(); + await expect(o.validate()).resolves.toBe(true); }); it('should validate a URL hosting YAML as expected', async () => { @@ -326,7 +396,7 @@ describe('#validate', () => { nock('http://example.com').get(`/api-${version}.yaml`).reply(200, yaml); const o = new OASNormalize(`http://example.com/api-${version}.yaml`); - await expect(o.validate({ convertToLatest: true })).resolves.toMatchSnapshot(); + await expect(o.validate()).resolves.toBe(true); }); it('should validate a YAML path as expected', async () => { @@ -334,7 +404,7 @@ describe('#validate', () => { enablePaths: true, }); - await expect(o.validate({ convertToLatest: true })).resolves.toMatchSnapshot(); + await expect(o.validate()).resolves.toBe(true); }); }); @@ -344,7 +414,7 @@ describe('#validate', () => { enablePaths: true, }); - await expect(o.validate({ convertToLatest: true })).resolves.toMatchSnapshot(); + await expect(o.validate()).resolves.toBe(true); }); }); }); diff --git a/packages/oas/package.json b/packages/oas/package.json index 7f31f499..a8d068b6 100644 --- a/packages/oas/package.json +++ b/packages/oas/package.json @@ -94,7 +94,6 @@ "jsonpath-plus": "^10.0.0", "jsonpointer": "^5.0.0", "memoizee": "^0.4.16", - "oas-normalize": "file:../oas-normalize", "openapi-types": "^12.1.1", "path-to-regexp": "^8.1.0", "remove-undefined-objects": "^5.0.0" diff --git a/packages/oas/src/reducer/index.ts b/packages/oas/src/reducer/index.ts index 5cec4cfa..0d80c430 100644 --- a/packages/oas/src/reducer/index.ts +++ b/packages/oas/src/reducer/index.ts @@ -1,7 +1,6 @@ import type { ComponentsObject, HttpMethods, OASDocument, TagObject } from '../types.js'; import jsonPointer from 'jsonpointer'; -import { getAPIDefinitionType } from 'oas-normalize/lib/utils'; import { query } from '../analyzer/util.js'; @@ -81,7 +80,7 @@ export default function reducer(definition: OASDocument, opts: ReducerOptions = const $refs: Set = new Set(); const usedTags: Set = new Set(); - if (getAPIDefinitionType(definition) !== 'openapi') { + if (!definition.openapi) { throw new Error('Sorry, only OpenAPI definitions are supported.'); }