mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-27 19:13:44 +03:00
227 lines
6.6 KiB
TypeScript
227 lines
6.6 KiB
TypeScript
'use strict';
|
|
import { Injectable } from '@angular/core';
|
|
import { SpecManager } from '../utils/spec-manager';
|
|
import { JsonPointer } from '../utils/JsonPointer';
|
|
import { defaults } from '../utils/helpers';
|
|
import { WarningsService } from './warnings.service';
|
|
|
|
interface Reference {
|
|
$ref: string;
|
|
description: string;
|
|
}
|
|
|
|
interface Schema {
|
|
properties: any;
|
|
allOf: any;
|
|
items: any;
|
|
additionalProperties: any;
|
|
}
|
|
|
|
@Injectable()
|
|
export class SchemaNormalizer {
|
|
_dereferencer:SchemaDereferencer;
|
|
constructor(private _schema:any) {
|
|
this._dereferencer = new SchemaDereferencer(_schema, this);
|
|
}
|
|
normalize(schema, ptr, opts:any ={}) {
|
|
let hasPtr = !!schema.$ref;
|
|
if (opts.resolved && !hasPtr) this._dereferencer.visit(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;
|
|
resolved = Object.assign({}, resolved);
|
|
AllOfMerger.merge(resolved, resolved.allOf);
|
|
}
|
|
return resolved;
|
|
});
|
|
if (opts.resolved && !hasPtr) this._dereferencer.exit(ptr);
|
|
res['x-redoc-normalized'] = true;
|
|
return res;
|
|
}
|
|
|
|
reset() {
|
|
this._dereferencer.reset();
|
|
}
|
|
}
|
|
|
|
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']);
|
|
if (Array.isArray(obj.additionalProperties)) {
|
|
SchemaWalker.walkEach(obj.additionalProperties, ptr, visitor);
|
|
} else {
|
|
let res = SchemaWalker.walk(obj.additionalProperties, ptr, visitor);
|
|
if (res) obj.additionalProperties = res;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
into['x-derived-from'] = [];
|
|
for (let i=0; i < schemas.length; i++) {
|
|
let subSchema = schemas[i];
|
|
into['x-derived-from'].push(subSchema._pointer);
|
|
|
|
AllOfMerger.checkCanMerge(subSchema, into);
|
|
|
|
into.type = into.type || subSchema.type;
|
|
if (into.type === 'object') {
|
|
AllOfMerger.mergeObject(into, subSchema, i);
|
|
}
|
|
// don't merge _pointer
|
|
let tmpPtr = subSchema._pointer;
|
|
subSchema._pointer = null;
|
|
defaults(into, subSchema);
|
|
subSchema._pointer = tmpPtr;
|
|
}
|
|
into.allOf = null;
|
|
}
|
|
|
|
private static mergeObject(into, subSchema, allOfNumber) {
|
|
if (subSchema.properties) {
|
|
into.properties = Object.assign({}, 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} at "#${into._pointer}"`;
|
|
throw new Error(errMessage);
|
|
}
|
|
|
|
if (into.type && subSchema.type && into.type !== subSchema.type) {
|
|
let errMessage = `allOf merging error: schemas with different types can't be merged: ` +
|
|
`"${into.type}" and "${subSchema.type}" at "#${into._pointer}"`;
|
|
throw new Error(errMessage);
|
|
}
|
|
|
|
if (into.type === 'array') {
|
|
WarningsService.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)
|
|
// TODO: merge properties
|
|
}
|
|
}
|
|
|
|
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: SpecManager, private normalizator: SchemaNormalizer) {
|
|
}
|
|
reset() {
|
|
this._refCouner.reset();
|
|
}
|
|
|
|
visit($ref) {
|
|
this._refCouner.visit($ref);
|
|
}
|
|
|
|
exit($ref) {
|
|
this._refCouner.exit($ref);
|
|
}
|
|
|
|
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) ) {
|
|
WarningsService.warn(`Other properties are defined at the same level as $ref at "#${pointer}". ` +
|
|
'They are IGNORED according to the JsonSchema spec');
|
|
resolved.description = resolved.description || schema.description;
|
|
}
|
|
|
|
resolved = this.normalizator.normalize(resolved, $ref);
|
|
this._refCouner.exit($ref);
|
|
return resolved;
|
|
}
|
|
}
|