Skip to content

Commit

Permalink
feat(oas): introduce support for examples vendor extensions in Swagge…
Browse files Browse the repository at this point in the history
…r converter

closes #224
  • Loading branch information
ostridm authored Feb 27, 2024
1 parent 30eb0e4 commit a47621a
Show file tree
Hide file tree
Showing 39 changed files with 1,939 additions and 148 deletions.
12 changes: 12 additions & 0 deletions packages/oas/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ const requests = await oas2har(response.data);
console.log(requests);
```

Some specifications may incorporate example values for parameters provided in vendor extension fields, to include such examples in output use the `oas2har` function as follows:

```js
import schema from './swagger.json' assert { type: 'json' };
import { oas2har } from '@har-sdk/oas';

const requests = await oas2har(schema, { includeVendorExamples: true });
console.log(requests);
```

Notice the `includeVendorExamples` option affects Swagger specifications only.

## License

Copyright © 2023 [Bright Security](https://brightsec.com/).
Expand Down
73 changes: 54 additions & 19 deletions packages/oas/src/converter/Sampler.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { ConvertError } from '../errors';
import { sample, Schema } from '@har-sdk/openapi-sampler';
import { isOASV2 } from '../utils';
import {
Options,
sample,
Schema,
VendorExtensions
} 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: {
Expand All @@ -12,23 +20,15 @@ export class Sampler {
tokens: string[];
}
): any {
return this.sample(
'schema' in param
? {
...param.schema,
...(param.example !== undefined ? { example: param.example } : {})
}
: param,
{
spec: context.spec,
jsonPointer: pointer.compile([
...context.tokens,
'parameters',
context.idx.toString(),
...('schema' in param ? ['schema'] : [])
])
}
);
return this.sample(this.createParamSchema(param), {
spec: context.spec,
jsonPointer: pointer.compile([
...context.tokens,
'parameters',
context.idx.toString(),
...('schema' in param ? ['schema'] : [])
])
});
}

/**
Expand All @@ -43,9 +43,44 @@ export class Sampler {
}
): any | undefined {
try {
return sample(schema, { skipReadOnly: true, quiet: true }, context?.spec);
const { includeVendorExamples } = this.options;
const options = {
...this.options,
skipReadOnly: true,
quiet: true,
includeVendorExamples:
context?.spec && isOASV2(context?.spec)
? includeVendorExamples
: false
};

return sample(schema, options, context?.spec);
} catch (e) {
throw new ConvertError(e.message, context?.jsonPointer);
}
}

private createParamSchema(param: OpenAPI.Parameter): Schema {
if ('schema' in param) {
const { schema, example, ...rest } = param;

return {
...schema,
...(example !== undefined ? { example } : {}),
...this.extractVendorExamples(rest)
};
}

return param as Schema;
}

private extractVendorExamples(param: OpenAPI.Parameter) {
return [VendorExtensions.X_EXAMPLE, VendorExtensions.X_EXAMPLES].reduce(
(acc, prop) => ({
...acc,
...(param[prop] !== undefined ? { [prop]: param[prop] } : {})
}),
{}
);
}
}
7 changes: 5 additions & 2 deletions packages/oas/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import type { OpenAPI, Request } from '@har-sdk/core';

export * from './errors';

export const oas2har = (collection: OpenAPI.Document): Promise<Request[]> => {
export const oas2har = (
collection: OpenAPI.Document,
options: { includeVendorExamples?: boolean } = {}
): Promise<Request[]> => {
if (!collection) {
throw new TypeError('Please provide a valid OAS specification.');
}

const sampler = new Sampler();
const sampler = new Sampler(options);
const baseUrlParser = new BaseUrlParser(sampler);
const subConverterFactory = new SubConverterFactory(sampler);
const subConverterRegistry = new SubConverterRegistry(subConverterFactory);
Expand Down
146 changes: 123 additions & 23 deletions packages/oas/tests/DefaultConverter.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
[
{
Expand Down Expand Up @@ -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);
});
});

Expand Down Expand Up @@ -271,18 +282,107 @@ 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({
jsonPointer: expected
});
})
);

it('should ignore x-example when includeVendorExamples is true (oas)', async () => {
// arrange
const { inputDoc, expectedDoc } = await createFixture({
inputFile: `./fixtures/x-example.oas.yaml`,
expectedFile: `./fixtures/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 vendor example when vendor examples inclusion disabled (swagger)',
async (input) => {
// arrange
const { inputDoc, expectedDoc } = await createFixture({
inputFile: `./fixtures/x-example.${input}.swagger.yaml`,
expectedFile: `./fixtures/x-example.${input}.disabled.swagger.result.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 use %s parameter vendor example when vendor examples inclusion enabled (swagger)',
async (input) => {
// arrange
const { inputDoc, expectedDoc } = await createFixture({
inputFile: `./fixtures/x-example.${input}.swagger.yaml`,
expectedFile: `./fixtures/x-example.${input}.swagger.result.json`
});

// act
const result: Request[] = await oas2har(inputDoc as any, {
includeVendorExamples: true
});

// assert
expect(result).toStrictEqual(expectedDoc);
}
);

it.each(['schemathesis', 'redocly', 'api-connect', 'smartbear'])(
'should ignore body parameter vendor example when vendor examples inclusion disabled (swagger, %s)',
async (input) => {
// arrange
const { inputDoc, expectedDoc } = await createFixture({
inputFile: `./fixtures/x-example.body.${input}.swagger.yaml`,
expectedFile: `./fixtures/x-example.body.disabled.swagger.result.json`
});

// act
const result: Request[] = await oas2har(inputDoc as any, {
includeVendorExamples: false
});

// assert
expect(result).toStrictEqual(expectedDoc);
}
);

it.each(['schemathesis', 'redocly', 'api-connect', 'smartbear'])(
'should use body parameter vendor example when vendor examples inclusion enabled (swagger, %s)',
async (input) => {
// arrange
const { inputDoc, expectedDoc } = await createFixture({
inputFile: `./fixtures/x-example.body.${input}.swagger.yaml`,
expectedFile: `./fixtures/x-example.body.${input}.swagger.result.json`
});

// act
const result: Request[] = await oas2har(inputDoc as any, {
includeVendorExamples: true
});

// assert
expect(result).toStrictEqual(expectedDoc);
}
);
});
});
Original file line number Diff line number Diff line change
@@ -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"
}
]
Loading

0 comments on commit a47621a

Please sign in to comment.