Skip to content

Commit 65930ad

Browse files
committed
fix: encode x-www-form-urlencoded examples correctly
fixes #870
1 parent 5af6ba7 commit 65930ad

File tree

3 files changed

+134
-11
lines changed

3 files changed

+134
-11
lines changed

src/services/models/Example.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { resolve as urlResolve } from 'url';
22

3-
import { OpenAPIExample, Referenced } from '../../types';
4-
import { isJsonLike } from '../../utils/openapi';
3+
import { OpenAPIEncoding, OpenAPIExample, Referenced } from '../../types';
4+
import { isFormUrlEncoded, isJsonLike, urlFormEncodePayload } from '../../utils/openapi';
55
import { OpenAPIParser } from '../OpenAPIParser';
66

77
const externalExamplesCache: { [url: string]: Promise<any> } = {};
@@ -12,7 +12,12 @@ export class ExampleModel {
1212
description?: string;
1313
externalValueUrl?: string;
1414

15-
constructor(parser: OpenAPIParser, infoOrRef: Referenced<OpenAPIExample>) {
15+
constructor(
16+
parser: OpenAPIParser,
17+
infoOrRef: Referenced<OpenAPIExample>,
18+
mime: string,
19+
encoding?: { [field: string]: OpenAPIEncoding },
20+
) {
1621
const example = parser.deref(infoOrRef);
1722
this.value = example.value;
1823
this.summary = example.summary;
@@ -21,6 +26,10 @@ export class ExampleModel {
2126
this.externalValueUrl = urlResolve(parser.specUrl || '', example.externalValue);
2227
}
2328
parser.exitRef(infoOrRef);
29+
30+
if (isFormUrlEncoded(mime) && this.value && typeof this.value === 'object') {
31+
this.value = urlFormEncodePayload(this.value, encoding);
32+
}
2433
}
2534

2635
getExternalValue(mimeType: string): Promise<any> {

src/services/models/MediaType.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,18 @@ export class MediaTypeModel {
3030
this.schema = info.schema && new SchemaModel(parser, info.schema, '', options);
3131
this.onlyRequiredInSamples = options.onlyRequiredInSamples;
3232
if (info.examples !== undefined) {
33-
this.examples = mapValues(info.examples, example => new ExampleModel(parser, example));
33+
this.examples = mapValues(
34+
info.examples,
35+
example => new ExampleModel(parser, example, name, info.encoding),
36+
);
3437
} else if (info.example !== undefined) {
3538
this.examples = {
36-
default: new ExampleModel(parser, { value: parser.shalowDeref(info.example) }),
39+
default: new ExampleModel(
40+
parser,
41+
{ value: parser.shalowDeref(info.example) },
42+
name,
43+
info.encoding,
44+
),
3745
};
3846
} else if (isJsonLike(name)) {
3947
this.generateExample(parser, info);
@@ -55,15 +63,25 @@ export class MediaTypeModel {
5563
sample[this.schema.discriminatorProp] = subSchema.title;
5664
}
5765

58-
this.examples[subSchema.title] = new ExampleModel(parser, {
59-
value: sample,
60-
});
66+
this.examples[subSchema.title] = new ExampleModel(
67+
parser,
68+
{
69+
value: sample,
70+
},
71+
this.name,
72+
info.encoding,
73+
);
6174
}
6275
} else if (this.schema) {
6376
this.examples = {
64-
default: new ExampleModel(parser, {
65-
value: Sampler.sample(info.schema, samplerOptions, parser.spec),
66-
}),
77+
default: new ExampleModel(
78+
parser,
79+
{
80+
value: Sampler.sample(info.schema, samplerOptions, parser.spec),
81+
},
82+
this.name,
83+
info.encoding,
84+
),
6785
};
6886
}
6987
}

src/utils/openapi.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { dirname } from 'path';
22

33
import { OpenAPIParser } from '../services/OpenAPIParser';
44
import {
5+
OpenAPIEncoding,
56
OpenAPIMediaType,
67
OpenAPIOperation,
78
OpenAPIParameter,
@@ -130,6 +131,101 @@ export function isJsonLike(contentType: string): boolean {
130131
return contentType.search(/json/i) !== -1;
131132
}
132133

134+
export function isFormUrlEncoded(contentType: string): boolean {
135+
return contentType === 'application/x-www-form-urlencoded';
136+
}
137+
138+
function formEncodeField(fieldVal: any, fieldName: string, explode: boolean): string {
139+
if (!fieldVal || !fieldVal.length) {
140+
return fieldName + '=';
141+
}
142+
143+
if (Array.isArray(fieldVal)) {
144+
if (explode) {
145+
return fieldVal.map(val => `${fieldName}=${val}`).join('&');
146+
} else {
147+
return fieldName + '=' + fieldVal.map(val => val.toString()).join(',');
148+
}
149+
} else if (typeof fieldVal === 'object') {
150+
if (explode) {
151+
return Object.keys(fieldVal)
152+
.map(k => `${k}=${fieldVal[k]}`)
153+
.join('&');
154+
} else {
155+
return (
156+
fieldName +
157+
'=' +
158+
Object.keys(fieldVal)
159+
.map(k => `${k},${fieldVal[k]}`)
160+
.join(',')
161+
);
162+
}
163+
} else {
164+
return fieldName + '=' + fieldVal.toString();
165+
}
166+
}
167+
168+
function delimitedEncodeField(fieldVal: any, fieldName: string, delimeter: string): string {
169+
if (Array.isArray(fieldVal)) {
170+
return fieldVal.map(v => v.toString()).join(delimeter);
171+
} else if (typeof fieldVal === 'object') {
172+
return Object.keys(fieldVal)
173+
.map(k => `${k}${delimeter}${fieldVal[k]}`)
174+
.join(delimeter);
175+
} else {
176+
return fieldName + '=' + fieldVal.toString();
177+
}
178+
}
179+
180+
function deepObjectEncodeField(fieldVal: any, fieldName: string): string {
181+
if (Array.isArray(fieldVal)) {
182+
console.warn('deepObject style cannot be used with array value:' + fieldVal.toString());
183+
return '';
184+
} else if (typeof fieldVal === 'object') {
185+
return Object.keys(fieldVal)
186+
.map(k => `${fieldName}[${k}]=${fieldVal[k]}`)
187+
.join('&');
188+
} else {
189+
console.warn('deepObject style cannot be used with non-object value:' + fieldVal.toString());
190+
return '';
191+
}
192+
}
193+
194+
/*
195+
* Should be used only for url-form-encoded body payloads
196+
* To be used for parmaters should be extended with other style values
197+
*/
198+
export function urlFormEncodePayload(
199+
payload: object,
200+
encoding: { [field: string]: OpenAPIEncoding } = {},
201+
) {
202+
if (Array.isArray(payload)) {
203+
throw new Error('Payload must have fields: ' + payload.toString());
204+
} else {
205+
return Object.keys(payload)
206+
.map(fieldName => {
207+
const fieldVal = payload[fieldName];
208+
const { style = 'form', explode = true } = encoding[fieldName] || {};
209+
switch (style) {
210+
case 'form':
211+
return formEncodeField(fieldVal, fieldName, explode);
212+
break;
213+
case 'spaceDelimited':
214+
return delimitedEncodeField(fieldVal, fieldName, '%20');
215+
case 'pipeDelimited':
216+
return delimitedEncodeField(fieldVal, fieldName, '|');
217+
case 'deepObject':
218+
return deepObjectEncodeField(fieldVal, fieldName);
219+
default:
220+
// TODO implement rest of styles for path parameters
221+
console.warn('Incorrect or unsupported encoding style: ' + style);
222+
return '';
223+
}
224+
})
225+
.join('&');
226+
}
227+
}
228+
133229
export function langFromMime(contentType: string): string {
134230
if (contentType.search(/xml/i) !== -1) {
135231
return 'xml';

0 commit comments

Comments
 (0)