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

247 lines
7.4 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';
export interface Reference {
$ref: string;
description: string;
}
export interface Schema {
properties: any;
allOf: any;
items: any;
additionalProperties: any;
}
export class SchemaNormalizer {
_dereferencer:SchemaDereferencer;
constructor(_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 (opts.childFor) this._dereferencer.visit(opts.childFor);
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);
if (opts.childFor) this._dereferencer.exit(opts.childFor);
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['x-redoc-visited']) {
obj['x-redoc-visited'] = false;
const res = visitor(obj, pointer);
// circular, return only title and description
return { title: res.title, description: res.description };
}
obj['x-redoc-visited'] = true;
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;
}
}
obj['x-redoc-visited'] = false;
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;
}
}
}
export class AllOfMerger {
static merge(into, schemas) {
into['x-derived-from'] = [];
let hadDiscriminator = !!into.discriminator;
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;
}
if (!hadDiscriminator) into.discriminator = null;
delete into.allOf;
}
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];
}
}
export 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;
let $ref = schema.$ref;
let resolved = this._spec.byPointer($ref);
if (!this._refCouner.visited($ref)) {
resolved._pointer = $ref;
} else {
// for circular referenced save only title and type
resolved = {
title: resolved.title,
type: resolved.type
};
}
this._refCouner.visit($ref);
// if resolved schema doesn't have title use name from ref
resolved.title = resolved.title || JsonPointer.baseName($ref);
let keysCount = Object.keys(schema).filter(key => !key.startsWith('x-redoc')).length;
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 = this.normalizator.normalize(resolved, $ref);
this._refCouner.exit($ref);
// schema description should always override $ref description
if (schema.description) {
// make a copy of resolved object with updated description, do not globally override the description
resolved = Object.assign({}, resolved);
resolved.description = schema.description;
}
return resolved;
}
}