From 9201593f25df95effb5ff56f48e30297c81f1e09 Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Mon, 12 Feb 2024 15:44:54 +0400 Subject: [PATCH] feat(oas): introduce support for examples vendor extensions in Swagger converter closes #224 --- packages/oas/src/converter/Sampler.ts | 22 ++- .../oas/src/converter/VendorExtensions.ts | 4 + packages/oas/src/index.ts | 17 +- packages/oas/tests/DefaultConverter.spec.ts | 146 +++++++++++++++--- ...body.api-connect-or-smartbear.swagger.yaml | 66 ++++++++ ...params.x-example.body.redocly.swagger.yaml | 68 ++++++++ ...s.x-example.body.schemathesis.swagger.yaml | 67 ++++++++ ...ody.swagger.include-examples-is-false.json | 48 ++++++ ...body.swagger.include-examples-is-true.json | 48 ++++++ ...ata.swagger.include-examples-is-false.json | 25 +++ ...data.swagger.include-examples-is-true.json | 25 +++ .../params.x-example.form-data.swagger.yaml | 23 +++ ...der.swagger.include-examples-is-false.json | 21 +++ ...ader.swagger.include-examples-is-true.json | 21 +++ .../params.x-example.header.swagger.yaml | 24 +++ .../fixtures/params.x-example.oas.result.json | 68 ++++++++ .../tests/fixtures/params.x-example.oas.yaml | 89 +++++++++++ ...ath.swagger.include-examples-is-false.json | 17 ++ ...path.swagger.include-examples-is-true.json | 17 ++ .../params.x-example.path.swagger.yaml | 24 +++ ...ery.swagger.include-examples-is-false.json | 22 +++ ...uery.swagger.include-examples-is-true.json | 22 +++ .../params.x-example.query.swagger.yaml | 24 +++ .../openapi-sampler/src/VendorExtensions.ts | 4 + packages/openapi-sampler/src/index.ts | 12 +- .../src/traverse/DefaultTraverse.ts | 79 +++++++++- .../openapi-sampler/src/traverse/Traverse.ts | 1 + .../openapi-sampler/tests/example.spec.ts | 63 ++++++++ 28 files changed, 1034 insertions(+), 33 deletions(-) create mode 100644 packages/oas/src/converter/VendorExtensions.ts create mode 100644 packages/oas/tests/fixtures/params.x-example.body.api-connect-or-smartbear.swagger.yaml create mode 100644 packages/oas/tests/fixtures/params.x-example.body.redocly.swagger.yaml create mode 100644 packages/oas/tests/fixtures/params.x-example.body.schemathesis.swagger.yaml create mode 100644 packages/oas/tests/fixtures/params.x-example.body.swagger.include-examples-is-false.json create mode 100644 packages/oas/tests/fixtures/params.x-example.body.swagger.include-examples-is-true.json create mode 100644 packages/oas/tests/fixtures/params.x-example.form-data.swagger.include-examples-is-false.json create mode 100644 packages/oas/tests/fixtures/params.x-example.form-data.swagger.include-examples-is-true.json create mode 100644 packages/oas/tests/fixtures/params.x-example.form-data.swagger.yaml create mode 100644 packages/oas/tests/fixtures/params.x-example.header.swagger.include-examples-is-false.json create mode 100644 packages/oas/tests/fixtures/params.x-example.header.swagger.include-examples-is-true.json create mode 100644 packages/oas/tests/fixtures/params.x-example.header.swagger.yaml create mode 100644 packages/oas/tests/fixtures/params.x-example.oas.result.json create mode 100644 packages/oas/tests/fixtures/params.x-example.oas.yaml create mode 100644 packages/oas/tests/fixtures/params.x-example.path.swagger.include-examples-is-false.json create mode 100644 packages/oas/tests/fixtures/params.x-example.path.swagger.include-examples-is-true.json create mode 100644 packages/oas/tests/fixtures/params.x-example.path.swagger.yaml create mode 100644 packages/oas/tests/fixtures/params.x-example.query.swagger.include-examples-is-false.json create mode 100644 packages/oas/tests/fixtures/params.x-example.query.swagger.include-examples-is-true.json create mode 100644 packages/oas/tests/fixtures/params.x-example.query.swagger.yaml create mode 100644 packages/openapi-sampler/src/VendorExtensions.ts diff --git a/packages/oas/src/converter/Sampler.ts b/packages/oas/src/converter/Sampler.ts index 6faa13e7..fbcd789c 100644 --- a/packages/oas/src/converter/Sampler.ts +++ b/packages/oas/src/converter/Sampler.ts @@ -1,9 +1,11 @@ import { ConvertError } from '../errors'; -import { sample, Schema } from '@har-sdk/openapi-sampler'; +import { VendorExtensions } from './VendorExtensions'; +import { Options, sample, Schema } from '@har-sdk/openapi-sampler'; import pointer from 'json-pointer'; import type { OpenAPI } from '@har-sdk/core'; export class Sampler { + constructor(private readonly options: Options) {} public sampleParam( param: OpenAPI.Parameter, context: { @@ -16,6 +18,18 @@ export class Sampler { 'schema' in param ? { ...param.schema, + ...(param[VendorExtensions.X_EXAMPLE] !== undefined + ? { + [VendorExtensions.X_EXAMPLE]: + param[VendorExtensions.X_EXAMPLE] + } + : {}), + ...(param[VendorExtensions.X_EXAMPLES] !== undefined + ? { + [VendorExtensions.X_EXAMPLES]: + param[VendorExtensions.X_EXAMPLES] + } + : {}), ...(param.example !== undefined ? { example: param.example } : {}) } : param, @@ -43,7 +57,11 @@ export class Sampler { } ): any | undefined { try { - return sample(schema, { skipReadOnly: true, quiet: true }, context?.spec); + return sample( + schema, + { ...this.options, skipReadOnly: true, quiet: true }, + context?.spec + ); } catch (e) { throw new ConvertError(e.message, context?.jsonPointer); } diff --git a/packages/oas/src/converter/VendorExtensions.ts b/packages/oas/src/converter/VendorExtensions.ts new file mode 100644 index 00000000..94aa6de2 --- /dev/null +++ b/packages/oas/src/converter/VendorExtensions.ts @@ -0,0 +1,4 @@ +export enum VendorExtensions { + X_EXAMPLE = 'x-example', + X_EXAMPLES = 'x-examples' +} diff --git a/packages/oas/src/index.ts b/packages/oas/src/index.ts index 7a7a0f4d..03dc8823 100644 --- a/packages/oas/src/index.ts +++ b/packages/oas/src/index.ts @@ -6,16 +6,29 @@ import { SubConverterFactory, SubConverterRegistry } from './converter'; +import { isOASV2 } from './utils'; import type { OpenAPI, Request } from '@har-sdk/core'; +import { Options } from '@har-sdk/openapi-sampler'; export * from './errors'; +export { Options }; -export const oas2har = (collection: OpenAPI.Document): Promise => { +export const oas2har = ( + collection: OpenAPI.Document, + options: Options = {} +): Promise => { if (!collection) { throw new TypeError('Please provide a valid OAS specification.'); } - const sampler = new Sampler(); + const { includeVendorExamples } = options; + + options = { + ...options, + includeVendorExamples: isOASV2(collection) ? includeVendorExamples : false + }; + + const sampler = new Sampler(options); const baseUrlParser = new BaseUrlParser(sampler); const subConverterFactory = new SubConverterFactory(sampler); const subConverterRegistry = new SubConverterRegistry(subConverterFactory); diff --git a/packages/oas/tests/DefaultConverter.spec.ts b/packages/oas/tests/DefaultConverter.spec.ts index 7a77d8c4..c093c1b7 100644 --- a/packages/oas/tests/DefaultConverter.spec.ts +++ b/packages/oas/tests/DefaultConverter.spec.ts @@ -1,11 +1,32 @@ import { ConvertError, oas2har } from '../src'; -import yaml, { load } from 'js-yaml'; +import { load } from 'js-yaml'; import { OpenAPIV2, Request } from '@har-sdk/core'; import { resolve } from 'path'; -import { readFile, readFileSync } from 'fs'; +import { readFile } from 'fs'; import { promisify } from 'util'; describe('DefaultConverter', () => { + const readFileAsync = promisify(readFile); + + const loadFile = async (fileName: string) => { + const filePath = resolve(__dirname, fileName); + + const content = await readFileAsync(filePath, 'utf-8'); + + return content.endsWith('.json') ? JSON.parse(content) : load(content); + }; + + const createFixture = async ({ + inputFile, + expectedFile + }: { + inputFile: string; + expectedFile: string; + }) => ({ + inputDoc: await loadFile(inputFile), + expectedDoc: await loadFile(expectedFile) + }); + describe('convert', () => { [ { @@ -183,24 +204,14 @@ describe('DefaultConverter', () => { } ].forEach(({ input: inputFile, expected: expectedFile, message }) => { it(message, async () => { - const content = readFileSync( - resolve(__dirname, `./fixtures/${inputFile}`), - 'utf-8' - ); - const input = inputFile.endsWith('json') - ? JSON.parse(content) - : load(content); - - const expected = JSON.parse( - readFileSync( - resolve(__dirname, `./fixtures/${expectedFile}`), - 'utf-8' - ) - ); + const { inputDoc, expectedDoc } = await createFixture({ + inputFile: `./fixtures/${inputFile}`, + expectedFile: `./fixtures/${expectedFile}` + }); - const result: Request[] = await oas2har(input as any); + const result: Request[] = await oas2har(inputDoc as any); - expect(result).toStrictEqual(expected); + expect(result).toStrictEqual(expectedDoc); }); }); @@ -271,12 +282,9 @@ describe('DefaultConverter', () => { /^convert-error-on-(.+)\.(.+)\.yaml$/, '$1 ($2)' )}`, async () => { - const content: string = await promisify(readFile)( - resolve(__dirname, `./fixtures/${input}`), - 'utf8' - ); + const inputDoc = await loadFile(`./fixtures/${input}`); - const result = oas2har(yaml.load(content) as OpenAPIV2.Document); + const result = oas2har(inputDoc as OpenAPIV2.Document); await expect(result).rejects.toThrow(ConvertError); await expect(result).rejects.toMatchObject({ @@ -284,5 +292,97 @@ describe('DefaultConverter', () => { }); }) ); + + it('should ignore x-example when includeVendorExamples is true (oas)', async () => { + // arrange + const { inputDoc, expectedDoc } = await createFixture({ + inputFile: `./fixtures/params.x-example.oas.yaml`, + expectedFile: `./fixtures/params.x-example.oas.result.json` + }); + + // act + const result: Request[] = await oas2har(inputDoc as any, { + includeVendorExamples: true + }); + + // assert + expect(result).toStrictEqual(expectedDoc); + }); + + it.each(['path', 'query', 'header', 'form-data'])( + 'should ignore %s parameter x-example when includeVendorExamples is false (swagger)', + async (input) => { + // arrange + const { inputDoc, expectedDoc } = await createFixture({ + inputFile: `./fixtures/params.x-example.${input}.swagger.yaml`, + expectedFile: `./fixtures/params.x-example.${input}.swagger.include-examples-is-false.json` + }); + + // act + const result: Request[] = await oas2har(inputDoc as any, { + includeVendorExamples: false + }); + + // assert + expect(result).toStrictEqual(expectedDoc); + } + ); + + it.each(['path', 'query', 'header', 'form-data'])( + 'should prefer %s parameter x-example over default when includeVendorExamples is true (swagger)', + async (input) => { + // arrange + const { inputDoc, expectedDoc } = await createFixture({ + inputFile: `./fixtures/params.x-example.${input}.swagger.yaml`, + expectedFile: `./fixtures/params.x-example.${input}.swagger.include-examples-is-true.json` + }); + + // act + const result: Request[] = await oas2har(inputDoc as any, { + includeVendorExamples: true + }); + + // assert + expect(result).toStrictEqual(expectedDoc); + } + ); + + it.each(['schemathesis', 'redocly', 'api-connect-or-smartbear'])( + 'should use %s body parameter x-example when includeVendorExamples is true (swagger)', + async (input) => { + // arrange + const { inputDoc, expectedDoc } = await createFixture({ + inputFile: `./fixtures/params.x-example.body.${input}.swagger.yaml`, + expectedFile: `./fixtures/params.x-example.body.swagger.include-examples-is-true.json` + }); + + // act + const result: Request[] = await oas2har(inputDoc as any, { + includeVendorExamples: true + }); + + // assert + expect(result).toStrictEqual(expectedDoc); + } + ); + + it.each(['schemathesis', 'redocly', 'api-connect-or-smartbear'])( + 'should not use %s body parameter x-example when includeVendorExamples is false (swagger)', + async (input) => { + // arrange + const { inputDoc, expectedDoc } = await createFixture({ + inputFile: `./fixtures/params.x-example.body.${input}.swagger.yaml`, + expectedFile: `./fixtures/params.x-example.body.swagger.include-examples-is-false.json` + }); + + // act + const result: Request[] = await oas2har(inputDoc as any, { + includeVendorExamples: false + }); + + // assert + expect(result).toStrictEqual(expectedDoc); + } + ); }); }); diff --git a/packages/oas/tests/fixtures/params.x-example.body.api-connect-or-smartbear.swagger.yaml b/packages/oas/tests/fixtures/params.x-example.body.api-connect-or-smartbear.swagger.yaml new file mode 100644 index 00000000..38160cea --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.body.api-connect-or-smartbear.swagger.yaml @@ -0,0 +1,66 @@ +swagger: '2.0' +info: + title: Sample API + version: 1.0.0 +host: example.com +schemes: + - https +paths: + /sample: + post: + consumes: + - application/json + produces: + - application/json + parameters: + - name: payload + in: body + required: true + schema: + type: array + items: + type: object + properties: + name: + type: string + default: 'default_name' + age: + type: integer + default: 10 + x-example: + - name: x_example_name + age: 30 + responses: + '201': + description: '' + /sample/{id}: + put: + consumes: + - application/json + produces: + - application/json + parameters: + - name: id + in: path + required: true + type: integer + default: 0 + x-example: 123 + - name: payload + in: body + required: true + schema: + type: object + properties: + name: + type: string + default: 'default_name' + age: + type: integer + default: 10 + x-example: + name: x_example_name + age: 30 + responses: + '201': + description: '' diff --git a/packages/oas/tests/fixtures/params.x-example.body.redocly.swagger.yaml b/packages/oas/tests/fixtures/params.x-example.body.redocly.swagger.yaml new file mode 100644 index 00000000..8436fa67 --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.body.redocly.swagger.yaml @@ -0,0 +1,68 @@ +swagger: '2.0' +info: + title: Sample API + version: 1.0.0 +host: example.com +schemes: + - https +paths: + /sample: + post: + consumes: + - application/json + produces: + - application/json + parameters: + - name: payload + in: body + required: true + schema: + type: array + items: + type: object + properties: + name: + type: string + default: 'default_name' + age: + type: integer + default: 10 + x-example: + application/json: + - name: x_example_name + age: 30 + responses: + '201': + description: '' + /sample/{id}: + put: + consumes: + - application/json + produces: + - application/json + parameters: + - name: id + in: path + required: true + type: integer + default: 0 + x-example: 123 + - name: payload + in: body + required: true + schema: + type: object + properties: + name: + type: string + default: 'default_name' + age: + type: integer + default: 10 + x-example: + example-name: + name: x_example_name + age: 30 + responses: + '201': + description: '' diff --git a/packages/oas/tests/fixtures/params.x-example.body.schemathesis.swagger.yaml b/packages/oas/tests/fixtures/params.x-example.body.schemathesis.swagger.yaml new file mode 100644 index 00000000..fc77633f --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.body.schemathesis.swagger.yaml @@ -0,0 +1,67 @@ +swagger: '2.0' +info: + title: Sample API + version: 1.0.0 +host: example.com +schemes: + - https +paths: + /sample: + post: + consumes: + - application/json + produces: + - application/json + parameters: + - name: payload + in: body + required: true + schema: + type: array + items: + type: object + properties: + name: + type: string + default: 'default_name' + age: + type: integer + default: 10 + x-example: + - name: x_example_name + age: 30 + responses: + '201': + description: '' + /sample/{id}: + put: + consumes: + - application/json + produces: + - application/json + parameters: + - name: id + in: path + required: true + type: integer + default: 0 + x-example: 123 + - name: payload + in: body + required: true + schema: + type: object + properties: + name: + type: string + default: 'default_name' + age: + type: integer + default: 10 + x-examples: + application/json: + name: x_example_name + age: 30 + responses: + '201': + description: '' diff --git a/packages/oas/tests/fixtures/params.x-example.body.swagger.include-examples-is-false.json b/packages/oas/tests/fixtures/params.x-example.body.swagger.include-examples-is-false.json new file mode 100644 index 00000000..dc2a4f99 --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.body.swagger.include-examples-is-false.json @@ -0,0 +1,48 @@ +[ + { + "queryString": [], + "cookies": [], + "method": "POST", + "headers": [ + { + "value": "application/json", + "name": "content-type" + }, + { + "value": "application/json", + "name": "accept" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "postData": { + "mimeType": "application/json", + "text": "[{\"name\":\"default_name\",\"age\":10}]" + }, + "url": "https://example.com/sample" + }, + { + "queryString": [], + "cookies": [], + "method": "PUT", + "headers": [ + { + "value": "application/json", + "name": "content-type" + }, + { + "value": "application/json", + "name": "accept" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "postData": { + "mimeType": "application/json", + "text": "{\"name\":\"default_name\",\"age\":10}" + }, + "url": "https://example.com/sample/0" + } +] diff --git a/packages/oas/tests/fixtures/params.x-example.body.swagger.include-examples-is-true.json b/packages/oas/tests/fixtures/params.x-example.body.swagger.include-examples-is-true.json new file mode 100644 index 00000000..8ccbaa69 --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.body.swagger.include-examples-is-true.json @@ -0,0 +1,48 @@ +[ + { + "queryString": [], + "cookies": [], + "method": "POST", + "headers": [ + { + "value": "application/json", + "name": "content-type" + }, + { + "value": "application/json", + "name": "accept" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "postData": { + "mimeType": "application/json", + "text": "[{\"name\":\"x_example_name\",\"age\":30}]" + }, + "url": "https://example.com/sample" + }, + { + "queryString": [], + "cookies": [], + "method": "PUT", + "headers": [ + { + "value": "application/json", + "name": "content-type" + }, + { + "value": "application/json", + "name": "accept" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "postData": { + "mimeType": "application/json", + "text": "{\"name\":\"x_example_name\",\"age\":30}" + }, + "url": "https://example.com/sample/123" + } +] diff --git a/packages/oas/tests/fixtures/params.x-example.form-data.swagger.include-examples-is-false.json b/packages/oas/tests/fixtures/params.x-example.form-data.swagger.include-examples-is-false.json new file mode 100644 index 00000000..4bcf62a6 --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.form-data.swagger.include-examples-is-false.json @@ -0,0 +1,25 @@ +[ + { + "queryString": [], + "cookies": [], + "method": "POST", + "headers": [ + { + "value": "application/x-www-form-urlencoded", + "name": "content-type" + }, + { + "value": "application/json", + "name": "accept" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "postData": { + "mimeType": "application/x-www-form-urlencoded", + "text": "name=default_name" + }, + "url": "https://example.com/sample" + } +] diff --git a/packages/oas/tests/fixtures/params.x-example.form-data.swagger.include-examples-is-true.json b/packages/oas/tests/fixtures/params.x-example.form-data.swagger.include-examples-is-true.json new file mode 100644 index 00000000..34597e0d --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.form-data.swagger.include-examples-is-true.json @@ -0,0 +1,25 @@ +[ + { + "queryString": [], + "cookies": [], + "method": "POST", + "headers": [ + { + "value": "application/x-www-form-urlencoded", + "name": "content-type" + }, + { + "value": "application/json", + "name": "accept" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "postData": { + "mimeType": "application/x-www-form-urlencoded", + "text": "name=x_example_name" + }, + "url": "https://example.com/sample" + } +] diff --git a/packages/oas/tests/fixtures/params.x-example.form-data.swagger.yaml b/packages/oas/tests/fixtures/params.x-example.form-data.swagger.yaml new file mode 100644 index 00000000..9680cbe1 --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.form-data.swagger.yaml @@ -0,0 +1,23 @@ +swagger: '2.0' +info: + title: Sample API + version: 1.0.0 +host: example.com +schemes: + - https +paths: + /sample: + post: + consumes: + - application/x-www-form-urlencoded + produces: + - application/json + parameters: + - name: name + in: formData + type: string + default: default_name + x-example: x_example_name + responses: + '201': + description: '' diff --git a/packages/oas/tests/fixtures/params.x-example.header.swagger.include-examples-is-false.json b/packages/oas/tests/fixtures/params.x-example.header.swagger.include-examples-is-false.json new file mode 100644 index 00000000..dd164a74 --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.header.swagger.include-examples-is-false.json @@ -0,0 +1,21 @@ +[ + { + "queryString": [], + "cookies": [], + "method": "GET", + "headers": [ + { + "value": "application/json", + "name": "accept" + }, + { + "value": "Bearer default_jwt_token", + "name": "authorization" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "url": "https://example.com/sample" + } +] diff --git a/packages/oas/tests/fixtures/params.x-example.header.swagger.include-examples-is-true.json b/packages/oas/tests/fixtures/params.x-example.header.swagger.include-examples-is-true.json new file mode 100644 index 00000000..3b9c0956 --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.header.swagger.include-examples-is-true.json @@ -0,0 +1,21 @@ +[ + { + "queryString": [], + "cookies": [], + "method": "GET", + "headers": [ + { + "value": "application/json", + "name": "accept" + }, + { + "value": "Bearer x_example_jwt_token", + "name": "authorization" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "url": "https://example.com/sample" + } +] diff --git a/packages/oas/tests/fixtures/params.x-example.header.swagger.yaml b/packages/oas/tests/fixtures/params.x-example.header.swagger.yaml new file mode 100644 index 00000000..41d3899b --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.header.swagger.yaml @@ -0,0 +1,24 @@ +swagger: '2.0' +info: + title: Sample API + version: 1.0.0 +host: example.com +schemes: + - https +paths: + /sample: + get: + consumes: + - application/json + produces: + - application/json + parameters: + - name: Authorization + in: header + required: true + type: string + default: 'Bearer default_jwt_token' + x-example: 'Bearer x_example_jwt_token' + responses: + '201': + description: '' diff --git a/packages/oas/tests/fixtures/params.x-example.oas.result.json b/packages/oas/tests/fixtures/params.x-example.oas.result.json new file mode 100644 index 00000000..a1c80e23 --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.oas.result.json @@ -0,0 +1,68 @@ +[ + { + "queryString": [ + { + "name": "filter", + "value": "default_filter" + } + ], + "cookies": [], + "method": "PUT", + "headers": [ + { + "value": "application/x-www-form-urlencoded", + "name": "content-type" + }, + { + "value": "Bearer default_jwt_token", + "name": "authorization" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "postData": { + "mimeType": "application/x-www-form-urlencoded", + "text": "name=default_name" + }, + "url": "https://example.com/sample/0?filter=default_filter" + }, + { + "queryString": [], + "cookies": [], + "method": "POST", + "headers": [ + { + "value": "application/json", + "name": "content-type" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "postData": { + "mimeType": "application/json", + "text": "{\"name\":\"default_name\",\"age\":42}" + }, + "url": "https://example.com/sample" + }, + { + "queryString": [], + "cookies": [], + "method": "PATCH", + "headers": [ + { + "value": "application/json", + "name": "content-type" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "postData": { + "mimeType": "application/json", + "text": "{\"name\":\"example_name\",\"age\":20}" + }, + "url": "https://example.com/sample" + } +] diff --git a/packages/oas/tests/fixtures/params.x-example.oas.yaml b/packages/oas/tests/fixtures/params.x-example.oas.yaml new file mode 100644 index 00000000..7718ea6e --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.oas.yaml @@ -0,0 +1,89 @@ +openapi: 3.0.1 +info: + title: Sample API + version: 1.0.0 +servers: + - url: https://example.com/ +paths: + /sample/{id}: + put: + parameters: + - name: id + in: path + required: true + schema: + type: integer + default: 0 + x-example: 123 + - name: filter + in: query + schema: + type: string + default: default_filter + x-example: x_example_filter + - name: Authorization + in: header + required: true + schema: + type: string + default: Bearer default_jwt_token + x-example: Bearer x_example_jwt_token + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + type: string + x-example: x_example_name + default: default_name + responses: + '201': + description: '' + content: {} + /sample: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + default: default_name + age: + type: integer + x-example: + name: x_example_name + age: 30 + required: true + responses: + '201': + description: '' + content: {} + x-codegen-request-body-name: payload + patch: + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + default: default_name + age: + type: integer + example: + name: example_name + age: 20 + x-example: + name: x_example_name + age: 30 + required: true + responses: + '201': + description: '' + content: {} diff --git a/packages/oas/tests/fixtures/params.x-example.path.swagger.include-examples-is-false.json b/packages/oas/tests/fixtures/params.x-example.path.swagger.include-examples-is-false.json new file mode 100644 index 00000000..04efd4e0 --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.path.swagger.include-examples-is-false.json @@ -0,0 +1,17 @@ +[ + { + "queryString": [], + "cookies": [], + "method": "GET", + "headers": [ + { + "value": "application/json", + "name": "accept" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "url": "https://example.com/sample/0" + } +] diff --git a/packages/oas/tests/fixtures/params.x-example.path.swagger.include-examples-is-true.json b/packages/oas/tests/fixtures/params.x-example.path.swagger.include-examples-is-true.json new file mode 100644 index 00000000..8b0e7754 --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.path.swagger.include-examples-is-true.json @@ -0,0 +1,17 @@ +[ + { + "queryString": [], + "cookies": [], + "method": "GET", + "headers": [ + { + "value": "application/json", + "name": "accept" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "url": "https://example.com/sample/123" + } +] diff --git a/packages/oas/tests/fixtures/params.x-example.path.swagger.yaml b/packages/oas/tests/fixtures/params.x-example.path.swagger.yaml new file mode 100644 index 00000000..df57449e --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.path.swagger.yaml @@ -0,0 +1,24 @@ +swagger: '2.0' +info: + title: Sample API + version: 1.0.0 +host: example.com +schemes: + - https +paths: + /sample/{id}: + get: + consumes: + - application/json + produces: + - application/json + parameters: + - name: id + in: path + required: true + type: integer + default: 0 + x-example: 123 + responses: + '201': + description: '' diff --git a/packages/oas/tests/fixtures/params.x-example.query.swagger.include-examples-is-false.json b/packages/oas/tests/fixtures/params.x-example.query.swagger.include-examples-is-false.json new file mode 100644 index 00000000..dd23d29d --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.query.swagger.include-examples-is-false.json @@ -0,0 +1,22 @@ +[ + { + "queryString": [ + { + "name": "filter", + "value": "default_filter" + } + ], + "cookies": [], + "method": "GET", + "headers": [ + { + "value": "application/json", + "name": "accept" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "url": "https://example.com/sample?filter=default_filter" + } +] diff --git a/packages/oas/tests/fixtures/params.x-example.query.swagger.include-examples-is-true.json b/packages/oas/tests/fixtures/params.x-example.query.swagger.include-examples-is-true.json new file mode 100644 index 00000000..1b905fe4 --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.query.swagger.include-examples-is-true.json @@ -0,0 +1,22 @@ +[ + { + "queryString": [ + { + "name": "filter", + "value": "x_example_filter" + } + ], + "cookies": [], + "method": "GET", + "headers": [ + { + "value": "application/json", + "name": "accept" + } + ], + "httpVersion": "HTTP/1.1", + "headersSize": 0, + "bodySize": 0, + "url": "https://example.com/sample?filter=x_example_filter" + } +] diff --git a/packages/oas/tests/fixtures/params.x-example.query.swagger.yaml b/packages/oas/tests/fixtures/params.x-example.query.swagger.yaml new file mode 100644 index 00000000..e1a460f5 --- /dev/null +++ b/packages/oas/tests/fixtures/params.x-example.query.swagger.yaml @@ -0,0 +1,24 @@ +swagger: '2.0' +info: + title: Sample API + version: 1.0.0 +host: example.com +schemes: + - https +paths: + /sample: + get: + consumes: + - application/json + produces: + - application/json + parameters: + - name: filter + in: query + required: false + type: string + default: 'default_filter' + x-example: 'x_example_filter' + responses: + '201': + description: '' diff --git a/packages/openapi-sampler/src/VendorExtensions.ts b/packages/openapi-sampler/src/VendorExtensions.ts new file mode 100644 index 00000000..94aa6de2 --- /dev/null +++ b/packages/openapi-sampler/src/VendorExtensions.ts @@ -0,0 +1,4 @@ +export enum VendorExtensions { + X_EXAMPLE = 'x-example', + X_EXAMPLES = 'x-examples' +} diff --git a/packages/openapi-sampler/src/index.ts b/packages/openapi-sampler/src/index.ts index 4c5bba9c..a856a1af 100644 --- a/packages/openapi-sampler/src/index.ts +++ b/packages/openapi-sampler/src/index.ts @@ -16,11 +16,13 @@ export const sample = ( options?: Options, spec?: Specification ): any | undefined => { - const opts = Object.assign( - {}, - { skipReadOnly: false, maxSampleDepth: 1 }, - options - ); + const { skipReadOnly, maxSampleDepth } = options ?? {}; + + const opts = { + ...options, + maxSampleDepth: maxSampleDepth ?? 1, + skipReadOnly: skipReadOnly ?? false + }; const traverse = new DefaultTraverse(); diff --git a/packages/openapi-sampler/src/traverse/DefaultTraverse.ts b/packages/openapi-sampler/src/traverse/DefaultTraverse.ts index 0b5ff674..dbaccbdc 100644 --- a/packages/openapi-sampler/src/traverse/DefaultTraverse.ts +++ b/packages/openapi-sampler/src/traverse/DefaultTraverse.ts @@ -4,6 +4,7 @@ import { getReplacementForCircular, mergeDeep } from '../utils'; +import { VendorExtensions } from '../VendorExtensions'; import { OpenAPISchema, Sampler } from '../samplers'; import JsonPointer from 'json-pointer'; import { OpenAPIV2, OpenAPIV3 } from '@har-sdk/core'; @@ -127,8 +128,13 @@ export class DefaultTraverse implements Traverse { let example: any; let type: string; + const vendorExample = options.includeVendorExamples + ? this.findVendorExample(schema) + : undefined; - if (this.isDefaultExists(schema)) { + if (vendorExample) { + example = vendorExample; + } else if (this.isDefaultExists(schema)) { example = schema.default; } else if ((schema as any).const !== undefined) { example = (schema as any).const; @@ -334,4 +340,75 @@ export class DefaultTraverse implements Traverse { undefined ); } + + private findVendorExample(schema: Schema) { + const example = + schema[VendorExtensions.X_EXAMPLE] ?? schema[VendorExtensions.X_EXAMPLES]; + + const schemaKeys = this.findSchemaKeys(schema); + + if ( + !example || + typeof example !== 'object' || + 0 === schemaKeys.keys.length + ) { + return example; + } + + return this.matchVendorExampleKeys(example, schemaKeys); + } + + private findSchemaKeys( + schema: Schema, + depth: number = 0 + ): { depth: number; keys: string[] } { + if ('items' in schema) { + return this.findSchemaKeys(schema.items, 1 + depth); + } + + return { + depth, + keys: 'properties' in schema ? Object.keys(schema.properties) : [] + }; + } + + private matchVendorExampleKeys( + example: unknown, + schemaKeys: { depth: number; keys: string[] }, + possibleExamples: unknown[] = [] + ): unknown { + if (!example || typeof example !== 'object') { + return undefined; + } + + if (schemaKeys.depth > 0 && Array.isArray(example)) { + return this.matchVendorExampleKeys( + [...example, undefined].shift(), + { + ...schemaKeys, + depth: schemaKeys.depth - 1 + }, + [...possibleExamples, example] + ); + } + + const objectKeys = Object.keys(example); + + if (objectKeys.every((key) => schemaKeys.keys.includes(key))) { + if (possibleExamples.length > 0) { + return possibleExamples.shift(); + } + + return example; + } + + for (const key of objectKeys) { + const value = this.matchVendorExampleKeys(example[key], schemaKeys); + if (value) { + return value; + } + } + + return undefined; + } } diff --git a/packages/openapi-sampler/src/traverse/Traverse.ts b/packages/openapi-sampler/src/traverse/Traverse.ts index e7a806c1..414ec584 100644 --- a/packages/openapi-sampler/src/traverse/Traverse.ts +++ b/packages/openapi-sampler/src/traverse/Traverse.ts @@ -4,6 +4,7 @@ export interface Options { skipReadOnly?: boolean; skipWriteOnly?: boolean; skipNonRequired?: boolean; + includeVendorExamples?: boolean; quiet?: boolean; maxSampleDepth?: number; } diff --git a/packages/openapi-sampler/tests/example.spec.ts b/packages/openapi-sampler/tests/example.spec.ts index 9ed43698..e300bda6 100644 --- a/packages/openapi-sampler/tests/example.spec.ts +++ b/packages/openapi-sampler/tests/example.spec.ts @@ -1,7 +1,9 @@ +import { VendorExtensions } from '../src/VendorExtensions'; import { sample } from '../src'; describe('Example', () => { it('should use example', () => { + // arrange const obj = { test: 'test', properties: { @@ -14,26 +16,87 @@ describe('Example', () => { type: 'object', example: obj }; + + // act const result = sample(schema); + + // assert expect(result).toEqual(obj); }); it('should use falsy example', () => { + // arrange const schema = { type: 'string', example: false }; + + // act const result = sample(schema); + + // assert expect(result).toEqual(false); }); it('should use enum', () => { + // arrange const enumList = ['test1', 'test2']; const schema = { type: 'string', enum: enumList }; + + // act const result = sample(schema); + + // assert expect(result).toEqual(enumList[0]); }); + + it.each([false, undefined])( + 'should not use vendor example when includeVendorExamples is %s', + (input) => { + // arrange + const schema = { + type: 'string', + [VendorExtensions.X_EXAMPLE]: 'foo' + }; + + // act + const result = sample(schema, { includeVendorExamples: input }); + + // assert + expect(result).toEqual('lorem'); + } + ); + + it('should prefer schema example over vendor example when includeVendorExamples is true', () => { + // arrange + const schema = { + type: 'string', + [VendorExtensions.X_EXAMPLE]: 'foo', + example: 'bar' + }; + + // act + const result = sample(schema, { includeVendorExamples: true }); + + // assert + expect(result).toEqual('bar'); + }); + + it('should prefer vendor example over default when includeVendorExamples is true', () => { + // arrange + const schema = { + type: 'string', + [VendorExtensions.X_EXAMPLE]: 'foo', + default: 'bar' + }; + + // act + const result = sample(schema, { includeVendorExamples: true }); + + // assert + expect(result).toEqual('foo'); + }); });