mirror of
				https://github.com/Redocly/redoc.git
				synced 2025-10-30 23:37:28 +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,19 +27,15 @@ | ||||||
|     "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" | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     } |  | ||||||
|   }, |   }, | ||||||
|   "paths": { |   "paths": { | ||||||
|     "test1": { |     "test1": { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user