mirror of
https://github.com/Redocly/redoc.git
synced 2025-02-12 07:50:34 +03:00
Refactor + perf optimizations
This commit is contained in:
parent
9341d4904e
commit
1e97ea655e
|
@ -59,9 +59,10 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
|
||||||
setTimeout( ()=> {
|
setTimeout( ()=> {
|
||||||
let $element = compRef.location.nativeElement;
|
let $element = compRef.location.nativeElement;
|
||||||
|
|
||||||
// skip caching view with tabs inside (discriminator) as it needs attached controller
|
// skip caching view with tabs inside (discriminator)
|
||||||
// FIXME: get rid of dependency on selector
|
// as it needs attached controller
|
||||||
if ($element.querySelector('.discriminator-wrap')) {
|
if (compRef.instance.hasDiscriminator) {
|
||||||
|
this._loadAfterSelf();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
insertAfter($element.cloneNode(true), this.elementRef.nativeElement);
|
insertAfter($element.cloneNode(true), this.elementRef.nativeElement);
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
</span>
|
</span>
|
||||||
<table *ngIf="!schema.isTrivial" class="params-wrap" [ngClass]="{'params-array': schema._isArray}">
|
<table *ngIf="!schema.isTrivial" class="params-wrap" [ngClass]="{'params-array': schema._isArray}">
|
||||||
<!-- <caption> {{_displayType}} </caption> -->
|
<!-- <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,
|
<tr class="param" [ngClass]="{'last': last,
|
||||||
'discriminator': prop.isDiscriminator && !derivedEmtpy,
|
'discriminator': prop.isDiscriminator && !derivedEmtpy,
|
||||||
'complex': prop._pointer,
|
'complex': prop._pointer,
|
||||||
|
@ -40,11 +40,11 @@
|
||||||
<span *ngFor="let enumItem of prop.enum" class="enum-value {{enumItem.type}}"> {{enumItem.val | json}} </span>
|
<span *ngFor="let enumItem of prop.enum" class="enum-value {{enumItem.type}}"> {{enumItem.val | json}} </span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="discriminator-info" *ngIf="prop.isDiscriminator">
|
||||||
<span>This field value determines the exact schema:</span>
|
<span>This field value determines the exact schema:</span>
|
||||||
<drop-down (change)="selectDerived($event)">
|
<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>
|
[value]="i">{{derived.name}}</option>
|
||||||
</drop-down>
|
</drop-down>
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,9 +58,9 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</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">
|
<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"
|
<json-schema class="discriminator-part" *ngIf="!derived.empty" [childFor]="pointer"
|
||||||
pointer="{{derived.$ref}}" [final]="derived.final" [isRequestSchema]="isRequestSchema">
|
pointer="{{derived.$ref}}" [final]="derived.final" [isRequestSchema]="isRequestSchema">
|
||||||
</json-schema>
|
</json-schema>
|
||||||
|
|
|
@ -56,7 +56,7 @@ describe('Redoc components', () => {
|
||||||
test: {}
|
test: {}
|
||||||
}};
|
}};
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
component.schema.properties[0]._displayType.should.be.equal('< * >');
|
component.schema._properties[0]._displayType.should.be.equal('< * >');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ElementRef, Input } from '@angular/core';
|
||||||
import { RedocComponent, BaseComponent, SchemaManager } from '../base';
|
import { RedocComponent, BaseComponent, SchemaManager } from '../base';
|
||||||
import { DropDown } from '../../shared/components/index';
|
import { DropDown } from '../../shared/components/index';
|
||||||
import JsonPointer from '../../utils/JsonPointer';
|
import JsonPointer from '../../utils/JsonPointer';
|
||||||
|
import { SchemaNormalizator } from '../../services/spec-helper.service';
|
||||||
|
|
||||||
@RedocComponent({
|
@RedocComponent({
|
||||||
selector: 'json-schema',
|
selector: 'json-schema',
|
||||||
|
@ -17,11 +18,13 @@ export class JsonSchema extends BaseComponent {
|
||||||
$element: any;
|
$element: any;
|
||||||
schema: any;
|
schema: any;
|
||||||
derivedEmtpy: boolean;
|
derivedEmtpy: boolean;
|
||||||
|
hasDiscriminator: boolean = false;
|
||||||
@Input() isArray: boolean;
|
@Input() isArray: boolean;
|
||||||
@Input() final: boolean = false;
|
@Input() final: boolean = false;
|
||||||
@Input() nestOdd: boolean;
|
@Input() nestOdd: boolean;
|
||||||
@Input() childFor: string;
|
@Input() childFor: string;
|
||||||
@Input() isRequestSchema: boolean;
|
@Input() isRequestSchema: boolean;
|
||||||
|
normalizer: SchemaNormalizator;
|
||||||
|
|
||||||
static injectPropertyData(propertySchema, propertyName, propPointer, hostPointer?) {
|
static injectPropertyData(propertySchema, propertyName, propPointer, hostPointer?) {
|
||||||
propertySchema = Object.assign({}, propertySchema);
|
propertySchema = Object.assign({}, propertySchema);
|
||||||
|
@ -35,12 +38,13 @@ export class JsonSchema extends BaseComponent {
|
||||||
constructor(schemaMgr:SchemaManager, elementRef:ElementRef) {
|
constructor(schemaMgr:SchemaManager, elementRef:ElementRef) {
|
||||||
super(schemaMgr);
|
super(schemaMgr);
|
||||||
this.$element = elementRef.nativeElement;
|
this.$element = elementRef.nativeElement;
|
||||||
|
this.normalizer = new SchemaNormalizator(schemaMgr);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectDerived(subClassIdx) {
|
selectDerived(subClassIdx) {
|
||||||
let subClass = this.schema.derived[subClassIdx];
|
let subClass = this.schema._derived[subClassIdx];
|
||||||
if (!subClass || subClass.active) return;
|
if (!subClass || subClass.active) return;
|
||||||
this.schema.derived.forEach((subSchema) => {
|
this.schema._derived.forEach((subSchema) => {
|
||||||
subSchema.active = false;
|
subSchema.active = false;
|
||||||
});
|
});
|
||||||
subClass.active = true;
|
subClass.active = true;
|
||||||
|
@ -65,14 +69,18 @@ export class JsonSchema extends BaseComponent {
|
||||||
if (!this.componentSchema) {
|
if (!this.componentSchema) {
|
||||||
throw new Error(`Can't load component schema at ${this.pointer}`);
|
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;
|
let schema = this.componentSchema;
|
||||||
BaseComponent.joinAllOf(schema, {omitParent: true});
|
|
||||||
this.schema = schema = this.unwrapArray(schema);
|
this.schema = schema = this.unwrapArray(schema);
|
||||||
runInjectors(schema, schema, schema._pointer || this.pointer, this.pointer);
|
runInjectors(schema, schema, schema._pointer || this.pointer, this.pointer);
|
||||||
|
|
||||||
schema.derived = schema.derived || [];
|
this.schema._derived = this.schema._derived || [];
|
||||||
|
|
||||||
if (!schema.isTrivial) {
|
if (!schema.isTrivial) {
|
||||||
this.prepareObjectPropertiesData(schema);
|
this.prepareObjectPropertiesData(schema);
|
||||||
|
@ -82,16 +90,17 @@ export class JsonSchema extends BaseComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
initDerived() {
|
initDerived() {
|
||||||
if (!this.schema.derived.length) return;
|
if (!this.schema._derived.length) return;
|
||||||
let enumArr = this.schema.properties[this.schema.properties.length - 1].enum;
|
this.hasDiscriminator = true;
|
||||||
|
let enumArr = this.schema._properties[this.schema._properties.length - 1].enum;
|
||||||
if (enumArr) {
|
if (enumArr) {
|
||||||
let enumOrder = {};
|
let enumOrder = {};
|
||||||
enumArr.forEach((enumItem, idx) => {
|
enumArr.forEach((enumItem, idx) => {
|
||||||
enumOrder[enumItem.val] = idx;
|
enumOrder[enumItem.val] = idx;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.schema.derived.sort((a, b) => {
|
this.schema._derived.sort((a, b) => {
|
||||||
return enumOrder[a.name] > enumOrder[b.name];
|
return enumOrder[a.name] > enumOrder[b.name] ? 1 : -1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.selectDerived(0);
|
this.selectDerived(0);
|
||||||
|
@ -138,7 +147,7 @@ export class JsonSchema extends BaseComponent {
|
||||||
if (this.isRequestSchema) {
|
if (this.isRequestSchema) {
|
||||||
props = props.filter(prop => !prop.readOnly);
|
props = props.filter(prop => !prop.readOnly);
|
||||||
}
|
}
|
||||||
schema.properties = props;
|
schema._properties = props;
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareAdditionalProperties(schema) {
|
prepareAdditionalProperties(schema) {
|
||||||
|
@ -165,7 +174,7 @@ const injectors = {
|
||||||
discriminator: {
|
discriminator: {
|
||||||
check: (propertySchema) => propertySchema.discriminator,
|
check: (propertySchema) => propertySchema.discriminator,
|
||||||
inject: (injectTo, propertySchema = injectTo, pointer) => {
|
inject: (injectTo, propertySchema = injectTo, pointer) => {
|
||||||
injectTo.derived = SchemaManager.instance().findDerivedDefinitions(pointer);
|
injectTo._derived = SchemaManager.instance().findDerivedDefinitions(pointer);
|
||||||
injectTo.discriminator = propertySchema.discriminator;
|
injectTo.discriminator = propertySchema.discriminator;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
import { provide, enableProdMode, ElementRef,
|
import { provide, enableProdMode, ElementRef,
|
||||||
ComponentRef, AfterViewInit } from '@angular/core';
|
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 { bootstrap } from '@angular/platform-browser-dynamic';
|
||||||
import { BrowserDomAdapter } from '@angular/platform-browser/src/browser/browser_adapter';
|
import { BrowserDomAdapter } from '@angular/platform-browser/src/browser/browser_adapter';
|
||||||
import { RedocComponent, BaseComponent } from '../base';
|
import { RedocComponent, BaseComponent } from '../base';
|
||||||
|
@ -60,7 +63,8 @@ export class Redoc extends BaseComponent implements AfterViewInit {
|
||||||
optionsService.options = options;
|
optionsService.options = options;
|
||||||
optionsService.options.specUrl = optionsService.options.specUrl || specUrl;
|
optionsService.options.specUrl = optionsService.options.specUrl || specUrl;
|
||||||
var providers = [
|
var providers = [
|
||||||
provide(OptionsService, {useValue: optionsService})
|
provide(OptionsService, {useValue: optionsService}),
|
||||||
|
provide(CompilerConfig, {useValue: new CompilerConfig({genDebugInfo: false, logBindingUpdate: false})})
|
||||||
];
|
];
|
||||||
|
|
||||||
if (Redoc.appRef) {
|
if (Redoc.appRef) {
|
||||||
|
@ -71,6 +75,8 @@ export class Redoc extends BaseComponent implements AfterViewInit {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!_modeLocked && !optionsService.options.debugMode) {
|
if (!_modeLocked && !optionsService.options.debugMode) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
|
compilerProd();
|
||||||
|
browserProd();
|
||||||
_modeLocked = true;
|
_modeLocked = true;
|
||||||
}
|
}
|
||||||
return bootstrap(Redoc, providers);
|
return bootstrap(Redoc, providers);
|
||||||
|
|
|
@ -6,6 +6,7 @@ import * as OpenAPISampler from 'openapi-sampler';
|
||||||
|
|
||||||
import { RedocComponent, BaseComponent, SchemaManager } from '../base';
|
import { RedocComponent, BaseComponent, SchemaManager } from '../base';
|
||||||
import { JsonFormatter } from '../../utils/JsonFormatterPipe';
|
import { JsonFormatter } from '../../utils/JsonFormatterPipe';
|
||||||
|
import { SchemaNormalizator } from '../../services/spec-helper.service';
|
||||||
|
|
||||||
@RedocComponent({
|
@RedocComponent({
|
||||||
selector: 'schema-sample',
|
selector: 'schema-sample',
|
||||||
|
@ -17,12 +18,17 @@ export class SchemaSample extends BaseComponent {
|
||||||
element: any;
|
element: any;
|
||||||
data: any;
|
data: any;
|
||||||
@Input() skipReadOnly:boolean;
|
@Input() skipReadOnly:boolean;
|
||||||
|
|
||||||
|
private _normalizer:SchemaNormalizator;
|
||||||
|
|
||||||
constructor(schemaMgr:SchemaManager, elementRef:ElementRef) {
|
constructor(schemaMgr:SchemaManager, elementRef:ElementRef) {
|
||||||
super(schemaMgr);
|
super(schemaMgr);
|
||||||
this.element = elementRef.nativeElement;
|
this.element = elementRef.nativeElement;
|
||||||
|
this._normalizer = new SchemaNormalizator(schemaMgr);
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
this.bindEvents();
|
||||||
this.data = {};
|
this.data = {};
|
||||||
|
|
||||||
let base:any = {};
|
let base:any = {};
|
||||||
|
@ -37,7 +43,10 @@ export class SchemaSample extends BaseComponent {
|
||||||
if (base.examples && base.examples['application/json']) {
|
if (base.examples && base.examples['application/json']) {
|
||||||
sample = base.examples['application/json'];
|
sample = base.examples['application/json'];
|
||||||
} else {
|
} else {
|
||||||
this.dereference(this.componentSchema);
|
this.componentSchema = this._normalizer.normalize(this.componentSchema, this.pointer);
|
||||||
|
if (this.fromCache()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
sample = OpenAPISampler.sample(this.componentSchema, {
|
sample = OpenAPISampler.sample(this.componentSchema, {
|
||||||
skipReadOnly: this.skipReadOnly
|
skipReadOnly: this.skipReadOnly
|
||||||
|
@ -46,10 +55,30 @@ export class SchemaSample extends BaseComponent {
|
||||||
// no sample available
|
// no sample available
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.cache(sample);
|
||||||
this.data.sample = 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) => {
|
this.element.addEventListener('click', (event) => {
|
||||||
var collapsed, target = event.target;
|
var collapsed, target = event.target;
|
||||||
if (event.target.className === 'collapser') {
|
if (event.target.className === 'collapser') {
|
||||||
|
|
|
@ -39,253 +39,5 @@ describe('Redoc components', () => {
|
||||||
component.prepareModel.and.callThrough();
|
component.prepareModel.and.callThrough();
|
||||||
component.init.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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
|
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { CORE_DIRECTIVES, JsonPipe, AsyncPipe } from '@angular/common';
|
import { CORE_DIRECTIVES, JsonPipe, AsyncPipe } from '@angular/common';
|
||||||
import { SchemaManager } from '../utils/SchemaManager';
|
import { SchemaManager } from '../utils/SchemaManager';
|
||||||
import JsonPointer from '../utils/JsonPointer';
|
|
||||||
import { MarkedPipe, JsonPointerEscapePipe } from '../utils/pipes';
|
import { MarkedPipe, JsonPointerEscapePipe } from '../utils/pipes';
|
||||||
|
|
||||||
export { SchemaManager };
|
export { SchemaManager };
|
||||||
|
@ -17,21 +16,6 @@ function safeConcat(a, b) {
|
||||||
return res.concat(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) {
|
function snapshot(obj) {
|
||||||
if(obj == undefined || typeof(obj) !== 'object') {
|
if(obj == undefined || typeof(obj) !== 'object') {
|
||||||
return obj;
|
return obj;
|
||||||
|
@ -98,69 +82,7 @@ export function RedocComponent(options) {
|
||||||
export class BaseComponent implements OnInit, OnDestroy {
|
export class BaseComponent implements OnInit, OnDestroy {
|
||||||
componentSchema: any = null;
|
componentSchema: any = null;
|
||||||
pointer: String;
|
pointer: String;
|
||||||
|
dereferencedCache = {};
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(public schemaMgr: SchemaManager) {
|
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
|
* onInit method is run by angular2 after all component inputs are resolved
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.componentSchema = snapshot(this.schemaMgr.byPointer(this.pointer || ''));
|
this.componentSchema = this.schemaMgr.byPointer(this.pointer || '');
|
||||||
this.prepareModel();
|
this.prepareModel();
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
@ -178,61 +100,6 @@ export class BaseComponent implements OnInit, OnDestroy {
|
||||||
this.destroy();
|
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
|
* Used to prepare model based on component schema
|
||||||
* @abstract
|
* @abstract
|
||||||
|
|
238
lib/services/spec-helper.service.spec.ts
Normal file
238
lib/services/spec-helper.service.spec.ts
Normal 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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
196
lib/services/spec-helper.service.ts
Normal file
196
lib/services/spec-helper.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -173,13 +173,11 @@ export class SchemaManager {
|
||||||
let globalDefs = this._schema.definitions || {};
|
let globalDefs = this._schema.definitions || {};
|
||||||
let res = [];
|
let res = [];
|
||||||
for (let defName of Object.keys(globalDefs)) {
|
for (let defName of Object.keys(globalDefs)) {
|
||||||
if (!globalDefs[defName].allOf) continue;
|
if (!globalDefs[defName].allOf &&
|
||||||
|
!globalDefs[defName]['x-derived-from']) continue;
|
||||||
let subTypes = globalDefs[defName].allOf;
|
let subTypes = globalDefs[defName]['x-derived-from'] ||
|
||||||
let idx = subTypes.findIndex((subType) => {
|
globalDefs[defName].allOf.map(subType => subType.$ref);
|
||||||
if (subType.$ref === defPointer) return true;
|
let idx = subTypes.findIndex(ref => ref === defPointer);
|
||||||
return false;
|
|
||||||
});
|
|
||||||
if (idx < 0) continue;
|
if (idx < 0) continue;
|
||||||
|
|
||||||
let empty = false;
|
let empty = false;
|
||||||
|
|
|
@ -14,3 +14,18 @@ export function statusCodeType(statusCode) {
|
||||||
}
|
}
|
||||||
return res;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -27,17 +27,13 @@
|
||||||
"ArrayOfSimple": {
|
"ArrayOfSimple": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"schema": {
|
"$ref": "#/definitions/Simple"
|
||||||
"$ref": "#/definitions/Simple"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Circular": {
|
"Circular": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"schema": {
|
"$ref": "#/definitions/Circular"
|
||||||
"$ref": "#/definitions/Circular"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue
Block a user