redoc/src/services/models/Schema.ts

269 lines
7.2 KiB
TypeScript
Raw Normal View History

2018-01-22 21:30:53 +03:00
import { action, observable } from 'mobx';
2017-10-12 00:01:37 +03:00
import { OpenAPISchema, Referenced } from '../../types';
import { OpenAPIParser } from '../OpenAPIParser';
2017-11-21 14:24:41 +03:00
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
2018-01-22 21:30:53 +03:00
import { FieldModel } from './Field';
2017-11-21 14:24:41 +03:00
2018-01-22 21:30:53 +03:00
import { MergedOpenAPISchema } from '../';
2017-10-12 00:01:37 +03:00
import {
detectType,
humanizeConstraints,
isNamedDefinition,
isPrimitiveType,
JsonPointer,
sortByRequired,
2017-10-12 00:01:37 +03:00
} from '../../utils/';
// TODO: refactor this model, maybe use getters instead of copying all the values
export class SchemaModel {
2018-07-26 17:34:44 +03:00
pointer: string;
2017-10-12 00:01:37 +03:00
type: string;
displayType: string;
typePrefix: string = '';
title: string;
description: string;
isPrimitive: boolean;
isCircular: boolean = false;
format?: string;
displayFormat?: string;
2017-10-12 00:01:37 +03:00
nullable: boolean;
deprecated: boolean;
pattern?: string;
example?: any;
enum: any[];
default?: any;
readOnly: boolean;
writeOnly: boolean;
2017-10-12 00:01:37 +03:00
constraints: string[];
fields?: FieldModel[];
items?: SchemaModel;
oneOf?: SchemaModel[];
oneOfType: string;
discriminatorProp: string;
@observable activeOneOf: number = 0;
rawSchema: OpenAPISchema;
schema: MergedOpenAPISchema;
/**
* @param isChild if schema discriminator Child
* When true forces dereferencing in allOfs even if circular
*/
constructor(
parser: OpenAPIParser,
2017-11-21 14:24:41 +03:00
schemaOrRef: Referenced<OpenAPISchema>,
2018-07-26 17:34:44 +03:00
pointer: string,
2017-11-21 14:24:41 +03:00
private options: RedocNormalizedOptions,
2017-10-12 00:01:37 +03:00
isChild: boolean = false,
) {
2018-07-26 17:34:44 +03:00
this.pointer = schemaOrRef.$ref || pointer || '';
2017-10-12 00:01:37 +03:00
this.rawSchema = parser.deref(schemaOrRef);
2018-07-26 17:34:44 +03:00
this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild);
2017-10-12 00:01:37 +03:00
this.init(parser, isChild);
parser.exitRef(schemaOrRef);
2018-01-22 21:30:53 +03:00
for (const parent$ref of this.schema.parentRefs || []) {
2017-10-12 00:01:37 +03:00
// exit all the refs visited during allOf traverse
2018-01-22 21:30:53 +03:00
parser.exitRef({ $ref: parent$ref });
2017-10-12 00:01:37 +03:00
}
}
/**
* Set specified alternative schema as active
* @param idx oneOf index
*/
@action
activateOneOf(idx: number) {
this.activeOneOf = idx;
}
init(parser: OpenAPIParser, isChild: boolean) {
const schema = this.schema;
this.isCircular = schema['x-circular-ref'];
this.title =
2018-07-26 17:34:44 +03:00
schema.title || (isNamedDefinition(this.pointer) && JsonPointer.baseName(this.pointer)) || '';
2017-10-12 00:01:37 +03:00
this.description = schema.description || '';
this.type = schema.type || detectType(schema);
this.format = schema.format;
this.nullable = !!schema.nullable;
this.enum = schema.enum || [];
this.example = schema.example;
this.deprecated = !!schema.deprecated;
this.pattern = schema.pattern;
this.constraints = humanizeConstraints(schema);
2017-12-07 20:12:34 +03:00
this.displayType = this.type;
this.displayFormat = this.format;
2017-10-12 00:01:37 +03:00
this.isPrimitive = isPrimitiveType(schema);
this.default = schema.default;
this.readOnly = !!schema.readOnly;
this.writeOnly = !!schema.writeOnly;
2017-10-12 00:01:37 +03:00
if (this.isCircular) {
return;
}
if (!isChild && getDiscriminator(schema) !== undefined) {
2017-10-12 00:01:37 +03:00
this.initDiscriminator(schema, parser);
return;
}
if (schema.oneOf !== undefined) {
this.initOneOf(schema.oneOf, parser);
this.oneOfType = 'One of';
if (schema.anyOf !== undefined) {
console.warn(
2018-07-26 17:34:44 +03:00
`oneOf and anyOf are not supported on the same level. Skipping anyOf at ${this.pointer}`,
2017-10-12 00:01:37 +03:00
);
}
return;
}
if (schema.anyOf !== undefined) {
this.initOneOf(schema.anyOf, parser);
this.oneOfType = 'Any of';
return;
}
if (this.type === 'object') {
2018-07-26 17:34:44 +03:00
this.fields = buildFields(parser, schema, this.pointer, this.options);
2017-10-12 00:01:37 +03:00
} else if (this.type === 'array' && schema.items) {
2018-07-26 17:34:44 +03:00
this.items = new SchemaModel(parser, schema.items, this.pointer + '/items', this.options);
2017-10-12 00:01:37 +03:00
this.displayType = this.items.displayType;
this.displayFormat = this.items.format;
2017-10-12 00:01:37 +03:00
this.typePrefix = this.items.typePrefix + 'Array of ';
this.title = this.title || this.items.title;
2017-10-12 00:01:37 +03:00
this.isPrimitive = this.items.isPrimitive;
if (this.example === undefined && this.items.example !== undefined) {
this.example = [this.items.example];
}
2017-10-12 00:01:37 +03:00
if (this.items.isPrimitive) {
this.enum = this.items.enum;
}
}
}
private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) {
this.oneOf = oneOf!.map(
(variant, idx) =>
new SchemaModel(
parser,
{
// merge base schema into each of oneOf's subschemas
allOf: [variant, { ...this.schema, oneOf: undefined, anyOf: undefined }],
} as OpenAPISchema,
2018-07-26 17:34:44 +03:00
this.pointer + '/oneOf/' + idx,
this.options,
),
2017-10-12 00:01:37 +03:00
);
this.displayType = this.oneOf.map(schema => schema.displayType).join(' or ');
}
private initDiscriminator(
schema: OpenAPISchema & {
2018-02-08 02:10:15 +03:00
parentRefs?: string[];
2017-10-12 00:01:37 +03:00
},
parser: OpenAPIParser,
) {
const discriminator = getDiscriminator(schema)!;
this.discriminatorProp = discriminator.propertyName;
2018-07-26 17:34:44 +03:00
const derived = parser.findDerived([...(schema.parentRefs || []), this.pointer]);
2017-10-12 00:01:37 +03:00
if (schema.oneOf) {
2018-01-22 21:30:53 +03:00
for (const variant of schema.oneOf) {
if (variant.$ref === undefined) {
continue;
}
2017-10-12 00:01:37 +03:00
const name = JsonPointer.dirName(variant.$ref);
derived[variant.$ref] = name;
}
}
const mapping = discriminator.mapping || {};
2018-01-22 21:30:53 +03:00
for (const name in mapping) {
2017-10-12 00:01:37 +03:00
derived[mapping[name]] = name;
}
const refs = Object.keys(derived);
this.oneOf = refs.map(ref => {
2018-01-22 21:30:53 +03:00
const innerSchema = new SchemaModel(parser, parser.byRef(ref)!, ref, this.options, true);
innerSchema.title = derived[ref];
return innerSchema;
2017-10-12 00:01:37 +03:00
});
}
}
2017-11-21 14:24:41 +03:00
function buildFields(
parser: OpenAPIParser,
schema: OpenAPISchema,
$ref: string,
options: RedocNormalizedOptions,
): FieldModel[] {
const props = schema.properties || {};
2017-10-12 00:01:37 +03:00
const additionalProps = schema.additionalProperties;
const defaults = schema.default || {};
const fields = Object.keys(props || []).map(fieldName => {
2018-03-07 17:43:56 +03:00
let field = props[fieldName];
if (!field) {
console.warn(
`Field "${fieldName}" is invalid, skipping.\n Field must be an object but got ${typeof field} at "${$ref}"`,
);
field = {};
}
2017-10-12 00:01:37 +03:00
const required =
schema.required === undefined ? false : schema.required.indexOf(fieldName) > -1;
return new FieldModel(
parser,
{
name: fieldName,
required,
schema: {
2018-03-07 17:43:56 +03:00
...field,
default: field.default === undefined ? defaults[fieldName] : field.default,
2017-10-12 00:01:37 +03:00
},
},
$ref + '/properties/' + fieldName,
2017-11-21 14:24:41 +03:00
options,
2017-10-12 00:01:37 +03:00
);
});
2017-11-21 14:24:41 +03:00
if (options.requiredPropsFirst) {
sortByRequired(fields, schema.required);
2017-11-21 14:24:41 +03:00
}
if (typeof additionalProps === 'object' || additionalProps === true) {
2017-10-12 00:01:37 +03:00
fields.push(
new FieldModel(
parser,
{
name: 'property name *',
2017-10-12 00:01:37 +03:00
required: false,
schema: additionalProps === true ? {} : additionalProps,
kind: 'additionalProperties',
2017-10-12 00:01:37 +03:00
},
$ref + '/additionalProperties',
2017-11-21 14:24:41 +03:00
options,
2017-10-12 00:01:37 +03:00
),
);
}
return fields;
}
function getDiscriminator(schema: OpenAPISchema): OpenAPISchema['discriminator'] {
return schema.discriminator || schema['x-discriminator'];
}