Skip to content

Commit

Permalink
fix: serialize parameter example values according to the spec (#917)
Browse files Browse the repository at this point in the history
  • Loading branch information
lo1tuma authored and RomanHotsiy committed Jun 20, 2019
1 parent 888f04e commit 3939286
Show file tree
Hide file tree
Showing 10 changed files with 399 additions and 38 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@
"slugify": "^1.3.4",
"stickyfill": "^1.1.1",
"swagger2openapi": "^5.2.3",
"tslib": "^1.9.3"
"tslib": "^1.9.3",
"uri-template-lite": "^19.4.0"
},
"bundlesize": [
{
Expand Down
6 changes: 5 additions & 1 deletion src/components/Fields/FieldDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ import { ExampleValue, FieldLabel } from '../../common-elements/fields';
export interface FieldDetailProps {
value?: any;
label: string;
raw?: boolean;
}

export class FieldDetail extends React.PureComponent<FieldDetailProps> {
render() {
if (this.props.value === undefined) {
return null;
}

const value = this.props.raw ? this.props.value : JSON.stringify(this.props.value);

return (
<div>
<FieldLabel> {this.props.label} </FieldLabel>{' '}
<ExampleValue>
{JSON.stringify(this.props.value)}
{value}
</ExampleValue>
</div>
);
Expand Down
15 changes: 14 additions & 1 deletion src/components/Fields/FieldDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
TypePrefix,
TypeTitle,
} from '../../common-elements/fields';
import { serializeParameterValue } from '../../utils/openapi';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Markdown } from '../Markdown/Markdown';
import { EnumValues } from './EnumValues';
Expand All @@ -27,6 +28,18 @@ export class FieldDetails extends React.PureComponent<FieldProps> {

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

let exampleField: JSX.Element | null = null;

if (showExamples) {
const label = l('example') + ':';
if (field.in && field.style) {
const serializedValue = serializeParameterValue(field, example);
exampleField = <FieldDetail label={label} value={serializedValue} raw={true} />;
} else {
exampleField = <FieldDetail label={label} value={example} />;
}
}

return (
<div>
<div>
Expand All @@ -53,7 +66,7 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
)}
<FieldDetail label={l('default') + ':'} value={schema.default} />
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
{showExamples && <FieldDetail label={l('example') + ':'} value={example} />}
{exampleField}
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}
<div>
<Markdown compact={true} source={description} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"description": "",
"example": undefined,
"expanded": false,
"explode": false,
"in": undefined,
"kind": "field",
"name": "packSize",
Expand Down Expand Up @@ -59,6 +60,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"description": "",
"example": undefined,
"expanded": false,
"explode": false,
"in": undefined,
"kind": "field",
"name": "type",
Expand Down
9 changes: 8 additions & 1 deletion src/services/__tests__/fixtures/fields.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
"in": "path",
"name": "test_name",
"schema": { "type": "string" }
},
"serializationParam": {
"in": "query",
"name": "serialization_test_name",
"schema": { "type": "array" },
"style": "form",
"explode": true
}
},
"headers": {
Expand All @@ -21,4 +28,4 @@
}
}
}
}
}
17 changes: 17 additions & 0 deletions src/services/__tests__/models/FieldModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ describe('Models', () => {
expect(field.schema.type).toEqual('string');
});

test('field details relevant for parameter serialization', () => {
const field = new FieldModel(
parser,
{
$ref: '#/components/parameters/serializationParam',
},
'#/components/parameters/serializationParam',
opts,
);

expect(field.name).toEqual('serialization_test_name');
expect(field.in).toEqual('query');
expect(field.schema.type).toEqual('array');
expect(field.style).toEqual('form');
expect(field.explode).toEqual(true);
});

test('field name should populated from name even if $ref (headers)', () => {
const field = new FieldModel(
parser,
Expand Down
32 changes: 30 additions & 2 deletions src/services/models/Field.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import { action, observable } from 'mobx';

import { OpenAPIParameter, Referenced } from '../../types';
import {
OpenAPIParameter,
OpenAPIParameterLocation,
OpenAPIParameterStyle,
Referenced,
} from '../../types';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';

import { extractExtensions } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser';
import { SchemaModel } from './Schema';

function getDefaultStyleValue(parameterLocation: OpenAPIParameterLocation): OpenAPIParameterStyle {
switch (parameterLocation) {
case 'header':
return 'simple';
case 'query':
return 'form';
case 'path':
return 'simple';
default:
return 'form';
}
}

/**
* Field or Parameter model ready to be used by components
*/
Expand All @@ -20,9 +38,11 @@ export class FieldModel {
description: string;
example?: string;
deprecated: boolean;
in?: string;
in?: OpenAPIParameterLocation;
kind: string;
extensions?: Dict<any>;
explode: boolean;
style?: OpenAPIParameterStyle;

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

if (info.style) {
this.style = info.style;
} else if (this.in) {
this.style = getDefaultStyleValue(this.in);
}

this.explode = !!info.explode;

this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated;
parser.exitRef(infoOrRef);

Expand Down
189 changes: 188 additions & 1 deletion src/utils/__tests__/openapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import {
mergeParams,
normalizeServers,
pluralizeType,
serializeParameterValue,
} from '../';

import { OpenAPIParser } from '../../services';
import { OpenAPIParameter } from '../../types';
import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types';

describe('Utils', () => {
describe('openapi getStatusCode', () => {
Expand Down Expand Up @@ -377,4 +378,190 @@ describe('Utils', () => {
);
});
});

describe('openapi serializeParameter', () => {
interface TestCase {
style: OpenAPIParameterStyle;
explode: boolean;
expected: string;
}
interface TestValueTypeGroup {
value: any;
description: string;
cases: TestCase[];
}
interface TestLocationGroup {
location: OpenAPIParameterLocation;
name: string;
description: string;
cases: TestValueTypeGroup[];
}
const testCases: TestLocationGroup[] = [
{
location: 'path',
name: 'id',
description: 'path parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'simple', explode: false, expected: '5' },
{ style: 'simple', explode: true, expected: '5' },
{ style: 'label', explode: false, expected: '.5' },
{ style: 'label', explode: true, expected: '.5' },
{ style: 'matrix', explode: false, expected: ';id=5' },
{ style: 'matrix', explode: true, expected: ';id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'simple', explode: false, expected: '3,4,5' },
{ style: 'simple', explode: true, expected: '3,4,5' },
{ style: 'label', explode: false, expected: '.3,4,5' },
{ style: 'label', explode: true, expected: '.3.4.5' },
{ style: 'matrix', explode: false, expected: ';id=3,4,5' },
{ style: 'matrix', explode: true, expected: ';id=3;id=4;id=5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
{ style: 'label', explode: false, expected: '.role,admin,firstName,Alex' },
{ style: 'label', explode: true, expected: '.role=admin,firstName=Alex' },
{ style: 'matrix', explode: false, expected: ';id=role,admin,firstName,Alex' },
{ style: 'matrix', explode: true, expected: ';role=admin;firstName=Alex' },
],
},
],
},
{
location: 'query',
name: 'id',
description: 'query parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'form', explode: true, expected: 'id=5' },
{ style: 'form', explode: false, expected: 'id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'form', explode: false, expected: 'id=3,4,5' },
{ style: 'spaceDelimited', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'spaceDelimited', explode: false, expected: 'id=3%204%205' },
{ style: 'pipeDelimited', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'pipeDelimited', explode: false, expected: 'id=3|4|5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
{ style: 'deepObject', explode: true, expected: 'id[role]=admin&id[firstName]=Alex' },
],
},
],
},
{
location: 'cookie',
name: 'id',
description: 'cookie parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'form', explode: true, expected: 'id=5' },
{ style: 'form', explode: false, expected: 'id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'form', explode: false, expected: 'id=3,4,5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
],
},
],
},
{
location: 'header',
name: 'id',
description: 'header parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'simple', explode: false, expected: '5' },
{ style: 'simple', explode: true, expected: '5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'simple', explode: false, expected: '3,4,5' },
{ style: 'simple', explode: true, expected: '3,4,5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
],
},
],
},
];

testCases.forEach(locationTestGroup => {
describe(locationTestGroup.description, () => {
locationTestGroup.cases.forEach(valueTypeTestGroup => {
describe(valueTypeTestGroup.description, () => {
valueTypeTestGroup.cases.forEach(testCase => {
it(`should serialize correctly when style is ${testCase.style} and explode is ${
testCase.explode
}`, () => {
const parameter: OpenAPIParameter = {
name: locationTestGroup.name,
in: locationTestGroup.location,
style: testCase.style,
explode: testCase.explode,
};
const serialized = serializeParameterValue(parameter, valueTypeTestGroup.value);

expect(serialized).toEqual(testCase.expected);
});
});
});
});
});
});
});
});
Loading

0 comments on commit 3939286

Please sign in to comment.