diff --git a/src/components/Schema/__tests__/DiscriminatorDropdown.test.tsx b/src/components/Schema/__tests__/DiscriminatorDropdown.test.tsx
index c2f2414a..609403ec 100644
--- a/src/components/Schema/__tests__/DiscriminatorDropdown.test.tsx
+++ b/src/components/Schema/__tests__/DiscriminatorDropdown.test.tsx
@@ -1,3 +1,4 @@
+import { RedocNormalizedOptions } from '../../../services/RedocNormalizedOptions';
import * as React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
@@ -17,6 +18,7 @@ describe('Components', () => {
parser,
{ $ref: '#/components/schemas/Pet' },
'#/components/schemas/Pet',
+ new RedocNormalizedOptions({}),
);
const schemaView = shallow();
expect(toJson(schemaView)).toMatchSnapshot();
@@ -29,6 +31,7 @@ describe('Components', () => {
parser,
{ $ref: '#/components/schemas/Pet' },
'#/components/schemas/Pet',
+ new RedocNormalizedOptions({}),
);
const schemaView = shallow(
number;
hideHostname: boolean;
expandResponses: { [code: string]: boolean } | 'all';
+ requiredPropsFirst: boolean;
constructor(raw: RedocRawOptions) {
this.theme = { ...(raw.theme || {}), ...defaultTheme }; // todo: merge deep
this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset);
this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname);
this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses);
+ this.requiredPropsFirst = argValueToBoolean(raw.requiredPropsFirst);
}
static normalizeExpandResponses(value: RedocRawOptions['expandResponses']) {
@@ -32,12 +41,12 @@ export class RedocNormalizedOptions {
res[code.trim()] = true;
});
return res;
- } else {
+ } else if (value !== undefined) {
console.warn(
`expandResponses must be a string but received value "${value}" of type ${typeof value}`,
);
- return {};
}
+ return {};
}
static normalizeHideHostname(value: RedocRawOptions['hideHostname']): boolean {
diff --git a/src/services/__tests__/models/Response.test.ts b/src/services/__tests__/models/Response.test.ts
index 8965eecc..5f433012 100644
--- a/src/services/__tests__/models/Response.test.ts
+++ b/src/services/__tests__/models/Response.test.ts
@@ -1,6 +1,8 @@
import { ResponseModel } from '../../models/Response';
import { OpenAPIParser } from '../../OpenAPIParser';
+import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';
+const opts = new RedocNormalizedOptions({});
describe('Models', () => {
describe('ResponseModel', () => {
let parser;
@@ -10,23 +12,23 @@ describe('Models', () => {
});
test('should calculate response type based on code', () => {
- let resp = new ResponseModel(parser, '200', false, {});
+ let resp = new ResponseModel(parser, '200', false, {}, opts);
expect(resp.type).toEqual('success');
- resp = new ResponseModel(parser, '120', false, {});
+ resp = new ResponseModel(parser, '120', false, {}, opts);
expect(resp.type).toEqual('info');
- resp = new ResponseModel(parser, '301', false, {});
+ resp = new ResponseModel(parser, '301', false, {}, opts);
expect(resp.type).toEqual('redirect');
- resp = new ResponseModel(parser, '400', false, {});
+ resp = new ResponseModel(parser, '400', false, {}, opts);
expect(resp.type).toEqual('error');
});
test('default should be sucessful by default', () => {
- let resp = new ResponseModel(parser, 'default', false, {});
+ let resp = new ResponseModel(parser, 'default', false, {}, opts);
expect(resp.type).toEqual('success');
});
test('default should be error if defaultAsError is true', () => {
- let resp = new ResponseModel(parser, 'default', true, {});
+ let resp = new ResponseModel(parser, 'default', true, {}, opts);
expect(resp.type).toEqual('error');
});
});
diff --git a/src/services/models/Field.ts b/src/services/models/Field.ts
index f4d9c783..85e9d105 100644
--- a/src/services/models/Field.ts
+++ b/src/services/models/Field.ts
@@ -1,6 +1,7 @@
import { observable, action } from 'mobx';
import { OpenAPIParameter, Referenced } from '../../types';
+import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { SchemaModel } from './Schema';
import { OpenAPIParser } from '../OpenAPIParser';
@@ -19,13 +20,18 @@ export class FieldModel {
public deprecated: boolean;
public in?: string;
- constructor(parser: OpenAPIParser, infoOrRef: Referenced, pointer: string) {
+ constructor(
+ parser: OpenAPIParser,
+ infoOrRef: Referenced,
+ pointer: string,
+ options: RedocNormalizedOptions,
+ ) {
const info = parser.deref(infoOrRef);
this.name = info.name;
this.in = info.in;
this.required = !!info.required;
- this.schema = new SchemaModel(parser, info.schema, pointer + '/schema');
+ this.schema = new SchemaModel(parser, info.schema || {}, pointer + '/schema', options);
this.description =
info.description === undefined ? this.schema.description || '' : info.description;
const example = info.example || this.schema.example;
diff --git a/src/services/models/MediaContent.ts b/src/services/models/MediaContent.ts
index 07c8fb8f..e943755a 100644
--- a/src/services/models/MediaContent.ts
+++ b/src/services/models/MediaContent.ts
@@ -3,6 +3,7 @@ import { observable, action, computed } from 'mobx';
import { OpenAPIMediaType } from '../../types';
import { MediaTypeModel } from './MediaType';
+import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { OpenAPIParser } from '../OpenAPIParser';
/**
@@ -20,13 +21,14 @@ export class MediaContentModel {
constructor(
public parser: OpenAPIParser,
info: { [mime: string]: OpenAPIMediaType },
- public isRequestType: boolean = false,
+ public isRequestType: boolean,
+ options: RedocNormalizedOptions,
) {
this.mediaTypes = Object.keys(info).map(name => {
const mime = info[name];
// reset deref cache just in case something is left there
parser.resetVisited();
- return new MediaTypeModel(parser, name, isRequestType, mime);
+ return new MediaTypeModel(parser, name, isRequestType, mime, options);
});
}
diff --git a/src/services/models/MediaType.ts b/src/services/models/MediaType.ts
index 8d2320ff..00c12f8f 100644
--- a/src/services/models/MediaType.ts
+++ b/src/services/models/MediaType.ts
@@ -2,6 +2,7 @@ import * as Sampler from 'openapi-sampler';
import { OpenAPIExample, OpenAPIMediaType } from '../../types';
import { SchemaModel } from './Schema';
+import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { mapValues, isJsonLike } from '../../utils';
import { OpenAPIParser } from '../OpenAPIParser';
@@ -16,10 +17,16 @@ export class MediaTypeModel {
/**
* @param isRequestType needed to know if skipe RO/RW fields in objects
*/
- constructor(parser: OpenAPIParser, name: string, isRequestType: boolean, info: OpenAPIMediaType) {
+ constructor(
+ parser: OpenAPIParser,
+ name: string,
+ isRequestType: boolean,
+ info: OpenAPIMediaType,
+ options: RedocNormalizedOptions,
+ ) {
this.name = name;
this.isRequestType = isRequestType;
- this.schema = info.schema && new SchemaModel(parser, info.schema, '');
+ this.schema = info.schema && new SchemaModel(parser, info.schema, '', options);
if (info.examples !== undefined) {
this.examples = mapValues(info.examples, example => new ExampleModel(parser, example));
} else if (info.example !== undefined) {
diff --git a/src/services/models/Operation.ts b/src/services/models/Operation.ts
index 1c806158..e2dd6fe6 100644
--- a/src/services/models/Operation.ts
+++ b/src/services/models/Operation.ts
@@ -71,12 +71,12 @@ export class OperationModel implements IMenuItem {
this.deprecated = !!operationSpec.deprecated;
this.operationId = operationSpec.operationId;
this.requestBody =
- operationSpec.requestBody && new RequestBodyModel(parser, operationSpec.requestBody);
+ operationSpec.requestBody && new RequestBodyModel(parser, operationSpec.requestBody, options);
this.codeSamples = operationSpec['x-code-samples'] || [];
this.path = JsonPointer.baseName(this._$ref, 2);
this.parameters = (operationSpec.parameters || []).map(
- paramOrRef => new FieldModel(parser, paramOrRef, this._$ref),
+ paramOrRef => new FieldModel(parser, paramOrRef, this._$ref, options),
);
let hasSuccessResponses = false;
diff --git a/src/services/models/RequestBody.ts b/src/services/models/RequestBody.ts
index dde14ce6..f9ed68f0 100644
--- a/src/services/models/RequestBody.ts
+++ b/src/services/models/RequestBody.ts
@@ -2,19 +2,24 @@ import { OpenAPIRequestBody, Referenced } from '../../types';
import { MediaContentModel } from './MediaContent';
import { OpenAPIParser } from '../OpenAPIParser';
+import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
export class RequestBodyModel {
description: string;
required: boolean;
content?: MediaContentModel;
- constructor(parser: OpenAPIParser, infoOrRef: Referenced) {
+ constructor(
+ parser: OpenAPIParser,
+ infoOrRef: Referenced,
+ options: RedocNormalizedOptions,
+ ) {
const info = parser.deref(infoOrRef);
this.description = info.description || '';
this.required = !!info.required;
parser.exitRef(infoOrRef);
if (info.content !== undefined) {
- this.content = new MediaContentModel(parser, info.content, true);
+ this.content = new MediaContentModel(parser, info.content, true, options);
}
}
}
diff --git a/src/services/models/Response.ts b/src/services/models/Response.ts
index 7691ee23..066e10a6 100644
--- a/src/services/models/Response.ts
+++ b/src/services/models/Response.ts
@@ -30,7 +30,7 @@ export class ResponseModel {
parser.exitRef(infoOrRef);
this.code = code;
if (info.content !== undefined) {
- this.content = new MediaContentModel(parser, info.content, false);
+ this.content = new MediaContentModel(parser, info.content, false, options);
}
this.description = info.description || '';
this.type = getStatusCodeType(code, defaultAsError);
@@ -39,7 +39,7 @@ export class ResponseModel {
if (headers !== undefined) {
this.headers = Object.keys(headers).map(name => {
const header = headers[name];
- return new FieldModel(parser, { ...header, name }, '');
+ return new FieldModel(parser, { ...header, name }, '', options);
});
}
}
diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts
index 1929460b..6373a8a9 100644
--- a/src/services/models/Schema.ts
+++ b/src/services/models/Schema.ts
@@ -1,10 +1,11 @@
-import { MergedOpenAPISchema } from '../';
import { observable, action } from 'mobx';
import { OpenAPISchema, Referenced } from '../../types';
import { FieldModel } from './Field';
import { OpenAPIParser } from '../OpenAPIParser';
+import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
+
import {
detectType,
humanizeConstraints,
@@ -12,6 +13,7 @@ import {
isPrimitiveType,
JsonPointer,
} from '../../utils/';
+import { MergedOpenAPISchema } from '../';
// TODO: refactor this model, maybe use getters instead of copying all the values
export class SchemaModel {
@@ -55,14 +57,11 @@ export class SchemaModel {
*/
constructor(
parser: OpenAPIParser,
- schemaOrRef?: Referenced,
- $ref?: string,
+ schemaOrRef: Referenced,
+ $ref: string,
+ private options: RedocNormalizedOptions,
isChild: boolean = false,
) {
- if (schemaOrRef === undefined) {
- return;
- }
-
this._$ref = schemaOrRef.$ref || $ref || '';
this.rawSchema = parser.deref(schemaOrRef);
this.schema = parser.mergeAllOf(this.rawSchema, this._$ref, isChild);
@@ -133,9 +132,9 @@ export class SchemaModel {
}
if (this.type === 'object') {
- this.fields = buildFields(parser, schema, this._$ref);
+ this.fields = buildFields(parser, schema, this._$ref, this.options);
} else if (this.type === 'array' && schema.items) {
- this.items = new SchemaModel(parser, schema.items, this._$ref + '/items');
+ this.items = new SchemaModel(parser, schema.items, this._$ref + '/items', this.options);
this.displayType = this.items.displayType;
this.typePrefix = this.items.typePrefix + 'Array of ';
this.isPrimitive = this.items.isPrimitive;
@@ -149,7 +148,7 @@ export class SchemaModel {
this.oneOf = oneOf!.map(
(variant, idx) =>
// TODO: merge base schema into each oneOf
- new SchemaModel(parser, variant, this._$ref + '/oneOf/' + idx),
+ new SchemaModel(parser, variant, this._$ref + '/oneOf/' + idx, this.options),
);
this.displayType = this.oneOf.map(schema => schema.displayType).join(' or ');
}
@@ -178,15 +177,20 @@ export class SchemaModel {
const refs = Object.keys(derived);
this.oneOf = refs.map(ref => {
- const schema = new SchemaModel(parser, parser.byRef(ref)!, ref, true);
+ const schema = new SchemaModel(parser, parser.byRef(ref)!, ref, this.options, true);
schema.title = derived[ref];
return schema;
});
}
}
-function buildFields(parser: OpenAPIParser, schema: OpenAPISchema, $ref: string): FieldModel[] {
- const props = schema.properties || [];
+function buildFields(
+ parser: OpenAPIParser,
+ schema: OpenAPISchema,
+ $ref: string,
+ options: RedocNormalizedOptions,
+): FieldModel[] {
+ const props = schema.properties || {};
const additionalProps = schema.additionalProperties;
const defaults = schema.default || {};
const fields = Object.keys(props || []).map(fieldName => {
@@ -204,9 +208,14 @@ function buildFields(parser: OpenAPIParser, schema: OpenAPISchema, $ref: string)
},
},
$ref + '/properties/' + fieldName,
+ options,
);
});
+ if (options.requiredPropsFirst) {
+ sortFields(fields, schema.required);
+ }
+
if (typeof additionalProps === 'object') {
fields.push(
new FieldModel(
@@ -217,9 +226,24 @@ function buildFields(parser: OpenAPIParser, schema: OpenAPISchema, $ref: string)
schema: additionalProps,
},
$ref + '/additionalProperties',
+ options,
),
);
}
return fields;
}
+
+function sortFields(fields: FieldModel[], order: string[] = []) {
+ fields.sort((a, b) => {
+ if (!a.required && b.required) {
+ return 1;
+ } else if (a.required && !b.required) {
+ return -1;
+ } else if (a.required && b.required) {
+ return order.indexOf(a.name) > order.indexOf(b.name) ? 1 : -1;
+ } else {
+ return 0;
+ }
+ });
+}