diff --git a/src/services/models/Example.ts b/src/services/models/Example.ts index a142a956..0097ed80 100644 --- a/src/services/models/Example.ts +++ b/src/services/models/Example.ts @@ -1,7 +1,7 @@ import { resolve as urlResolve } from 'url'; -import { OpenAPIExample, Referenced } from '../../types'; -import { isJsonLike } from '../../utils/openapi'; +import { OpenAPIEncoding, OpenAPIExample, Referenced } from '../../types'; +import { isFormUrlEncoded, isJsonLike, urlFormEncodePayload } from '../../utils/openapi'; import { OpenAPIParser } from '../OpenAPIParser'; const externalExamplesCache: { [url: string]: Promise } = {}; @@ -12,7 +12,12 @@ export class ExampleModel { description?: string; externalValueUrl?: string; - constructor(parser: OpenAPIParser, infoOrRef: Referenced) { + constructor( + parser: OpenAPIParser, + infoOrRef: Referenced, + mime: string, + encoding?: { [field: string]: OpenAPIEncoding }, + ) { const example = parser.deref(infoOrRef); this.value = example.value; this.summary = example.summary; @@ -21,6 +26,10 @@ export class ExampleModel { this.externalValueUrl = urlResolve(parser.specUrl || '', example.externalValue); } parser.exitRef(infoOrRef); + + if (isFormUrlEncoded(mime) && this.value && typeof this.value === 'object') { + this.value = urlFormEncodePayload(this.value, encoding); + } } getExternalValue(mimeType: string): Promise { diff --git a/src/services/models/MediaType.ts b/src/services/models/MediaType.ts index 5b1ab5d2..eda76e32 100644 --- a/src/services/models/MediaType.ts +++ b/src/services/models/MediaType.ts @@ -30,10 +30,18 @@ export class MediaTypeModel { this.schema = info.schema && new SchemaModel(parser, info.schema, '', options); this.onlyRequiredInSamples = options.onlyRequiredInSamples; if (info.examples !== undefined) { - this.examples = mapValues(info.examples, example => new ExampleModel(parser, example)); + this.examples = mapValues( + info.examples, + example => new ExampleModel(parser, example, name, info.encoding), + ); } else if (info.example !== undefined) { this.examples = { - default: new ExampleModel(parser, { value: parser.shalowDeref(info.example) }), + default: new ExampleModel( + parser, + { value: parser.shalowDeref(info.example) }, + name, + info.encoding, + ), }; } else if (isJsonLike(name)) { this.generateExample(parser, info); @@ -55,15 +63,25 @@ export class MediaTypeModel { sample[this.schema.discriminatorProp] = subSchema.title; } - this.examples[subSchema.title] = new ExampleModel(parser, { - value: sample, - }); + this.examples[subSchema.title] = new ExampleModel( + parser, + { + value: sample, + }, + this.name, + info.encoding, + ); } } else if (this.schema) { this.examples = { - default: new ExampleModel(parser, { - value: Sampler.sample(info.schema, samplerOptions, parser.spec), - }), + default: new ExampleModel( + parser, + { + value: Sampler.sample(info.schema, samplerOptions, parser.spec), + }, + this.name, + info.encoding, + ), }; } } diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index a9bd35e2..a6d142f1 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -2,6 +2,7 @@ import { dirname } from 'path'; import { OpenAPIParser } from '../services/OpenAPIParser'; import { + OpenAPIEncoding, OpenAPIMediaType, OpenAPIOperation, OpenAPIParameter, @@ -130,6 +131,101 @@ export function isJsonLike(contentType: string): boolean { return contentType.search(/json/i) !== -1; } +export function isFormUrlEncoded(contentType: string): boolean { + return contentType === 'application/x-www-form-urlencoded'; +} + +function formEncodeField(fieldVal: any, fieldName: string, explode: boolean): string { + if (!fieldVal || !fieldVal.length) { + return fieldName + '='; + } + + if (Array.isArray(fieldVal)) { + if (explode) { + return fieldVal.map(val => `${fieldName}=${val}`).join('&'); + } else { + return fieldName + '=' + fieldVal.map(val => val.toString()).join(','); + } + } else if (typeof fieldVal === 'object') { + if (explode) { + return Object.keys(fieldVal) + .map(k => `${k}=${fieldVal[k]}`) + .join('&'); + } else { + return ( + fieldName + + '=' + + Object.keys(fieldVal) + .map(k => `${k},${fieldVal[k]}`) + .join(',') + ); + } + } else { + return fieldName + '=' + fieldVal.toString(); + } +} + +function delimitedEncodeField(fieldVal: any, fieldName: string, delimeter: string): string { + if (Array.isArray(fieldVal)) { + return fieldVal.map(v => v.toString()).join(delimeter); + } else if (typeof fieldVal === 'object') { + return Object.keys(fieldVal) + .map(k => `${k}${delimeter}${fieldVal[k]}`) + .join(delimeter); + } else { + return fieldName + '=' + fieldVal.toString(); + } +} + +function deepObjectEncodeField(fieldVal: any, fieldName: string): string { + if (Array.isArray(fieldVal)) { + console.warn('deepObject style cannot be used with array value:' + fieldVal.toString()); + return ''; + } else if (typeof fieldVal === 'object') { + return Object.keys(fieldVal) + .map(k => `${fieldName}[${k}]=${fieldVal[k]}`) + .join('&'); + } else { + console.warn('deepObject style cannot be used with non-object value:' + fieldVal.toString()); + return ''; + } +} + +/* + * Should be used only for url-form-encoded body payloads + * To be used for parmaters should be extended with other style values + */ +export function urlFormEncodePayload( + payload: object, + encoding: { [field: string]: OpenAPIEncoding } = {}, +) { + if (Array.isArray(payload)) { + throw new Error('Payload must have fields: ' + payload.toString()); + } else { + return Object.keys(payload) + .map(fieldName => { + const fieldVal = payload[fieldName]; + const { style = 'form', explode = true } = encoding[fieldName] || {}; + switch (style) { + case 'form': + return formEncodeField(fieldVal, fieldName, explode); + break; + case 'spaceDelimited': + return delimitedEncodeField(fieldVal, fieldName, '%20'); + case 'pipeDelimited': + return delimitedEncodeField(fieldVal, fieldName, '|'); + case 'deepObject': + return deepObjectEncodeField(fieldVal, fieldName); + default: + // TODO implement rest of styles for path parameters + console.warn('Incorrect or unsupported encoding style: ' + style); + return ''; + } + }) + .join('&'); + } +} + export function langFromMime(contentType: string): string { if (contentType.search(/xml/i) !== -1) { return 'xml';