Skip to content

Commit 3939286

Browse files
lo1tumaRomanHotsiy
authored andcommitted
fix: serialize parameter example values according to the spec (#917)
1 parent 888f04e commit 3939286

File tree

10 files changed

+399
-38
lines changed

10 files changed

+399
-38
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@
154154
"slugify": "^1.3.4",
155155
"stickyfill": "^1.1.1",
156156
"swagger2openapi": "^5.2.3",
157-
"tslib": "^1.9.3"
157+
"tslib": "^1.9.3",
158+
"uri-template-lite": "^19.4.0"
158159
},
159160
"bundlesize": [
160161
{

src/components/Fields/FieldDetail.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@ import { ExampleValue, FieldLabel } from '../../common-elements/fields';
44
export interface FieldDetailProps {
55
value?: any;
66
label: string;
7+
raw?: boolean;
78
}
89

910
export class FieldDetail extends React.PureComponent<FieldDetailProps> {
1011
render() {
1112
if (this.props.value === undefined) {
1213
return null;
1314
}
15+
16+
const value = this.props.raw ? this.props.value : JSON.stringify(this.props.value);
17+
1418
return (
1519
<div>
1620
<FieldLabel> {this.props.label} </FieldLabel>{' '}
1721
<ExampleValue>
18-
{JSON.stringify(this.props.value)}
22+
{value}
1923
</ExampleValue>
2024
</div>
2125
);

src/components/Fields/FieldDetails.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
TypePrefix,
1010
TypeTitle,
1111
} from '../../common-elements/fields';
12+
import { serializeParameterValue } from '../../utils/openapi';
1213
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
1314
import { Markdown } from '../Markdown/Markdown';
1415
import { EnumValues } from './EnumValues';
@@ -27,6 +28,18 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
2728

2829
const { schema, description, example, deprecated } = field;
2930

31+
let exampleField: JSX.Element | null = null;
32+
33+
if (showExamples) {
34+
const label = l('example') + ':';
35+
if (field.in && field.style) {
36+
const serializedValue = serializeParameterValue(field, example);
37+
exampleField = <FieldDetail label={label} value={serializedValue} raw={true} />;
38+
} else {
39+
exampleField = <FieldDetail label={label} value={example} />;
40+
}
41+
}
42+
3043
return (
3144
<div>
3245
<div>
@@ -53,7 +66,7 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
5366
)}
5467
<FieldDetail label={l('default') + ':'} value={schema.default} />
5568
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
56-
{showExamples && <FieldDetail label={l('example') + ':'} value={example} />}
69+
{exampleField}
5770
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}
5871
<div>
5972
<Markdown compact={true} source={description} />

src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
1010
"description": "",
1111
"example": undefined,
1212
"expanded": false,
13+
"explode": false,
1314
"in": undefined,
1415
"kind": "field",
1516
"name": "packSize",
@@ -59,6 +60,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
5960
"description": "",
6061
"example": undefined,
6162
"expanded": false,
63+
"explode": false,
6264
"in": undefined,
6365
"kind": "field",
6466
"name": "type",

src/services/__tests__/fixtures/fields.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
"in": "path",
1111
"name": "test_name",
1212
"schema": { "type": "string" }
13+
},
14+
"serializationParam": {
15+
"in": "query",
16+
"name": "serialization_test_name",
17+
"schema": { "type": "array" },
18+
"style": "form",
19+
"explode": true
1320
}
1421
},
1522
"headers": {
@@ -21,4 +28,4 @@
2128
}
2229
}
2330
}
24-
}
31+
}

src/services/__tests__/models/FieldModel.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,23 @@ describe('Models', () => {
2626
expect(field.schema.type).toEqual('string');
2727
});
2828

29+
test('field details relevant for parameter serialization', () => {
30+
const field = new FieldModel(
31+
parser,
32+
{
33+
$ref: '#/components/parameters/serializationParam',
34+
},
35+
'#/components/parameters/serializationParam',
36+
opts,
37+
);
38+
39+
expect(field.name).toEqual('serialization_test_name');
40+
expect(field.in).toEqual('query');
41+
expect(field.schema.type).toEqual('array');
42+
expect(field.style).toEqual('form');
43+
expect(field.explode).toEqual(true);
44+
});
45+
2946
test('field name should populated from name even if $ref (headers)', () => {
3047
const field = new FieldModel(
3148
parser,

src/services/models/Field.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
import { action, observable } from 'mobx';
22

3-
import { OpenAPIParameter, Referenced } from '../../types';
3+
import {
4+
OpenAPIParameter,
5+
OpenAPIParameterLocation,
6+
OpenAPIParameterStyle,
7+
Referenced,
8+
} from '../../types';
49
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
510

611
import { extractExtensions } from '../../utils/openapi';
712
import { OpenAPIParser } from '../OpenAPIParser';
813
import { SchemaModel } from './Schema';
914

15+
function getDefaultStyleValue(parameterLocation: OpenAPIParameterLocation): OpenAPIParameterStyle {
16+
switch (parameterLocation) {
17+
case 'header':
18+
return 'simple';
19+
case 'query':
20+
return 'form';
21+
case 'path':
22+
return 'simple';
23+
default:
24+
return 'form';
25+
}
26+
}
27+
1028
/**
1129
* Field or Parameter model ready to be used by components
1230
*/
@@ -20,9 +38,11 @@ export class FieldModel {
2038
description: string;
2139
example?: string;
2240
deprecated: boolean;
23-
in?: string;
41+
in?: OpenAPIParameterLocation;
2442
kind: string;
2543
extensions?: Dict<any>;
44+
explode: boolean;
45+
style?: OpenAPIParameterStyle;
2646

2747
constructor(
2848
parser: OpenAPIParser,
@@ -40,6 +60,14 @@ export class FieldModel {
4060
info.description === undefined ? this.schema.description || '' : info.description;
4161
this.example = info.example || this.schema.example;
4262

63+
if (info.style) {
64+
this.style = info.style;
65+
} else if (this.in) {
66+
this.style = getDefaultStyleValue(this.in);
67+
}
68+
69+
this.explode = !!info.explode;
70+
4371
this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated;
4472
parser.exitRef(infoOrRef);
4573

src/utils/__tests__/openapi.test.ts

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import {
88
mergeParams,
99
normalizeServers,
1010
pluralizeType,
11+
serializeParameterValue,
1112
} from '../';
1213

1314
import { OpenAPIParser } from '../../services';
14-
import { OpenAPIParameter } from '../../types';
15+
import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types';
1516

1617
describe('Utils', () => {
1718
describe('openapi getStatusCode', () => {
@@ -377,4 +378,190 @@ describe('Utils', () => {
377378
);
378379
});
379380
});
381+
382+
describe('openapi serializeParameter', () => {
383+
interface TestCase {
384+
style: OpenAPIParameterStyle;
385+
explode: boolean;
386+
expected: string;
387+
}
388+
interface TestValueTypeGroup {
389+
value: any;
390+
description: string;
391+
cases: TestCase[];
392+
}
393+
interface TestLocationGroup {
394+
location: OpenAPIParameterLocation;
395+
name: string;
396+
description: string;
397+
cases: TestValueTypeGroup[];
398+
}
399+
const testCases: TestLocationGroup[] = [
400+
{
401+
location: 'path',
402+
name: 'id',
403+
description: 'path parameters',
404+
cases: [
405+
{
406+
value: 5,
407+
description: 'primitive values',
408+
cases: [
409+
{ style: 'simple', explode: false, expected: '5' },
410+
{ style: 'simple', explode: true, expected: '5' },
411+
{ style: 'label', explode: false, expected: '.5' },
412+
{ style: 'label', explode: true, expected: '.5' },
413+
{ style: 'matrix', explode: false, expected: ';id=5' },
414+
{ style: 'matrix', explode: true, expected: ';id=5' },
415+
],
416+
},
417+
{
418+
value: [3, 4, 5],
419+
description: 'array values',
420+
cases: [
421+
{ style: 'simple', explode: false, expected: '3,4,5' },
422+
{ style: 'simple', explode: true, expected: '3,4,5' },
423+
{ style: 'label', explode: false, expected: '.3,4,5' },
424+
{ style: 'label', explode: true, expected: '.3.4.5' },
425+
{ style: 'matrix', explode: false, expected: ';id=3,4,5' },
426+
{ style: 'matrix', explode: true, expected: ';id=3;id=4;id=5' },
427+
],
428+
},
429+
{
430+
value: { role: 'admin', firstName: 'Alex' },
431+
description: 'object values',
432+
cases: [
433+
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
434+
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
435+
{ style: 'label', explode: false, expected: '.role,admin,firstName,Alex' },
436+
{ style: 'label', explode: true, expected: '.role=admin,firstName=Alex' },
437+
{ style: 'matrix', explode: false, expected: ';id=role,admin,firstName,Alex' },
438+
{ style: 'matrix', explode: true, expected: ';role=admin;firstName=Alex' },
439+
],
440+
},
441+
],
442+
},
443+
{
444+
location: 'query',
445+
name: 'id',
446+
description: 'query parameters',
447+
cases: [
448+
{
449+
value: 5,
450+
description: 'primitive values',
451+
cases: [
452+
{ style: 'form', explode: true, expected: 'id=5' },
453+
{ style: 'form', explode: false, expected: 'id=5' },
454+
],
455+
},
456+
{
457+
value: [3, 4, 5],
458+
description: 'array values',
459+
cases: [
460+
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
461+
{ style: 'form', explode: false, expected: 'id=3,4,5' },
462+
{ style: 'spaceDelimited', explode: true, expected: 'id=3&id=4&id=5' },
463+
{ style: 'spaceDelimited', explode: false, expected: 'id=3%204%205' },
464+
{ style: 'pipeDelimited', explode: true, expected: 'id=3&id=4&id=5' },
465+
{ style: 'pipeDelimited', explode: false, expected: 'id=3|4|5' },
466+
],
467+
},
468+
{
469+
value: { role: 'admin', firstName: 'Alex' },
470+
description: 'object values',
471+
cases: [
472+
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
473+
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
474+
{ style: 'deepObject', explode: true, expected: 'id[role]=admin&id[firstName]=Alex' },
475+
],
476+
},
477+
],
478+
},
479+
{
480+
location: 'cookie',
481+
name: 'id',
482+
description: 'cookie parameters',
483+
cases: [
484+
{
485+
value: 5,
486+
description: 'primitive values',
487+
cases: [
488+
{ style: 'form', explode: true, expected: 'id=5' },
489+
{ style: 'form', explode: false, expected: 'id=5' },
490+
],
491+
},
492+
{
493+
value: [3, 4, 5],
494+
description: 'array values',
495+
cases: [
496+
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
497+
{ style: 'form', explode: false, expected: 'id=3,4,5' },
498+
],
499+
},
500+
{
501+
value: { role: 'admin', firstName: 'Alex' },
502+
description: 'object values',
503+
cases: [
504+
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
505+
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
506+
],
507+
},
508+
],
509+
},
510+
{
511+
location: 'header',
512+
name: 'id',
513+
description: 'header parameters',
514+
cases: [
515+
{
516+
value: 5,
517+
description: 'primitive values',
518+
cases: [
519+
{ style: 'simple', explode: false, expected: '5' },
520+
{ style: 'simple', explode: true, expected: '5' },
521+
],
522+
},
523+
{
524+
value: [3, 4, 5],
525+
description: 'array values',
526+
cases: [
527+
{ style: 'simple', explode: false, expected: '3,4,5' },
528+
{ style: 'simple', explode: true, expected: '3,4,5' },
529+
],
530+
},
531+
{
532+
value: { role: 'admin', firstName: 'Alex' },
533+
description: 'object values',
534+
cases: [
535+
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
536+
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
537+
],
538+
},
539+
],
540+
},
541+
];
542+
543+
testCases.forEach(locationTestGroup => {
544+
describe(locationTestGroup.description, () => {
545+
locationTestGroup.cases.forEach(valueTypeTestGroup => {
546+
describe(valueTypeTestGroup.description, () => {
547+
valueTypeTestGroup.cases.forEach(testCase => {
548+
it(`should serialize correctly when style is ${testCase.style} and explode is ${
549+
testCase.explode
550+
}`, () => {
551+
const parameter: OpenAPIParameter = {
552+
name: locationTestGroup.name,
553+
in: locationTestGroup.location,
554+
style: testCase.style,
555+
explode: testCase.explode,
556+
};
557+
const serialized = serializeParameterValue(parameter, valueTypeTestGroup.value);
558+
559+
expect(serialized).toEqual(testCase.expected);
560+
});
561+
});
562+
});
563+
});
564+
});
565+
});
566+
});
380567
});

0 commit comments

Comments
 (0)