Merge commit 'afa5511973ad01c8f86d11bd080e4ecac2161a65' into releases

This commit is contained in:
RedocBot 2017-03-09 22:59:46 +00:00 committed by travis@localhost
commit 7a81326474
16 changed files with 174 additions and 38 deletions

View File

@ -144,9 +144,9 @@ ReDoc makes use of the following [vendor extensions](http://swagger.io/specifica
## Advanced usage ## Advanced usage
Instead of adding `spec-url` attribute to the `<redoc>` element you can initialize ReDoc via globally exposed `Redoc` object: Instead of adding `spec-url` attribute to the `<redoc>` element you can initialize ReDoc via globally exposed `Redoc` object:
```js ```js
Redoc.init(specUrl, options) Redoc.init(specOrSpecUrl, options)
``` ```
`specOrSpecUrl` is either JSON object with specification or an URL to the spec in `JSON` or `YAML` format.
`options` is javascript object with camel-cased version of `<redoc>` tag attribute names as the keys, e.g.: `options` is javascript object with camel-cased version of `<redoc>` tag attribute names as the keys, e.g.:
```js ```js
Redoc.init('http://petstore.swagger.io/v2/swagger.json', { Redoc.init('http://petstore.swagger.io/v2/swagger.json', {

View File

@ -91,8 +91,8 @@ export class JsonSchema extends BaseSearchableComponent implements OnInit {
}).sort((a, b) => { }).sort((a, b) => {
return enumOrder[a.name] > enumOrder[b.name] ? 1 : -1; return enumOrder[a.name] > enumOrder[b.name] ? 1 : -1;
}); });
this.descendants.forEach((d, idx) => d.idx = idx);
} }
this.descendants.forEach((d, idx) => d.idx = idx);
this.selectDescendantByIdx(0); this.selectDescendantByIdx(0);
} }

View File

@ -15,7 +15,7 @@ import { BaseComponent } from '../base';
import * as detectScollParent from 'scrollparent'; import * as detectScollParent from 'scrollparent';
import { SpecManager } from '../../utils/spec-manager'; import { SpecManager } from '../../utils/spec-manager';
import { SearchService, OptionsService, Hash, AppStateService, SchemaHelper } from '../../services/'; import { SearchService, OptionsService, Options, Hash, AppStateService, SchemaHelper } from '../../services/';
import { LazyTasksService } from '../../shared/components/LazyFor/lazy-for'; import { LazyTasksService } from '../../shared/components/LazyFor/lazy-for';
@Component({ @Component({
@ -29,7 +29,7 @@ export class Redoc extends BaseComponent implements OnInit {
error: any; error: any;
specLoaded: boolean; specLoaded: boolean;
options: any; options: Options;
loadingProgress: number; loadingProgress: number;
@ -84,7 +84,8 @@ export class Redoc extends BaseComponent implements OnInit {
} }
load() { load() {
this.specMgr.load(this.options.specUrl).catch(err => { // bunlde spec directly if passsed or load by URL
this.specMgr.load(this.options.spec || this.options.specUrl).catch(err => {
throw err; throw err;
}); });

View File

@ -5,6 +5,7 @@ import { enableProdMode } from '@angular/core';
import { Redoc } from './components/index'; import { Redoc } from './components/index';
import { BrowserDomAdapter as DOM } from './utils/browser-adapter'; import { BrowserDomAdapter as DOM } from './utils/browser-adapter';
import { disableDebugTools } from '@angular/platform-browser'; import { disableDebugTools } from '@angular/platform-browser';
import { isString } from './utils/helpers';
var bootstrapRedoc; var bootstrapRedoc;
if (AOT) { if (AOT) {
@ -21,13 +22,16 @@ if (IS_PRODUCTION) {
export const version = LIB_VERSION; export const version = LIB_VERSION;
var moduleRef; var moduleRef;
export function init(specUrl:string, options:any = {}) { export function init(specUrlOrSpec:string|any, options:any = {}) {
if (moduleRef) { if (moduleRef) {
destroy(); destroy();
} }
Redoc._preOptions = options; Redoc._preOptions = options;
options.specUrl = options.specUrl || specUrl; options.specUrl = options.specUrl || (isString(specUrlOrSpec) ? specUrlOrSpec : '');
if (!isString(specUrlOrSpec)) {
options.spec = specUrlOrSpec;
}
return bootstrapRedoc() return bootstrapRedoc()
.then(appRef => { .then(appRef => {
moduleRef = appRef; moduleRef = appRef;

View File

@ -141,7 +141,7 @@ export class MenuService {
onHashChange(hash?: string) { onHashChange(hash?: string) {
if (hash == undefined) return; if (hash == undefined) return;
let activated = this.activateByHash(hash); let activated = this.activateByHash(hash);
if (!this.tasks.empty) { if (!this.tasks.processed) {
this.tasks.start(this.activeIdx, this); this.tasks.start(this.activeIdx, this);
this.scrollService.setStickElement(this.getCurrentEl()); this.scrollService.setStickElement(this.getCurrentEl());
if (activated) this.scrollToActive(); if (activated) this.scrollToActive();

View File

@ -19,7 +19,7 @@ const OPTION_NAMES = new Set([
'requiredPropsFirst' 'requiredPropsFirst'
]); ]);
interface Options { export interface Options {
scrollYOffset?: any; scrollYOffset?: any;
disableLazySchemas?: boolean; disableLazySchemas?: boolean;
specUrl?: string; specUrl?: string;
@ -29,6 +29,7 @@ interface Options {
expandResponses?: Set<string> | 'all'; expandResponses?: Set<string> | 'all';
$scrollParent?: HTMLElement | Window; $scrollParent?: HTMLElement | Window;
requiredPropsFirst?: boolean; requiredPropsFirst?: boolean;
spec?: any;
} }
@Injectable() @Injectable()

View File

@ -99,6 +99,7 @@ class SchemaWalker {
export class AllOfMerger { export class AllOfMerger {
static merge(into, schemas) { static merge(into, schemas) {
into['x-derived-from'] = []; into['x-derived-from'] = [];
let hadDiscriminator = !!into.discriminator;
for (let i=0; i < schemas.length; i++) { for (let i=0; i < schemas.length; i++) {
let subSchema = schemas[i]; let subSchema = schemas[i];
into['x-derived-from'].push(subSchema._pointer); into['x-derived-from'].push(subSchema._pointer);
@ -115,7 +116,7 @@ export class AllOfMerger {
defaults(into, subSchema); defaults(into, subSchema);
subSchema._pointer = tmpPtr; subSchema._pointer = tmpPtr;
} }
into.discriminator = null; if (!hadDiscriminator) into.discriminator = null;
into.allOf = null; into.allOf = null;
} }
@ -214,7 +215,8 @@ class SchemaDereferencer {
// if resolved schema doesn't have title use name from ref // if resolved schema doesn't have title use name from ref
resolved.title = resolved.title || JsonPointer.baseName($ref); resolved.title = resolved.title || JsonPointer.baseName($ref);
let keysCount = Object.keys(schema).length; let keysCount = Object.keys(schema).filter(key => !key.startsWith('x-redoc')).length;
if ( keysCount > 2 || (keysCount === 2 && !schema.description) ) { if ( keysCount > 2 || (keysCount === 2 && !schema.description) ) {
WarningsService.warn(`Other properties are defined at the same level as $ref at "#${pointer}". ` + WarningsService.warn(`Other properties are defined at the same level as $ref at "#${pointer}". ` +
'They are IGNORED according to the JsonSchema spec'); 'They are IGNORED according to the JsonSchema spec');

View File

@ -186,10 +186,13 @@ export class SearchService {
let title = name; let title = name;
schema = this.normalizer.normalize(schema, schema._pointer || absolutePointer, { childFor: parent }); schema = this.normalizer.normalize(schema, schema._pointer || absolutePointer, { childFor: parent });
// prevent endless discriminator recursion
if (schema._pointer && schema._pointer === parent) return;
let body = schema.description; // TODO: defaults, examples, etc... let body = schema.description; // TODO: defaults, examples, etc...
if (schema.type === 'array') { if (schema.type === 'array') {
this.indexSchema(schema.items, title, JsonPointer.join(absolutePointer, ['items']), menuPointer); this.indexSchema(schema.items, title, JsonPointer.join(absolutePointer, ['items']), menuPointer, parent);
return; return;
} }
@ -205,20 +208,18 @@ export class SearchService {
body += ' ' + schema.enum.join(' '); body += ' ' + schema.enum.join(' ');
} }
if (!parent) {
// redoc doesn't display top level descriptions and titles
this.index({ this.index({
pointer: absolutePointer, pointer: absolutePointer,
menuId: menuPointer, menuId: menuPointer,
title, title,
body body
}); });
}
if (schema.properties) { if (schema.properties) {
Object.keys(schema.properties).forEach(propName => { Object.keys(schema.properties).forEach(propName => {
let propPtr = JsonPointer.join(absolutePointer, ['properties', propName]); let propPtr = JsonPointer.join(absolutePointer, ['properties', propName]);
this.indexSchema(schema.properties[propName], propName, propPtr, menuPointer); let prop:SwaggerSchema = schema.properties[propName];
this.indexSchema(prop, propName, propPtr, menuPointer, parent);
}); });
} }
} }

View File

@ -27,7 +27,7 @@ export class CopyButton implements OnInit {
onClick() { onClick() {
let copied; let copied;
if (this.copyText) { if (this.copyText) {
copied = Clipboard.copyCustom(JSON.stringify(this.copyText)); copied = Clipboard.copyCustom(JSON.stringify(this.copyText, null, 2));
} else { } else {
copied = Clipboard.copyElement(this.copyElement); copied = Clipboard.copyElement(this.copyElement);
} }

View File

@ -31,6 +31,7 @@ export class LazyTasksService {
private _tasks = []; private _tasks = [];
private _current: number = 0; private _current: number = 0;
private _syncCount: number = 0; private _syncCount: number = 0;
private _emptyProcessed = false;
private menuService; private menuService;
public loadProgress = new BehaviorSubject<number>(0); public loadProgress = new BehaviorSubject<number>(0);
@ -38,8 +39,10 @@ export class LazyTasksService {
constructor(public optionsService: OptionsService) { constructor(public optionsService: OptionsService) {
} }
get empty() { get processed() {
return this._current === this._tasks.length; let res = this._tasks.length && (this._current >= this._tasks.length) || this._emptyProcessed;
if (!this._tasks.length) this._emptyProcessed = true;
return res;
} }
set syncCount(n: number) { set syncCount(n: number) {
@ -97,10 +100,17 @@ export class LazyTasksService {
} else { } else {
this.sortTasks(idx); this.sortTasks(idx);
} }
syncCount = Math.min(syncCount, this._tasks.length);
if (this.allSync) syncCount = this._tasks.length; if (this.allSync) syncCount = this._tasks.length;
for (var i = this._current; i < syncCount; i++) { for (var i = this._current; i < syncCount; i++) {
this.nextTaskSync(); this.nextTaskSync();
} }
if (!this._tasks.length) {
this.loadProgress.next(100);
return;
}
this.nextTask(); this.nextTask();
} }
} }

View File

@ -8,7 +8,7 @@ export function stringify(obj:any) {
return JSON.stringify(obj); return JSON.stringify(obj);
} }
export function isString(str:any) { export function isString(str:any):str is String {
return typeof str === 'string'; return typeof str === 'string';
} }

View File

@ -9,6 +9,7 @@ import { MdRenderer } from './md-renderer';
import { SwaggerOperation, SwaggerParameter } from './swagger-typings'; import { SwaggerOperation, SwaggerParameter } from './swagger-typings';
import { snapshot } from './helpers'; import { snapshot } from './helpers';
import { WarningsService } from '../services/warnings.service';
function getDiscriminator(obj) { function getDiscriminator(obj) {
return obj.discriminator || obj['x-extendedDiscriminator']; return obj.discriminator || obj['x-extendedDiscriminator'];
@ -181,7 +182,37 @@ export class SpecManager {
let globalDefs = this._schema.definitions || {}; let globalDefs = this._schema.definitions || {};
let res:DescendantInfo[] = []; let res:DescendantInfo[] = [];
// from the spec: When used, the value MUST be the name of this schema or any schema that inherits it.
// but most of people use it as an abstract class so here is workaround to allow using it other way
// check if parent definition name is in the enum of possible values
if (definition.discriminator) {
let prop = definition.properties[definition.discriminator];
if (prop.enum && prop.enum.indexOf(JsonPointer.baseName(defPointer)) > -1) {
res.push({
name: JsonPointer.baseName(defPointer),
$ref: defPointer
});
}
}
let extendedDiscriminatorProp = definition['x-extendedDiscriminator']; let extendedDiscriminatorProp = definition['x-extendedDiscriminator'];
let pointers;
if (definition['x-derived-from']) {
// support inherited discriminator o_O
let derivedDiscriminator = definition['x-derived-from'].filter(ptr => {
if (!ptr) return false;
let def = this.byPointer(ptr);
return def && def.discriminator;
});
pointers = [defPointer, ...derivedDiscriminator];
} else {
pointers = [defPointer];
}
for (let defName of Object.keys(globalDefs)) { for (let defName of Object.keys(globalDefs)) {
let def = globalDefs[defName]; let def = globalDefs[defName];
if (!def.allOf && if (!def.allOf &&
@ -189,12 +220,6 @@ export class SpecManager {
let subTypes = def['x-derived-from'] || let subTypes = def['x-derived-from'] ||
def.allOf.map(subType => subType._pointer || subType.$ref); def.allOf.map(subType => subType._pointer || subType.$ref);
let pointers;
if (definition['x-derived-from']) {
pointers = [defPointer, ...definition['x-derived-from']];
} else {
pointers = [defPointer];
}
let idx = -1; let idx = -1;
for (let ptr of pointers) { for (let ptr of pointers) {
@ -204,13 +229,24 @@ export class SpecManager {
if (idx < 0) continue; if (idx < 0) continue;
let derivedName = defName; let derivedName;
if (extendedDiscriminatorProp) { if (extendedDiscriminatorProp) {
let subDefs = def.allOf || [];
for (let def of subDefs) {
let prop = def.properties && def.properties[extendedDiscriminatorProp]; let prop = def.properties && def.properties[extendedDiscriminatorProp];
if (prop && prop.enum && prop.enum.length === 1) { if (prop && prop.enum && prop.enum.length === 1) {
derivedName = prop.enum[0]; derivedName = prop.enum[0];
break;
} }
} }
if (derivedName == undefined) {
WarningsService.warn(`Incorrect usage of x-extendedDiscriminator at ${defPointer}: `
+ `can't find corresponding enum with single value in definition "${defName}"`);
continue;
}
} else {
derivedName = defName;
}
res.push({name: derivedName, $ref: `#/definitions/${defName}`}); res.push({name: derivedName, $ref: `#/definitions/${defName}`});
} }

View File

@ -13,6 +13,11 @@ declare module "*.css" {
export default content; export default content;
} }
declare module "*.json" {
const content: string;
export default content;
}
declare var LIB_VERSION: any; declare var LIB_VERSION: any;
declare var IS_PRODUCTION: any; declare var IS_PRODUCTION: any;
declare var AOT: any; declare var AOT: any;

View File

@ -1,7 +1,7 @@
{ {
"name": "redoc", "name": "redoc",
"description": "Swagger-generated API Reference Documentation", "description": "Swagger-generated API Reference Documentation",
"version": "1.10.2", "version": "1.11.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/Rebilly/ReDoc" "url": "git://github.com/Rebilly/ReDoc"

View File

@ -1,6 +1,8 @@
'use strict'; 'use strict';
import { SpecManager } from '../../lib/utils/spec-manager'; import { SpecManager } from '../../lib/utils/spec-manager';
import * as xExtendedDefs from './x-extended-defs.json';
describe('Utils', () => { describe('Utils', () => {
describe('Schema manager', () => { describe('Schema manager', () => {
let specMgr: SpecManager; let specMgr: SpecManager;
@ -175,6 +177,23 @@ describe('Utils', () => {
deriveDefs.should.be.instanceof(Array); deriveDefs.should.be.instanceof(Array);
deriveDefs.should.be.empty(); deriveDefs.should.be.empty();
}); });
it('should correctly work with x-extendedDiscriminator', () => {
specMgr._schema = {
definitions: xExtendedDefs
};
let deriveDefs = specMgr.findDerivedDefinitions('#/definitions/Payment');
deriveDefs.should.be.instanceof(Array);
deriveDefs.should.be.deepEqual([
{
name: 'cash',
$ref: '#/definitions/CashPayment'
}, {
name: 'paypal',
$ref: '#/definitions/PayPalPayment'
}])
});
}); });
}); });
}); });

View File

@ -0,0 +1,57 @@
{
"Payment": {
"x-extendedDiscriminator": "type",
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"CashPayment": {
"allOf": [
{
"$ref": "#/definitions/Payment"
},
{
"properties": {
"type": {
"type": "string",
"enum": [
"cash"
]
},
"currency": {
"type": "string"
}
}
}
]
},
"PayPalPayment": {
"allOf": [
{
"$ref": "#/definitions/Payment"
},
{
"properties": {
"type": {
"type": "string",
"enum": [
"paypal"
]
},
"userEmail": {
"type": "string"
}
}
}
]
}
}