diff --git a/README.md b/README.md index 350f7b1d..aefd18c5 100644 --- a/README.md +++ b/README.md @@ -144,9 +144,9 @@ ReDoc makes use of the following [vendor extensions](http://swagger.io/specifica ## Advanced usage Instead of adding `spec-url` attribute to the `` element you can initialize ReDoc via globally exposed `Redoc` object: ```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 `` tag attribute names as the keys, e.g.: ```js Redoc.init('http://petstore.swagger.io/v2/swagger.json', { diff --git a/lib/components/JsonSchema/json-schema.ts b/lib/components/JsonSchema/json-schema.ts index bea59675..e514b79b 100644 --- a/lib/components/JsonSchema/json-schema.ts +++ b/lib/components/JsonSchema/json-schema.ts @@ -91,8 +91,8 @@ export class JsonSchema extends BaseSearchableComponent implements OnInit { }).sort((a, b) => { 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); } diff --git a/lib/components/Redoc/redoc.ts b/lib/components/Redoc/redoc.ts index 6e89080f..db5e3cb5 100644 --- a/lib/components/Redoc/redoc.ts +++ b/lib/components/Redoc/redoc.ts @@ -15,7 +15,7 @@ import { BaseComponent } from '../base'; import * as detectScollParent from 'scrollparent'; 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'; @Component({ @@ -29,7 +29,7 @@ export class Redoc extends BaseComponent implements OnInit { error: any; specLoaded: boolean; - options: any; + options: Options; loadingProgress: number; @@ -84,7 +84,8 @@ export class Redoc extends BaseComponent implements OnInit { } 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; }); diff --git a/lib/index.ts b/lib/index.ts index 163de55a..b1a27a1e 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -5,6 +5,7 @@ import { enableProdMode } from '@angular/core'; import { Redoc } from './components/index'; import { BrowserDomAdapter as DOM } from './utils/browser-adapter'; import { disableDebugTools } from '@angular/platform-browser'; +import { isString } from './utils/helpers'; var bootstrapRedoc; if (AOT) { @@ -21,13 +22,16 @@ if (IS_PRODUCTION) { export const version = LIB_VERSION; var moduleRef; -export function init(specUrl:string, options:any = {}) { +export function init(specUrlOrSpec:string|any, options:any = {}) { if (moduleRef) { destroy(); } Redoc._preOptions = options; - options.specUrl = options.specUrl || specUrl; + options.specUrl = options.specUrl || (isString(specUrlOrSpec) ? specUrlOrSpec : ''); + if (!isString(specUrlOrSpec)) { + options.spec = specUrlOrSpec; + } return bootstrapRedoc() .then(appRef => { moduleRef = appRef; diff --git a/lib/services/menu.service.ts b/lib/services/menu.service.ts index d0f95144..f3248c4d 100644 --- a/lib/services/menu.service.ts +++ b/lib/services/menu.service.ts @@ -141,7 +141,7 @@ export class MenuService { onHashChange(hash?: string) { if (hash == undefined) return; let activated = this.activateByHash(hash); - if (!this.tasks.empty) { + if (!this.tasks.processed) { this.tasks.start(this.activeIdx, this); this.scrollService.setStickElement(this.getCurrentEl()); if (activated) this.scrollToActive(); diff --git a/lib/services/options.service.ts b/lib/services/options.service.ts index 268a43f5..1747efef 100644 --- a/lib/services/options.service.ts +++ b/lib/services/options.service.ts @@ -19,7 +19,7 @@ const OPTION_NAMES = new Set([ 'requiredPropsFirst' ]); -interface Options { +export interface Options { scrollYOffset?: any; disableLazySchemas?: boolean; specUrl?: string; @@ -29,6 +29,7 @@ interface Options { expandResponses?: Set | 'all'; $scrollParent?: HTMLElement | Window; requiredPropsFirst?: boolean; + spec?: any; } @Injectable() diff --git a/lib/services/schema-normalizer.service.ts b/lib/services/schema-normalizer.service.ts index 433d03a8..4a3881e2 100644 --- a/lib/services/schema-normalizer.service.ts +++ b/lib/services/schema-normalizer.service.ts @@ -99,6 +99,7 @@ class SchemaWalker { 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); @@ -115,7 +116,7 @@ export class AllOfMerger { defaults(into, subSchema); subSchema._pointer = tmpPtr; } - into.discriminator = null; + if (!hadDiscriminator) into.discriminator = null; into.allOf = null; } @@ -214,7 +215,8 @@ class SchemaDereferencer { // if resolved schema doesn't have title use name from 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) ) { WarningsService.warn(`Other properties are defined at the same level as $ref at "#${pointer}". ` + 'They are IGNORED according to the JsonSchema spec'); diff --git a/lib/services/search.service.ts b/lib/services/search.service.ts index 93b90055..a1f70cd9 100644 --- a/lib/services/search.service.ts +++ b/lib/services/search.service.ts @@ -186,10 +186,13 @@ export class SearchService { let title = name; 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... 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; } @@ -205,20 +208,18 @@ export class SearchService { body += ' ' + schema.enum.join(' '); } - if (!parent) { - // redoc doesn't display top level descriptions and titles - this.index({ - pointer: absolutePointer, - menuId: menuPointer, - title, - body - }); - } + this.index({ + pointer: absolutePointer, + menuId: menuPointer, + title, + body + }); if (schema.properties) { Object.keys(schema.properties).forEach(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); }); } } diff --git a/lib/shared/components/CopyButton/copy-button.directive.ts b/lib/shared/components/CopyButton/copy-button.directive.ts index bf57bc1c..6cd7810c 100644 --- a/lib/shared/components/CopyButton/copy-button.directive.ts +++ b/lib/shared/components/CopyButton/copy-button.directive.ts @@ -27,7 +27,7 @@ export class CopyButton implements OnInit { onClick() { let copied; if (this.copyText) { - copied = Clipboard.copyCustom(JSON.stringify(this.copyText)); + copied = Clipboard.copyCustom(JSON.stringify(this.copyText, null, 2)); } else { copied = Clipboard.copyElement(this.copyElement); } diff --git a/lib/shared/components/LazyFor/lazy-for.ts b/lib/shared/components/LazyFor/lazy-for.ts index 94019cf8..772ba8ca 100644 --- a/lib/shared/components/LazyFor/lazy-for.ts +++ b/lib/shared/components/LazyFor/lazy-for.ts @@ -31,6 +31,7 @@ export class LazyTasksService { private _tasks = []; private _current: number = 0; private _syncCount: number = 0; + private _emptyProcessed = false; private menuService; public loadProgress = new BehaviorSubject(0); @@ -38,8 +39,10 @@ export class LazyTasksService { constructor(public optionsService: OptionsService) { } - get empty() { - return this._current === this._tasks.length; + get processed() { + 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) { @@ -97,10 +100,17 @@ export class LazyTasksService { } else { this.sortTasks(idx); } + syncCount = Math.min(syncCount, this._tasks.length); if (this.allSync) syncCount = this._tasks.length; for (var i = this._current; i < syncCount; i++) { this.nextTaskSync(); } + + if (!this._tasks.length) { + this.loadProgress.next(100); + return; + } + this.nextTask(); } } diff --git a/lib/utils/helpers.ts b/lib/utils/helpers.ts index 86fcc839..ae9a3674 100644 --- a/lib/utils/helpers.ts +++ b/lib/utils/helpers.ts @@ -8,7 +8,7 @@ export function stringify(obj:any) { return JSON.stringify(obj); } -export function isString(str:any) { +export function isString(str:any):str is String { return typeof str === 'string'; } diff --git a/lib/utils/spec-manager.ts b/lib/utils/spec-manager.ts index 379f624a..6f379d24 100644 --- a/lib/utils/spec-manager.ts +++ b/lib/utils/spec-manager.ts @@ -9,6 +9,7 @@ import { MdRenderer } from './md-renderer'; import { SwaggerOperation, SwaggerParameter } from './swagger-typings'; import { snapshot } from './helpers'; +import { WarningsService } from '../services/warnings.service'; function getDiscriminator(obj) { return obj.discriminator || obj['x-extendedDiscriminator']; @@ -181,7 +182,37 @@ export class SpecManager { let globalDefs = this._schema.definitions || {}; 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 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)) { let def = globalDefs[defName]; if (!def.allOf && @@ -189,12 +220,6 @@ export class SpecManager { let subTypes = def['x-derived-from'] || 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; for (let ptr of pointers) { @@ -204,12 +229,23 @@ export class SpecManager { if (idx < 0) continue; - let derivedName = defName; + let derivedName; if (extendedDiscriminatorProp) { - let prop = def.properties && def.properties[extendedDiscriminatorProp]; - if (prop && prop.enum && prop.enum.length === 1) { - derivedName = prop.enum[0]; + let subDefs = def.allOf || []; + for (let def of subDefs) { + let prop = def.properties && def.properties[extendedDiscriminatorProp]; + if (prop && prop.enum && prop.enum.length === 1) { + 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}`}); diff --git a/manual-types/index.d.ts b/manual-types/index.d.ts index 1a39435e..ba8e22db 100644 --- a/manual-types/index.d.ts +++ b/manual-types/index.d.ts @@ -13,6 +13,11 @@ declare module "*.css" { export default content; } +declare module "*.json" { + const content: string; + export default content; +} + declare var LIB_VERSION: any; declare var IS_PRODUCTION: any; declare var AOT: any; diff --git a/package.json b/package.json index d6153aa0..b3746589 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "redoc", "description": "Swagger-generated API Reference Documentation", - "version": "1.10.2", + "version": "1.11.0", "repository": { "type": "git", "url": "git://github.com/Rebilly/ReDoc" diff --git a/tests/unit/SpecManager.spec.ts b/tests/unit/SpecManager.spec.ts index f79e1409..ec128788 100644 --- a/tests/unit/SpecManager.spec.ts +++ b/tests/unit/SpecManager.spec.ts @@ -1,6 +1,8 @@ 'use strict'; import { SpecManager } from '../../lib/utils/spec-manager'; +import * as xExtendedDefs from './x-extended-defs.json'; + describe('Utils', () => { describe('Schema manager', () => { let specMgr: SpecManager; @@ -175,6 +177,23 @@ describe('Utils', () => { deriveDefs.should.be.instanceof(Array); 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' + }]) + }); }); }); }); diff --git a/tests/unit/x-extended-defs.json b/tests/unit/x-extended-defs.json new file mode 100644 index 00000000..b0d487ea --- /dev/null +++ b/tests/unit/x-extended-defs.json @@ -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" + } + } + } + ] + } +}