Refactor + perf optimizations

This commit is contained in:
Roman Hotsiy 2016-06-22 19:13:57 +03:00
parent 9341d4904e
commit 1e97ea655e
13 changed files with 526 additions and 419 deletions

View File

@ -59,9 +59,10 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
setTimeout( ()=> {
let $element = compRef.location.nativeElement;
// skip caching view with tabs inside (discriminator) as it needs attached controller
// FIXME: get rid of dependency on selector
if ($element.querySelector('.discriminator-wrap')) {
// skip caching view with tabs inside (discriminator)
// as it needs attached controller
if (compRef.instance.hasDiscriminator) {
this._loadAfterSelf();
return;
}
insertAfter($element.cloneNode(true), this.elementRef.nativeElement);

View File

@ -19,7 +19,7 @@
</span>
<table *ngIf="!schema.isTrivial" class="params-wrap" [ngClass]="{'params-array': schema._isArray}">
<!-- <caption> {{_displayType}} </caption> -->
<template ngFor [ngForOf]="schema.properties" let-prop="$implicit" let-last="last">
<template ngFor [ngForOf]="schema._properties" let-prop="$implicit" let-last="last">
<tr class="param" [ngClass]="{'last': last,
'discriminator': prop.isDiscriminator && !derivedEmtpy,
'complex': prop._pointer,
@ -40,11 +40,11 @@
<span *ngFor="let enumItem of prop.enum" class="enum-value {{enumItem.type}}"> {{enumItem.val | json}} </span>
</div>
</div>
<div class="param-description" innerHtml="{{prop.description | marked}}"></div>
<div class="param-description" [innerHtml]="prop.description | marked"></div>
<div class="discriminator-info" *ngIf="prop.isDiscriminator">
<span>This field value determines the exact schema:</span>
<drop-down (change)="selectDerived($event)">
<option *ngFor="let derived of schema.derived; let i=index"
<option *ngFor="let derived of schema._derived; let i=index"
[value]="i">{{derived.name}}</option>
</drop-down>
</div>
@ -58,9 +58,9 @@
</td>
</tr>
</template>
<tr *ngIf="schema.derived.length" class="param-wrap discriminator-wrap" [ngClass]="{'empty': derivedEmtpy}">
<tr *ngIf="schema._derived.length" class="param-wrap discriminator-wrap" [ngClass]="{'empty': derivedEmtpy}">
<td colspan="2">
<div class="derived-schema" *ngFor="let derived of schema.derived" [ngClass]="{active: derived.active}">
<div class="derived-schema" *ngFor="let derived of schema._derived" [ngClass]="{active: derived.active}">
<json-schema class="discriminator-part" *ngIf="!derived.empty" [childFor]="pointer"
pointer="{{derived.$ref}}" [final]="derived.final" [isRequestSchema]="isRequestSchema">
</json-schema>

View File

@ -56,7 +56,7 @@ describe('Redoc components', () => {
test: {}
}};
fixture.detectChanges();
component.schema.properties[0]._displayType.should.be.equal('< * >');
component.schema._properties[0]._displayType.should.be.equal('< * >');
});
});
});

View File

@ -5,6 +5,7 @@ import { ElementRef, Input } from '@angular/core';
import { RedocComponent, BaseComponent, SchemaManager } from '../base';
import { DropDown } from '../../shared/components/index';
import JsonPointer from '../../utils/JsonPointer';
import { SchemaNormalizator } from '../../services/spec-helper.service';
@RedocComponent({
selector: 'json-schema',
@ -17,11 +18,13 @@ export class JsonSchema extends BaseComponent {
$element: any;
schema: any;
derivedEmtpy: boolean;
hasDiscriminator: boolean = false;
@Input() isArray: boolean;
@Input() final: boolean = false;
@Input() nestOdd: boolean;
@Input() childFor: string;
@Input() isRequestSchema: boolean;
normalizer: SchemaNormalizator;
static injectPropertyData(propertySchema, propertyName, propPointer, hostPointer?) {
propertySchema = Object.assign({}, propertySchema);
@ -35,12 +38,13 @@ export class JsonSchema extends BaseComponent {
constructor(schemaMgr:SchemaManager, elementRef:ElementRef) {
super(schemaMgr);
this.$element = elementRef.nativeElement;
this.normalizer = new SchemaNormalizator(schemaMgr);
}
selectDerived(subClassIdx) {
let subClass = this.schema.derived[subClassIdx];
let subClass = this.schema._derived[subClassIdx];
if (!subClass || subClass.active) return;
this.schema.derived.forEach((subSchema) => {
this.schema._derived.forEach((subSchema) => {
subSchema.active = false;
});
subClass.active = true;
@ -65,14 +69,18 @@ export class JsonSchema extends BaseComponent {
if (!this.componentSchema) {
throw new Error(`Can't load component schema at ${this.pointer}`);
}
this.dereference();
if (this.componentSchema['x-redoc-js-precompiled']) {
this.schema = this.unwrapArray(this.componentSchema);
return;
}
this.componentSchema = this.normalizer.normalize(this.componentSchema, this.pointer);
this.componentSchema['x-redoc-js-precompiled'] = true;
let schema = this.componentSchema;
BaseComponent.joinAllOf(schema, {omitParent: true});
this.schema = schema = this.unwrapArray(schema);
runInjectors(schema, schema, schema._pointer || this.pointer, this.pointer);
schema.derived = schema.derived || [];
this.schema._derived = this.schema._derived || [];
if (!schema.isTrivial) {
this.prepareObjectPropertiesData(schema);
@ -82,16 +90,17 @@ export class JsonSchema extends BaseComponent {
}
initDerived() {
if (!this.schema.derived.length) return;
let enumArr = this.schema.properties[this.schema.properties.length - 1].enum;
if (!this.schema._derived.length) return;
this.hasDiscriminator = true;
let enumArr = this.schema._properties[this.schema._properties.length - 1].enum;
if (enumArr) {
let enumOrder = {};
enumArr.forEach((enumItem, idx) => {
enumOrder[enumItem.val] = idx;
});
this.schema.derived.sort((a, b) => {
return enumOrder[a.name] > enumOrder[b.name];
this.schema._derived.sort((a, b) => {
return enumOrder[a.name] > enumOrder[b.name] ? 1 : -1;
});
}
this.selectDerived(0);
@ -138,7 +147,7 @@ export class JsonSchema extends BaseComponent {
if (this.isRequestSchema) {
props = props.filter(prop => !prop.readOnly);
}
schema.properties = props;
schema._properties = props;
}
prepareAdditionalProperties(schema) {
@ -165,7 +174,7 @@ const injectors = {
discriminator: {
check: (propertySchema) => propertySchema.discriminator,
inject: (injectTo, propertySchema = injectTo, pointer) => {
injectTo.derived = SchemaManager.instance().findDerivedDefinitions(pointer);
injectTo._derived = SchemaManager.instance().findDerivedDefinitions(pointer);
injectTo.discriminator = propertySchema.discriminator;
}
},

View File

@ -2,6 +2,9 @@
import { provide, enableProdMode, ElementRef,
ComponentRef, AfterViewInit } from '@angular/core';
import {enableProdMode as compilerProd} from '@angular/compiler/src/facade/lang';
import {enableProdMode as browserProd } from '@angular/platform-browser/src/facade/lang';
import {CompilerConfig} from '@angular/compiler';
import { bootstrap } from '@angular/platform-browser-dynamic';
import { BrowserDomAdapter } from '@angular/platform-browser/src/browser/browser_adapter';
import { RedocComponent, BaseComponent } from '../base';
@ -60,7 +63,8 @@ export class Redoc extends BaseComponent implements AfterViewInit {
optionsService.options = options;
optionsService.options.specUrl = optionsService.options.specUrl || specUrl;
var providers = [
provide(OptionsService, {useValue: optionsService})
provide(OptionsService, {useValue: optionsService}),
provide(CompilerConfig, {useValue: new CompilerConfig({genDebugInfo: false, logBindingUpdate: false})})
];
if (Redoc.appRef) {
@ -71,6 +75,8 @@ export class Redoc extends BaseComponent implements AfterViewInit {
.then(() => {
if (!_modeLocked && !optionsService.options.debugMode) {
enableProdMode();
compilerProd();
browserProd();
_modeLocked = true;
}
return bootstrap(Redoc, providers);

View File

@ -6,6 +6,7 @@ import * as OpenAPISampler from 'openapi-sampler';
import { RedocComponent, BaseComponent, SchemaManager } from '../base';
import { JsonFormatter } from '../../utils/JsonFormatterPipe';
import { SchemaNormalizator } from '../../services/spec-helper.service';
@RedocComponent({
selector: 'schema-sample',
@ -17,12 +18,17 @@ export class SchemaSample extends BaseComponent {
element: any;
data: any;
@Input() skipReadOnly:boolean;
private _normalizer:SchemaNormalizator;
constructor(schemaMgr:SchemaManager, elementRef:ElementRef) {
super(schemaMgr);
this.element = elementRef.nativeElement;
this._normalizer = new SchemaNormalizator(schemaMgr);
}
init() {
this.bindEvents();
this.data = {};
let base:any = {};
@ -37,7 +43,10 @@ export class SchemaSample extends BaseComponent {
if (base.examples && base.examples['application/json']) {
sample = base.examples['application/json'];
} else {
this.dereference(this.componentSchema);
this.componentSchema = this._normalizer.normalize(this.componentSchema, this.pointer);
if (this.fromCache()) {
return;
}
try {
sample = OpenAPISampler.sample(this.componentSchema, {
skipReadOnly: this.skipReadOnly
@ -46,10 +55,30 @@ export class SchemaSample extends BaseComponent {
// no sample available
}
}
this.cache(sample);
this.data.sample = sample;
}
cache(sample) {
if (this.skipReadOnly) {
this.componentSchema['x-redoc-ro-sample'] = sample;
} else {
this.componentSchema['x-redoc-rw-sample'] = sample;
}
}
fromCache() {
if (this.skipReadOnly && this.componentSchema['x-redoc-ro-sample']) {
this.data.sample = this.componentSchema['x-redoc-ro-sample'];
return true;
} else if (this.componentSchema['x-redoc-rw-sample']) {
this.data.sample = this.componentSchema['x-redoc-rw-sample'];
return true;
}
return false;
}
bindEvents() {
this.element.addEventListener('click', (event) => {
var collapsed, target = event.target;
if (event.target.className === 'collapser') {

View File

@ -39,253 +39,5 @@ describe('Redoc components', () => {
component.prepareModel.and.callThrough();
component.init.and.callThrough();
});
describe('dereference', () => {
beforeAll((done) => {
schemaMgr.load('/tests/schemas/base-component-dereference.json').then(
() => done()
);
});
describe('simple dereference', () => {
let paramWithRef;
beforeAll(() => {
component.pointer = '/paths/test1/get';
component.ngOnInit();
component.dereference();
paramWithRef = component.componentSchema.parameters[0];
});
it('should not contain $ref property', () => {
expect(paramWithRef.$ref).toBeUndefined();
});
it('should inject Title if not exist based on reference', () => {
paramWithRef.title.should.be.equal('Simple');
});
it('should inject pointer', () => {
paramWithRef._pointer.should.be.equal('#/definitions/Simple');
});
it('should insert correct definition instead of reference', () => {
delete paramWithRef.title;
delete paramWithRef._pointer;
paramWithRef.should.be.deepEqual(schemaMgr.schema.definitions.Simple);
});
});
describe('nested dereference', () => {
let paramWithRef;
beforeAll(() => {
component.pointer = '/paths/test2/get';
component.ngOnInit();
component.dereference();
paramWithRef = component.componentSchema.parameters[0];
});
it('should not touch title if exist', () => {
paramWithRef.title.should.be.equal('NesteTitle');
});
it('should resolve nested schema', () => {
expect(paramWithRef.properties.subschema.$ref).toBeUndefined();
paramWithRef._pointer.should.be.equal('#/definitions/Nested');
paramWithRef.properties.subschema._pointer.should.be.equal('#/definitions/Simple');
paramWithRef.properties.subschema.type.should.be.equal('object');
});
});
describe('array schema dereference', () => {
let paramWithRef;
beforeAll(() => {
component.pointer = '/paths/test3/get';
component.ngOnInit();
component.dereference();
paramWithRef = component.componentSchema.parameters[0];
});
it('should resolve array schema', () => {
expect(paramWithRef.$ref).toBeUndefined();
expect(paramWithRef.items.schema.$ref).toBeUndefined();
paramWithRef.type.should.be.equal('array');
paramWithRef._pointer.should.be.equal('#/definitions/ArrayOfSimple');
paramWithRef.items.schema._pointer.should.be.equal('#/definitions/Simple');
paramWithRef.items.schema.type.should.be.equal('object');
});
});
describe('circular dereference', () => {
let paramWithRef;
beforeAll(() => {
component.pointer = '/paths/test4/get';
component.ngOnInit();
component.dereference();
paramWithRef = component.componentSchema.parameters[0];
});
it('should resolve circular schema', () => {
expect(paramWithRef.$ref).toBeUndefined();
expect(paramWithRef.items.schema.$ref).toBeUndefined();
paramWithRef.type.should.be.equal('array');
paramWithRef._pointer.should.be.equal('#/definitions/Circular');
expect(paramWithRef.items.schema._pointer).toBeUndefined();
paramWithRef.items.schema.title.should.be.equal('Circular');
});
});
describe('$ref with other fields on the same level', () => {
let paramWithRef;
beforeAll(() => {
component.pointer = '/paths/test5/get';
component.ngOnInit();
component.dereference();
paramWithRef = component.componentSchema.parameters[0];
});
it('should skip other fields', () => {
expect(paramWithRef.$ref).toBeUndefined();
expect(paramWithRef.title).toBeDefined();
paramWithRef.title.should.be.equal('Simple');
});
it('should preserve description field', () => {
expect(paramWithRef.$ref).toBeUndefined();
expect(paramWithRef.description).toBeDefined();
paramWithRef.description.should.be.equal('test');
});
});
});
describe('mergeAllOf', () => {
beforeAll((done) => {
schemaMgr.load('tests/schemas/base-component-joinallof.json').then(() => done());
});
describe('Simple allOf merge', () => {
let joined;
beforeAll(() => {
component.pointer = '/definitions/SimpleAllOf';
component.ngOnInit();
component.dereference();
BaseComponent.joinAllOf(component.componentSchema);
joined = component.componentSchema;
});
it('should remove $allOf field', () => {
expect(joined.allOf).toBeNull();
});
it('should set type object', () => {
joined.type.should.be.equal('object');
});
it('should merge properties', () => {
Object.keys(joined.properties).length.should.be.equal(3);
Object.keys(joined.properties).should.be.deepEqual(['prop1', 'prop2', 'prop3']);
});
it('should merge required', () => {
joined.required.length.should.be.equal(2);
joined.required.should.be.deepEqual(['prop1', 'prop3']);
});
});
describe('AllOf with refrence', () => {
let joined;
beforeAll(() => {
component.pointer = '/definitions/AllOfWithRef';
component.ngOnInit();
component.dereference();
BaseComponent.joinAllOf(component.componentSchema);
joined = component.componentSchema;
});
it('should remove $allOf field', () => {
expect(joined.allOf).toBeNull();
});
it('should set type object', () => {
joined.type.should.be.equal('object');
});
it('should merge properties', () => {
Object.keys(joined.properties).length.should.be.equal(2);
Object.keys(joined.properties).should.be.deepEqual(['id', 'prop3']);
});
it('should merge required', () => {
joined.required.length.should.be.equal(2);
joined.required.should.be.deepEqual(['id', 'prop3']);
});
});
describe('AllOf with other properties on the allOf level', () => {
let joined;
beforeAll(() => {
component.pointer = '/definitions/AllOfWithOther';
component.ngOnInit();
component.dereference();
BaseComponent.joinAllOf(component.componentSchema);
joined = component.componentSchema;
});
it('should remove $allOf field', () => {
expect(joined.allOf).toBeNull();
});
it('should set type object', () => {
joined.type.should.be.equal('object');
});
it('should merge properties', () => {
Object.keys(joined.properties).length.should.be.equal(1);
Object.keys(joined.properties).should.be.deepEqual(['id']);
});
it('should merge required', () => {
joined.required.length.should.be.equal(1);
joined.required.should.be.deepEqual(['id']);
});
it('should preserve parent properties', () => {
joined.description.should.be.equal('Test');
joined.readOnly.should.be.equal(true);
});
});
describe('allOf edgecases', () => {
it('should merge properties and required when defined on allOf level', () => {
component.pointer = '/definitions/PropertiesOnAllOfLevel';
component.ngOnInit();
component.dereference();
(() => BaseComponent.joinAllOf(component.componentSchema)).should.not.throw();
let joined = component.componentSchema;
Object.keys(joined.properties).length.should.be.equal(3);
});
it('should throw when merging schemas with different types', () => {
component.pointer = '/definitions/BadAllOf1';
component.ngOnInit();
component.dereference();
(() => BaseComponent.joinAllOf(component.componentSchema)).should.throw();
});
it('should handle nested allOF', () => {
component.pointer = '/definitions/NestedAllOf';
component.ngOnInit();
component.dereference();
(() => BaseComponent.joinAllOf(component.componentSchema)).should.not.throw();
let joined = component.componentSchema;
Object.keys(joined.properties).length.should.be.equal(4);
Object.keys(joined.properties).should.be.deepEqual(['prop1', 'prop2', 'prop3', 'prop4']);
joined.required.should.be.deepEqual(['prop1', 'prop3']);
});
});
xdescribe('Merge array allOf', () => {
//emtpy
});
});
});
});

View File

@ -2,7 +2,6 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { CORE_DIRECTIVES, JsonPipe, AsyncPipe } from '@angular/common';
import { SchemaManager } from '../utils/SchemaManager';
import JsonPointer from '../utils/JsonPointer';
import { MarkedPipe, JsonPointerEscapePipe } from '../utils/pipes';
export { SchemaManager };
@ -17,21 +16,6 @@ function safeConcat(a, b) {
return res.concat(b);
}
function defaults(target, src) {
var props = Object.keys(src);
var index = -1,
length = props.length;
while (++index < length) {
var key = props[index];
if (target[key] === undefined) {
target[key] = src[key];
}
}
return target;
}
function snapshot(obj) {
if(obj == undefined || typeof(obj) !== 'object') {
return obj;
@ -98,69 +82,7 @@ export function RedocComponent(options) {
export class BaseComponent implements OnInit, OnDestroy {
componentSchema: any = null;
pointer: String;
static joinAllOf(schema: any, opts?: any) {
function merge(into, schemas) {
for (let subSchema of schemas) {
if (opts && opts.omitParent && subSchema.discriminator) continue;
// TODO: add support for merge array schemas
if (typeof subSchema !== 'object') {
let errMessage = `Items of allOf should be Object: ${typeof subSchema} found
${subSchema}`;
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`;
throw new Error(errMessage);
}
if (into.type === 'array') {
console.warn('allOf: subschemas with type array are not supported yet');
}
// TODO: add check if can be merged correctly (no different properties with the same name)
into.type = into.type || subSchema.type;
if (into.type === 'object' && subSchema.properties) {
if (!into.properties) into.properties = {};
Object.assign(into.properties, subSchema.properties);
Object.keys(subSchema.properties).forEach(propName => {
if (!subSchema.properties[propName]._pointer) {
subSchema.properties[propName]._pointer = subSchema._pointer ?
JsonPointer.join(subSchema._pointer, ['properties', propName]) : null;
}
});
}
if (into.type === 'object' && subSchema.required) {
if (!into.required) into.required = [];
into.required.push(...subSchema.required);
}
// don't merge _pointer
subSchema._pointer = null;
defaults(into, subSchema);
}
into.allOf = null;
}
function traverse(obj) {
if (obj == undefined || typeof(obj) !== 'object') {
return;
}
for(var key in obj) {
if (obj.hasOwnProperty(key)) {
traverse(obj[key]);
}
}
if (obj.allOf) {
merge(obj, obj.allOf);
}
}
traverse(schema);
}
dereferencedCache = {};
constructor(public schemaMgr: SchemaManager) {
}
@ -169,7 +91,7 @@ export class BaseComponent implements OnInit, OnDestroy {
* onInit method is run by angular2 after all component inputs are resolved
*/
ngOnInit() {
this.componentSchema = snapshot(this.schemaMgr.byPointer(this.pointer || ''));
this.componentSchema = this.schemaMgr.byPointer(this.pointer || '');
this.prepareModel();
this.init();
}
@ -178,61 +100,6 @@ export class BaseComponent implements OnInit, OnDestroy {
this.destroy();
}
/**
* simple in-place schema dereferencing. Schema is already bundled so no need in global dereferencing.
*/
dereference(schema = Object.assign({}, this.componentSchema)) {
let dereferencedCache = {};
let resolve = (schema) => {
let resolvedRef;
if (schema && schema.$ref) {
resolvedRef = schema.$ref;
let resolved = this.schemaMgr.byPointer(schema.$ref);
let baseName = JsonPointer.baseName(schema.$ref);
if (!dereferencedCache[schema.$ref]) {
// if resolved schema doesn't have title use name from ref
resolved = Object.assign({}, resolved);
resolved._pointer = schema.$ref;
} else {
// for circular referenced save only title and type
resolved = {
title: resolved.title,
type: resolved.type
};
}
dereferencedCache[schema.$ref] = dereferencedCache[schema.$ref] ? dereferencedCache[schema.$ref] + 1 : 1;
resolved.title = resolved.title || baseName;
let keysCount = Object.keys(schema).length;
if ( keysCount > 2 || (keysCount === 2 && !schema.description) ) {
// allow only description field on the same level as $ref because it is
// common pattern over specs in the wild
console.warn(`other properties defined at the same level as $ref at '${this.pointer}'.
They are IGNORRED according to JsonSchema spec`);
}
schema = schema.description ? {
description: schema.description
} : {};
Object.assign(schema, resolved);
}
Object.keys(schema).forEach((key) => {
let value = schema[key];
if (value && typeof value === 'object') {
schema[key] = resolve(value);
}
});
if (resolvedRef) dereferencedCache[resolvedRef] = dereferencedCache[resolvedRef] ? dereferencedCache[resolvedRef] - 1 : 0;
return schema;
};
this.componentSchema = snapshot(resolve(schema));
}
/**
* Used to prepare model based on component schema
* @abstract

View File

@ -0,0 +1,238 @@
'use strict';
import { SchemaNormalizator } from './spec-helper.service';
import {
describe,
it
} from '@angular/core/testing';
import { SchemaManager } from '../utils/SchemaManager';;
describe('Spec Helper', () => {
let schemaMgr:SchemaManager = new SchemaManager();
let normalizer = new SchemaNormalizator(schemaMgr);
describe('Dereference', () => {
beforeAll(done => {
schemaMgr.load('/tests/schemas/base-component-dereference.json').then(
() => done()
);
});
describe('simple dereference', () => {
let resolved;
let pointer;
beforeAll(() => {
pointer = '/paths/test1/get/parameters/0';
resolved = normalizer.normalize(schemaMgr.byPointer(pointer), pointer);
});
it('should not contain $ref property', () => {
expect(resolved.$ref).toBeUndefined();
});
it('should inject Title if not exist based on reference', () => {
resolved.title.should.be.equal('Simple');
});
it('should inject pointer', () => {
resolved._pointer.should.be.equal('#/definitions/Simple');
});
it('should insert correct definition instead of reference', () => {
delete resolved.title;
delete resolved._pointer;
resolved.should.be.deepEqual(schemaMgr.schema.definitions.Simple);
});
});
describe('nested dereference', () => {
let resolved;
beforeAll(() => {
let pointer = '/paths/test2/get/parameters/0';
resolved = normalizer.normalize(schemaMgr.byPointer(pointer), pointer);
});
it('should not touch title if exist', () => {
resolved.title.should.be.equal('NesteTitle');
});
it('should resolve nested schema', () => {
expect(resolved.properties.subschema.$ref).toBeUndefined();
resolved._pointer.should.be.equal('#/definitions/Nested');
resolved.properties.subschema._pointer.should.be.equal('#/definitions/Simple');
resolved.properties.subschema.type.should.be.equal('object');
});
});
describe('array schema dereference', () => {
let resolved;
beforeAll(() => {
let pointer = '/paths/test3/get/parameters/0';
resolved = normalizer.normalize(schemaMgr.byPointer(pointer), pointer);
});
it('should resolve array schema', () => {
console.log(resolved);
expect(resolved.$ref).toBeUndefined();
expect(resolved.items.$ref).toBeUndefined();
resolved.type.should.be.equal('array');
resolved._pointer.should.be.equal('#/definitions/ArrayOfSimple');
resolved.items._pointer.should.be.equal('#/definitions/Simple');
resolved.items.type.should.be.equal('object');
});
});
describe('circular dereference', () => {
let resolved;
beforeAll(() => {
let pointer = '/paths/test4/get/parameters/0';
resolved = normalizer.normalize(schemaMgr.byPointer(pointer), pointer);
});
it('should resolve circular schema', () => {
expect(resolved.$ref).toBeUndefined();
expect(resolved.items.$ref).toBeUndefined();
resolved.type.should.be.equal('array');
resolved._pointer.should.be.equal('#/definitions/Circular');
expect(resolved.items._pointer).toBeUndefined();
resolved.items.title.should.be.equal('Circular');
});
});
describe('$ref with other fields on the same level', () => {
let resolved;
beforeAll(() => {
let pointer = '/paths/test5/get/parameters/0';
resolved = normalizer.normalize(schemaMgr.byPointer(pointer), pointer);
});
it('should skip other fields', () => {
expect(resolved.$ref).toBeUndefined();
expect(resolved.title).toBeDefined();
resolved.title.should.be.equal('Simple');
});
it('should preserve description field', () => {
expect(resolved.$ref).toBeUndefined();
expect(resolved.description).toBeDefined();
resolved.description.should.be.equal('test');
});
});
});
describe('mergeAllOf', () => {
beforeAll((done) => {
schemaMgr.load('tests/schemas/base-component-joinallof.json').then(() => done());
});
describe('Simple allOf merge', () => {
let joined;
beforeAll(() => {
let pointer = '/definitions/SimpleAllOf';
joined = normalizer.normalize(schemaMgr.byPointer(pointer), pointer);
});
it('should remove $allOf field', () => {
expect(joined.allOf).toBeNull();
});
it('should set type object', () => {
joined.type.should.be.equal('object');
});
it('should merge properties', () => {
Object.keys(joined.properties).length.should.be.equal(3);
Object.keys(joined.properties).should.be.deepEqual(['prop1', 'prop2', 'prop3']);
});
it('should merge required', () => {
joined.required.length.should.be.equal(2);
joined.required.should.be.deepEqual(['prop1', 'prop3']);
});
});
describe('AllOf with refrence', () => {
let joined;
beforeAll(() => {
let pointer = '/definitions/AllOfWithRef';
joined = normalizer.normalize(schemaMgr.byPointer(pointer), pointer);
});
it('should remove $allOf field', () => {
expect(joined.allOf).toBeNull();
});
it('should set type object', () => {
joined.type.should.be.equal('object');
});
it('should merge properties', () => {
Object.keys(joined.properties).length.should.be.equal(2);
Object.keys(joined.properties).should.be.deepEqual(['id', 'prop3']);
});
it('should merge required', () => {
joined.required.length.should.be.equal(2);
joined.required.should.be.deepEqual(['id', 'prop3']);
});
});
describe('AllOf with other properties on the allOf level', () => {
let joined;
beforeAll(() => {
let pointer = '/definitions/AllOfWithOther';
joined = normalizer.normalize(schemaMgr.byPointer(pointer), pointer);
});
it('should remove $allOf field', () => {
expect(joined.allOf).toBeNull();
});
it('should set type object', () => {
joined.type.should.be.equal('object');
});
it('should merge properties', () => {
Object.keys(joined.properties).length.should.be.equal(1);
Object.keys(joined.properties).should.be.deepEqual(['id']);
});
it('should merge required', () => {
joined.required.length.should.be.equal(1);
joined.required.should.be.deepEqual(['id']);
});
it('should preserve parent properties', () => {
joined.description.should.be.equal('Test');
joined.readOnly.should.be.equal(true);
});
});
describe('allOf edgecases', () => {
it('should merge properties and required when defined on allOf level', () => {
let pointer = '/definitions/PropertiesOnAllOfLevel';
let joined;
(() => joined = normalizer.normalize(schemaMgr.byPointer(pointer), pointer)).should.not.throw();
Object.keys(joined.properties).length.should.be.equal(3);
});
it('should throw when merging schemas with different types', () => {
let pointer = '/definitions/BadAllOf1';
(() => normalizer.normalize(schemaMgr.byPointer(pointer), pointer)).should.throw();
});
it('should handle nested allOF', () => {
let pointer = '/definitions/NestedAllOf';
let joined;
(() => joined = normalizer.normalize(schemaMgr.byPointer(pointer), pointer)).should.not.throw();
Object.keys(joined.properties).length.should.be.equal(4);
Object.keys(joined.properties).should.be.deepEqual(['prop1', 'prop2', 'prop3', 'prop4']);
joined.required.should.be.deepEqual(['prop1', 'prop3']);
});
});
xdescribe('Merge array allOf', () => {
//emtpy
});
});
});

View File

@ -0,0 +1,196 @@
'use strict';
import { Injectable } from '@angular/core';
import { SchemaManager } from '../utils/SchemaManager';
import { JsonPointer } from '../utils/JsonPointer';
import { defaults } from '../utils/helpers';
interface Reference {
$ref: string;
description: string;
}
interface Schema {
properties: any;
allOf: any;
items: any;
additionalProperties: any;
}
@Injectable()
export class SchemaNormalizator {
_dereferencer:SchemaDereferencer;
constructor(private _schema:any) {
this._dereferencer = new SchemaDereferencer(_schema, this);
}
normalize(schema, ptr) {
if (schema['x-redoc-normalized']) return schema;
let res = SchemaWalker.walk(schema, ptr, (subSchema, ptr) => {
let resolved = this._dereferencer.dereference(subSchema, ptr);
if (resolved.allOf) {
resolved._pointer = resolved._pointer || ptr;
AllOfMerger.merge(resolved, resolved.allOf, {omitParent: true});
}
return resolved;
});
res['x-redoc-normalized'] = true;
return res;
}
}
class SchemaWalker {
static walk(obj:Schema, pointer:string, visitor:Function) {
if (obj == undefined || typeof(obj) !== 'object') {
return;
}
if (obj.properties) {
let ptr = JsonPointer.join(pointer, ['properties']);
SchemaWalker.walkEach(obj.properties, ptr, visitor);
}
if (obj.additionalProperties) {
let ptr = JsonPointer.join(pointer, ['additionalProperties']);
SchemaWalker.walkEach(obj.additionalProperties, ptr, visitor);
}
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;
}
}
return visitor(obj, pointer);
}
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;
}
}
}
class AllOfMerger {
static merge(into, schemas, opts) {
into['x-derived-from'] = [];
for (let i=0; i < schemas.length; i++) {
let subSchema = schemas[i];
into['x-derived-from'].push(subSchema._pointer);
if (opts && opts.omitParent && subSchema.discriminator) continue;
AllOfMerger.checkCanMerge(subSchema, into);
into.type = into.type || subSchema.type;
if (into.type === 'object') {
AllOfMerger.mergeObject(into, subSchema, i);
}
// don't merge _pointer
subSchema._pointer = null;
defaults(into, subSchema);
}
into.allOf = null;
}
private static mergeObject(into, subSchema, allOfNumber) {
if (subSchema.properties) {
if (!into.properties) into.properties = {};
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}`;
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`;
throw new Error(errMessage);
}
if (into.type === 'array') {
console.warn('allOf: subschemas with type array are not supported yet');
}
// TODO: add check if can be merged correctly (no different properties with the same name)
}
}
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];
}
}
class SchemaDereferencer {
private _refCouner = new RefCounter();
constructor(private _spec: SchemaManager, private normalizator: SchemaNormalizator) {
}
dereference(schema: Reference, pointer:string):any {
if (!schema || !schema.$ref) return schema;
window['derefCount'] = window['derefCount'] ? window['derefCount'] + 1 : 1;
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).length;
if ( keysCount > 2 || (keysCount === 2 && !schema.description) ) {
console.warn(`other properties defined at the same level as $ref at '${pointer}'.
They are IGNORRED according to JsonSchema spec`);
resolved.description = resolved.description || schema.description;
}
resolved = this.normalizator.normalize(resolved, $ref);
this._refCouner.exit($ref);
return resolved;
}
}

View File

@ -173,13 +173,11 @@ export class SchemaManager {
let globalDefs = this._schema.definitions || {};
let res = [];
for (let defName of Object.keys(globalDefs)) {
if (!globalDefs[defName].allOf) continue;
let subTypes = globalDefs[defName].allOf;
let idx = subTypes.findIndex((subType) => {
if (subType.$ref === defPointer) return true;
return false;
});
if (!globalDefs[defName].allOf &&
!globalDefs[defName]['x-derived-from']) continue;
let subTypes = globalDefs[defName]['x-derived-from'] ||
globalDefs[defName].allOf.map(subType => subType.$ref);
let idx = subTypes.findIndex(ref => ref === defPointer);
if (idx < 0) continue;
let empty = false;

View File

@ -14,3 +14,18 @@ export function statusCodeType(statusCode) {
}
return res;
}
export function defaults(target, src) {
var props = Object.keys(src);
var index = -1,
length = props.length;
while (++index < length) {
var key = props[index];
if (target[key] === undefined) {
target[key] = src[key];
}
}
return target;
}

View File

@ -27,17 +27,13 @@
"ArrayOfSimple": {
"type": "array",
"items": {
"schema": {
"$ref": "#/definitions/Simple"
}
"$ref": "#/definitions/Simple"
}
},
"Circular": {
"type": "array",
"items": {
"schema": {
"$ref": "#/definitions/Circular"
}
"$ref": "#/definitions/Circular"
}
}
},