redoc/lib/services/schema-normalizer.service.ts

247 lines
7.4 KiB
TypeScript
Raw Normal View History

2016-06-22 19:13:57 +03:00
'use strict';
import { Injectable } from '@angular/core';
2016-10-23 20:18:42 +03:00
import { SpecManager } from '../utils/spec-manager';
2016-06-22 19:13:57 +03:00
import { JsonPointer } from '../utils/JsonPointer';
import { defaults } from '../utils/helpers';
import { WarningsService } from './warnings.service';
2016-06-22 19:13:57 +03:00
2017-04-18 16:37:05 +03:00
export interface Reference {
2016-06-22 19:13:57 +03:00
$ref: string;
description: string;
}
2017-04-18 16:37:05 +03:00
export interface Schema {
2016-06-22 19:13:57 +03:00
properties: any;
allOf: any;
items: any;
additionalProperties: any;
}
2016-06-22 21:17:48 +03:00
export class SchemaNormalizer {
2016-06-22 19:13:57 +03:00
_dereferencer:SchemaDereferencer;
2016-12-02 12:59:29 +03:00
constructor(_schema:any) {
2016-06-22 19:13:57 +03:00
this._dereferencer = new SchemaDereferencer(_schema, this);
}
normalize(schema, ptr, opts:any ={}) {
2016-09-01 09:53:42 +03:00
let hasPtr = !!schema.$ref;
if (opts.resolved && !hasPtr) this._dereferencer.visit(ptr);
if (opts.childFor) this._dereferencer.visit(opts.childFor);
2016-11-05 17:25:59 +03:00
if (schema['x-redoc-normalized']) return schema;
let res = SchemaWalker.walk(schema, ptr, (subSchema, ptr) => {
2016-06-22 19:13:57 +03:00
let resolved = this._dereferencer.dereference(subSchema, ptr);
if (resolved.allOf) {
resolved._pointer = resolved._pointer || ptr;
2016-07-28 07:51:22 +03:00
resolved = Object.assign({}, resolved);
2016-09-01 09:53:42 +03:00
AllOfMerger.merge(resolved, resolved.allOf);
2016-06-22 19:13:57 +03:00
}
return resolved;
});
2016-09-01 09:53:42 +03:00
if (opts.resolved && !hasPtr) this._dereferencer.exit(ptr);
if (opts.childFor) this._dereferencer.exit(opts.childFor);
2016-11-05 17:25:59 +03:00
res['x-redoc-normalized'] = true;
2016-06-22 19:13:57 +03:00
return res;
}
reset() {
this._dereferencer.reset();
}
2016-06-22 19:13:57 +03:00
}
class SchemaWalker {
static walk(obj:Schema, pointer:string, visitor:Function) {
if (obj == undefined || typeof(obj) !== 'object') {
return;
}
if (obj['x-redoc-visited']) {
obj['x-redoc-visited'] = false;
const res = visitor(obj, pointer);
// circular, return only title and description
2018-02-23 15:34:57 +03:00
return { title: res.title, description: res.description };
}
2018-02-23 15:34:57 +03:00
obj['x-redoc-visited'] = true;
2016-06-22 19:13:57 +03:00
if (obj.properties) {
let ptr = JsonPointer.join(pointer, ['properties']);
SchemaWalker.walkEach(obj.properties, ptr, visitor);
}
2016-06-25 13:02:13 +03:00
2016-06-22 19:13:57 +03:00
if (obj.additionalProperties) {
let ptr = JsonPointer.join(pointer, ['additionalProperties']);
2016-06-25 13:02:13 +03:00
if (Array.isArray(obj.additionalProperties)) {
SchemaWalker.walkEach(obj.additionalProperties, ptr, visitor);
} else {
let res = SchemaWalker.walk(obj.additionalProperties, ptr, visitor);
if (res) obj.additionalProperties = res;
}
2016-06-22 19:13:57 +03:00
}
if (obj.allOf) {
let ptr = JsonPointer.join(pointer, ['allOf']);
SchemaWalker.walkEach(obj.allOf, ptr, visitor);
}
if (obj.items) {
let ptr = JsonPointer.join(pointer, ['items']);
if (Array.isArray(obj.items)) {
SchemaWalker.walkEach(obj.items, ptr, visitor);
} else {
let res = SchemaWalker.walk(obj.items, ptr, visitor);
if (res) obj.items = res;
}
}
obj['x-redoc-visited'] = false;
return visitor(obj, pointer);
2016-06-22 19:13:57 +03:00
}
private static walkEach(obj:Object, pointer:string, visitor:Function) {
for(let key of Object.keys(obj)) {
let ptr = JsonPointer.join(pointer, [key]);
let res = SchemaWalker.walk(obj[key], ptr, visitor);
if (res) obj[key] = res;
}
}
}
2017-01-26 17:46:28 +03:00
export class AllOfMerger {
2016-09-01 09:53:42 +03:00
static merge(into, schemas) {
2016-06-22 19:13:57 +03:00
into['x-derived-from'] = [];
let hadDiscriminator = !!into.discriminator;
2016-06-22 19:13:57 +03:00
for (let i=0; i < schemas.length; i++) {
let subSchema = schemas[i];
into['x-derived-from'].push(subSchema._pointer);
AllOfMerger.checkCanMerge(subSchema, into);
into.type = into.type || subSchema.type;
if (into.type === 'object') {
AllOfMerger.mergeObject(into, subSchema, i);
}
// don't merge _pointer
let tmpPtr = subSchema._pointer;
2016-06-22 19:13:57 +03:00
subSchema._pointer = null;
defaults(into, subSchema);
subSchema._pointer = tmpPtr;
2016-06-22 19:13:57 +03:00
}
if (!hadDiscriminator) into.discriminator = null;
delete into.allOf;
2016-06-22 19:13:57 +03:00
}
private static mergeObject(into, subSchema, allOfNumber) {
if (subSchema.properties) {
into.properties = Object.assign({}, into.properties || {});
2016-06-22 19:13:57 +03:00
Object.assign(into.properties, subSchema.properties);
Object.keys(subSchema.properties).forEach(propName => {
let prop = subSchema.properties[propName];
if (!prop._pointer) {
let schemaPtr = subSchema._pointer || JsonPointer.join(into._pointer, ['allOf', allOfNumber]);
prop._pointer = prop._pointer || JsonPointer.join(schemaPtr, ['properties', propName]);
}
});
}
if (subSchema.required) {
if (!into.required) into.required = [];
into.required.push(...subSchema.required);
}
}
private static checkCanMerge(subSchema, into) {
// TODO: add support for merge array schemas
if (typeof subSchema !== 'object') {
let errMessage = `Items of allOf should be Object: ${typeof subSchema} found ` +
`${subSchema} at "#${into._pointer}"`;
2016-06-22 19:13:57 +03:00
throw new Error(errMessage);
}
if (into.type && subSchema.type && into.type !== subSchema.type) {
let errMessage = `allOf merging error: schemas with different types can't be merged: ` +
`"${into.type}" and "${subSchema.type}" at "#${into._pointer}"`;
2016-06-22 19:13:57 +03:00
throw new Error(errMessage);
}
if (into.type === 'array') {
WarningsService.warn('allOf: subschemas with type "array" are not supported yet');
2016-06-22 19:13:57 +03:00
}
// TODO: add check if can be merged correctly (no different properties with the same name)
// TODO: merge properties
2016-06-22 19:13:57 +03:00
}
}
class RefCounter {
private _counter = {};
reset():void {
this._counter = {};
}
visit(ref:string):void {
this._counter[ref] = this._counter[ref] ? this._counter[ref] + 1 : 1;
}
exit(ref:string):void {
this._counter[ref] = this._counter[ref] && this._counter[ref] - 1;
}
visited(ref:string):boolean {
return !!this._counter[ref];
}
}
2017-04-18 16:37:05 +03:00
export class SchemaDereferencer {
2016-06-22 19:13:57 +03:00
private _refCouner = new RefCounter();
2016-06-22 21:17:48 +03:00
constructor(private _spec: SpecManager, private normalizator: SchemaNormalizer) {
2016-06-22 19:13:57 +03:00
}
reset() {
this._refCouner.reset();
}
2016-06-22 19:13:57 +03:00
2016-09-01 09:53:42 +03:00
visit($ref) {
this._refCouner.visit($ref);
}
exit($ref) {
this._refCouner.exit($ref);
}
2016-06-22 19:13:57 +03:00
dereference(schema: Reference, pointer:string):any {
if (!schema || !schema.$ref) return schema;
let $ref = schema.$ref;
let resolved = this._spec.byPointer($ref);
if (!this._refCouner.visited($ref)) {
resolved._pointer = $ref;
} else {
// for circular referenced save only title and type
resolved = {
title: resolved.title,
type: resolved.type
};
}
this._refCouner.visit($ref);
// if resolved schema doesn't have title use name from ref
resolved.title = resolved.title || JsonPointer.baseName($ref);
let keysCount = Object.keys(schema).filter(key => !key.startsWith('x-redoc')).length;
2016-06-22 19:13:57 +03:00
if ( keysCount > 2 || (keysCount === 2 && !schema.description) ) {
WarningsService.warn(`Other properties are defined at the same level as $ref at "#${pointer}". ` +
2016-09-08 21:31:17 +03:00
'They are IGNORED according to the JsonSchema spec');
2016-06-22 19:13:57 +03:00
}
resolved = this.normalizator.normalize(resolved, $ref);
this._refCouner.exit($ref);
// schema description should always override $ref description
if (schema.description) {
// make a copy of resolved object with updated description, do not globally override the description
resolved = Object.assign({}, resolved);
resolved.description = schema.description;
}
2016-06-22 19:13:57 +03:00
return resolved;
}
}