Skip to content

Commit

Permalink
Merge branch 'master' into feat_#185/add_support_for_extra_format_opt…
Browse files Browse the repository at this point in the history
…ions_in_OpenAPI_Sampler
  • Loading branch information
pmstss committed Feb 21, 2023
2 parents c3a9300 + d0b0a2e commit 8ac12bf
Show file tree
Hide file tree
Showing 22 changed files with 1,006 additions and 118 deletions.
12 changes: 8 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/oas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@har-sdk/core": "*",
"@har-sdk/openapi-sampler": "*",
"json-pointer": "^0.6.2",
"jstoxml": "^2.2.9",
"jstoxml": "^3.2.6",
"qs": "^6.10.3",
"tslib": "^2.3.1",
"url-template": "^2.0.8"
Expand Down
29 changes: 24 additions & 5 deletions packages/oas/src/converter/DefaultConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
SecurityRequirementsParser
} from './security';
import type { PathItemObject } from '../types';
import { getOperation } from '../utils';
import { getOperation, isOASV2 } from '../utils';
import $RefParser, { JSONSchema } from '@apidevtools/json-schema-ref-parser';
import type {
Header,
Expand All @@ -20,6 +20,7 @@ import pointer from 'json-pointer';
export class DefaultConverter implements Converter {
private spec: OpenAPI.Document;
private securityRequirements?: SecurityRequirementsParser<OpenAPI.Document>;
private readonly refParser = new $RefParser();

constructor(
private readonly baseUrlParser: BaseUrlParser,
Expand All @@ -28,10 +29,7 @@ export class DefaultConverter implements Converter {
) {}

public async convert(spec: OpenAPI.Document): Promise<Request[]> {
this.spec = (await new $RefParser().dereference(
JSON.parse(JSON.stringify(spec)) as JSONSchema,
{ resolve: { file: false, http: false } }
)) as OpenAPI.Document;
this.spec = await this.normalizeSpec(spec);

this.securityRequirements = this.securityRequirementsFactory.create(
this.spec
Expand All @@ -48,6 +46,27 @@ export class DefaultConverter implements Converter {
);
}

private async normalizeSpec(
spec: OpenAPI.Document
): Promise<OpenAPI.Document> {
const copy: OpenAPI.Document = JSON.parse(JSON.stringify(spec));
const schemas = isOASV2(copy) ? copy.definitions : copy.components?.schemas;
// ADHOC: requires the schema name be identical to the model name rather than using the 'root' name.
// For details please refer to the documentation at https://swagger.io/docs/specification/data-models/representing-xml/
for (const [name, schema] of Object.entries(schemas ?? {})) {
if ('$ref' in schema) {
continue;
}

schema.xml ??= {};
schema.xml.name ??= name;
}

return (await this.refParser.dereference(copy as JSONSchema, {
resolve: { file: false, http: false }
})) as OpenAPI.Document;
}

private createHarEntry(path: string, method: string): Request {
const queryString = this.convertPart<QueryString[]>(
SubPart.QUERY_STRING,
Expand Down
1 change: 1 addition & 0 deletions packages/oas/src/converter/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './parts';
export * from './security';
export * from './serializers';
export * from './Converter';
export * from './DefaultConverter';
export * from './Sampler';
102 changes: 43 additions & 59 deletions packages/oas/src/converter/parts/postdata/BodyConverter.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { Sampler } from '../../Sampler';
import { SubConverter } from '../../SubConverter';
import { OpenAPI, OpenAPIV3, PostData } from '@har-sdk/core';
import { toXML, XmlElement } from 'jstoxml';
import type { Sampler } from '../../Sampler';
import type { SubConverter } from '../../SubConverter';
import { XmlSerializer } from '../../serializers';
import type { OpenAPI, OpenAPIV2, OpenAPIV3, PostData } from '@har-sdk/core';
import { stringify } from 'qs';

export interface EncodingData {
value: unknown;
contentType: string;
fields?: Record<string, OpenAPIV3.EncodingObject>;
schema?: OpenAPIV3.SchemaObject | OpenAPIV2.SchemaObject;
}

export abstract class BodyConverter<T extends OpenAPI.Document>
implements SubConverter<PostData | null>
{
private readonly xmlSerializer = new XmlSerializer();
private readonly JPG_IMAGE = '/9j/7g=='; // 0xff, 0xd8, 0xff, 0xee
private readonly PNG_IMAGE = 'iVBORw0KGgo='; // 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0A, 0x1a, 0x0a
private readonly ICO_IMAGE = 'AAABAA=='; // 0x00, 0x00, 0x01, 0x00
Expand All @@ -27,35 +35,25 @@ export abstract class BodyConverter<T extends OpenAPI.Document>
method: string
): string | undefined;

protected encodePayload(
data: unknown,
contentType: string,
encoding?: OpenAPIV3.EncodingObject
): { mimeType: string; text: string } {
let encodedData = data;

if (encoding) {
encodedData = this.encodeProperties(
Object.keys(encoding),
data,
encoding
);
}

protected encodePayload({ contentType, ...options }: EncodingData): PostData {
return {
mimeType: contentType.includes('multipart')
? `${contentType}; boundary=${this.BOUNDARY}`
: contentType,
text: this.encodeValue(encodedData, contentType)
text: this.encodeValue({
contentType,
...options
})
};
}

// eslint-disable-next-line complexity
private encodeValue(
value: unknown,
contentType: string,
encoding?: string
): string {
protected encodeValue({
value,
contentType,
schema,
fields
}: EncodingData): string {
const [mime]: string[] = contentType
.split(',')
.map((x) => x.trim().replace(/;.+?$/, ''));
Expand All @@ -66,10 +64,12 @@ export abstract class BodyConverter<T extends OpenAPI.Document>
case 'application/x-www-form-urlencoded':
return this.encodeFormUrlencoded(value);
case 'application/xml':
return this.encodeXml(value);
case 'text/xml':
case 'application/atom+xml':
return this.encodeXml(value, schema);
case 'multipart/form-data':
case 'multipart/mixed':
return this.encodeMultipartFormData(value, encoding);
return this.encodeMultipartFormData(value, fields);
case 'image/x-icon':
case 'image/ico':
case 'image/vnd.microsoft.icon':
Expand All @@ -87,12 +87,18 @@ export abstract class BodyConverter<T extends OpenAPI.Document>
}
}

private encodeMultipartFormData(value: unknown, encoding?: string): string {
// TODO: move the logic that receives the content type from the encoding object
// to the {@link Oas3RequestBodyConverter} class.
private encodeMultipartFormData(
value: unknown,
fields?: Record<string, OpenAPIV3.EncodingObject>
): string {
const EOL = '\r\n';

return Object.entries(value || {})
.map(([key, val]: [string, unknown]) => {
const contentType = encoding ?? this.inferMultipartContentType(val);
const contentType =
fields?.[key]?.contentType ?? this.inferMultipartContentType(val);
const filenameRequired = this.filenameRequired(contentType);
const content = this.encodeOther(val);

Expand All @@ -116,8 +122,8 @@ export abstract class BodyConverter<T extends OpenAPI.Document>
.concat(`${EOL}--${this.BOUNDARY}--`);
}

private filenameRequired(contentType: string) {
return !['application/json', 'text/plain'].includes(contentType);
private filenameRequired(contentType: string): boolean {
return 'application/octet-stream' === contentType;
}

private inferMultipartContentType(value: unknown): string {
Expand All @@ -139,45 +145,23 @@ export abstract class BodyConverter<T extends OpenAPI.Document>
}
}

private encodeProperties(
keys: string[],
data: unknown,
encoding?: OpenAPIV3.EncodingObject
): unknown {
const sample = keys.reduce((encodedSample, encodingKey) => {
const { contentType }: OpenAPIV3.EncodingObject =
encoding?.[encodingKey] ?? {};

encodedSample[encodingKey] = this.encodeValue(
data[encodingKey],
contentType,
encodingKey
);

return encodedSample;
}, {});

return Object.assign({}, data, sample);
}

private encodeJson(value: unknown): string {
return typeof value === 'string' ? value : JSON.stringify(value);
}

// TODO: we should take into account the the Encoding Object's style property (OAS3)
private encodeFormUrlencoded(value: unknown): string {
return stringify(value, {
format: 'RFC3986',
encode: false
});
}

private encodeXml(value: unknown): string {
const xmlOptions = {
header: true,
indent: ' '
};

return toXML(value as XmlElement, xmlOptions);
private encodeXml(
data: unknown,
schema: OpenAPIV3.SchemaObject | OpenAPIV2.SchemaObject
): string {
return this.xmlSerializer.serialize(data, schema);
}

private encodeOther(value: unknown): string {
Expand Down
14 changes: 7 additions & 7 deletions packages/oas/src/converter/parts/postdata/Oas2BodyConverter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BodyConverter } from './BodyConverter';
import { Sampler } from '../../Sampler';
import type { Sampler } from '../../Sampler';
import { filterLocationParams, getParameters, isOASV2 } from '../../../utils';
import { OpenAPIV2, PostData } from '@har-sdk/core';
import type { OpenAPIV2, PostData } from '@har-sdk/core';

export class Oas2BodyConverter extends BodyConverter<OpenAPIV2.Document> {
constructor(spec: OpenAPIV2.Document, sampler: Sampler) {
Expand Down Expand Up @@ -58,20 +58,20 @@ export class Oas2BodyConverter extends BodyConverter<OpenAPIV2.Document> {
tokens: string[];
contentType?: string;
}
): { mimeType: string; text: string } {
): PostData {
const formDataParams = filterLocationParams(params, 'formdata');
if (!formDataParams.length) {
return null;
}

const data = Object.fromEntries(
const value = Object.fromEntries(
formDataParams.map((param) => [
param.name,
this.sampleParam(param, { ...sampleOptions, params })
])
);

return this.encodePayload(data, contentType);
return this.encodePayload({ value, contentType });
}

private convertUnsupportedParamToString(
Expand All @@ -94,14 +94,14 @@ export class Oas2BodyConverter extends BodyConverter<OpenAPIV2.Document> {
tokens: string[];
contentType?: string;
}
): { mimeType: string; text: string } {
): PostData {
const bodyParams = filterLocationParams(params, 'body');

for (const param of bodyParams) {
if ('schema' in param) {
const value = this.sampleParam(param, { ...sampleOptions, params });

return this.encodePayload(value, contentType);
return this.encodePayload({ value, contentType, schema: param.schema });
}
}
}
Expand Down
Loading

0 comments on commit 8ac12bf

Please sign in to comment.