From fd49082db20783c802e58b216f837ac2092fa101 Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Sat, 25 Feb 2017 23:38:19 +0200 Subject: [PATCH] feat: Add support for `x-servers` --- demo/swagger.yaml | 5 + .../EndpointLink/endpoint-link.html | 17 +++ .../EndpointLink/endpoint-link.scss | 101 ++++++++++++++++++ .../EndpointLink/endpoint-link.spec.ts | 79 ++++++++++++++ lib/components/EndpointLink/endpoint-link.ts | 69 ++++++++++++ lib/components/Method/method.html | 7 +- lib/components/Method/method.scss | 48 --------- lib/components/Method/method.spec.ts | 5 +- lib/components/Method/method.ts | 14 +-- lib/components/index.ts | 5 +- lib/shared/styles/_variables.scss | 1 + lib/utils/spec-manager.ts | 14 ++- tests/unit/SpecManager.spec.ts | 8 +- 13 files changed, 294 insertions(+), 79 deletions(-) create mode 100644 lib/components/EndpointLink/endpoint-link.html create mode 100644 lib/components/EndpointLink/endpoint-link.scss create mode 100644 lib/components/EndpointLink/endpoint-link.spec.ts create mode 100644 lib/components/EndpointLink/endpoint-link.ts diff --git a/demo/swagger.yaml b/demo/swagger.yaml index b3cae32daf..f21ecef5a8 100644 --- a/demo/swagger.yaml +++ b/demo/swagger.yaml @@ -79,6 +79,11 @@ securityDefinitions: type: apiKey name: api_key in: header +x-servers: + - url: //petstore.swagger.io/v2 + description: Default server + - url: //petstore.swagger.io/sandbox + description: Sandbox server paths: /pet: post: diff --git a/lib/components/EndpointLink/endpoint-link.html b/lib/components/EndpointLink/endpoint-link.html new file mode 100644 index 0000000000..2763667be0 --- /dev/null +++ b/lib/components/EndpointLink/endpoint-link.html @@ -0,0 +1,17 @@ +
+
{{verb}}
+ {{path}} + + + +
+
+
+
+
+ {{server.url}}{{path}} +
+
+
diff --git a/lib/components/EndpointLink/endpoint-link.scss b/lib/components/EndpointLink/endpoint-link.scss new file mode 100644 index 0000000000..e7f59fe357 --- /dev/null +++ b/lib/components/EndpointLink/endpoint-link.scss @@ -0,0 +1,101 @@ +@import '../../shared/styles/variables'; + +:host { + display: block; + position: relative; + cursor: pointer; +} + +.method-endpoint { + padding: 10px 20px; + border-radius: $border-radius*2; + background-color: darken($black, 2%); + display: block; + font-weight: $light; + white-space: nowrap; + overflow-x: auto; + border: 1px solid transparent; +} + +.method-endpoint > .method-params-subheader { + padding-top: 1px; + padding-bottom: 0; + margin: 0; + font-size: 12/14em; + color: $black; + vertical-align: middle; + display: inline-block; + border-radius: $border-radius; +} + +.method-api-url { + color: rgba($black, .8); + &-path { + font-family: $headers-font, $headers-font-family; + position: relative; + top: 1px; + color: #ffffff; + margin-left: 10px; + } +} + +.http-verb { + color: $black; + background: #ffffff; + padding: 3px 10px; + text-transform: uppercase; + display: inline-block; + margin: 0; +} + +.servers-overlay { + position: absolute; + width: 100%; + z-index: 100; + background: $side-bar-bg-color; + color: $black; + box-sizing: border-box; + box-shadow: 4px 4px 6px rgba(0, 0, 0, 0.33); + overflow: hidden; + border-bottom-left-radius: $border-radius*2; + border-bottom-right-radius: $border-radius*2; +} + +.server-item { + padding: 10px; + //margin-bottom: 10px; + + & > .url { + padding: 5px; + border: 1px solid $border-color; + background: $background-color; + word-break: break-all; + } + + &:last-child { + margin-bottom: 0; + } +} + +.expand-icon { + height: 20px; + width: 20px; + display: inline-block; + float: right; + margin-top: 2px; + background: darken($black, 2%); + transform: rotateZ(0); + transition: all 0.2s ease; +} + +:host.expanded { + > .method-endpoint { + border-color: $side-bar-bg-color; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + .expand-icon { + transform: rotateZ(180deg); + } +} diff --git a/lib/components/EndpointLink/endpoint-link.spec.ts b/lib/components/EndpointLink/endpoint-link.spec.ts new file mode 100644 index 0000000000..99664065fc --- /dev/null +++ b/lib/components/EndpointLink/endpoint-link.spec.ts @@ -0,0 +1,79 @@ +'use strict'; + +import { Component } from '@angular/core'; +import { + inject, + async, + TestBed +} from '@angular/core/testing'; + +import { getChildDebugElement } from '../../../tests/helpers'; + +import { EndpointLink } from './endpoint-link'; +import { SpecManager } from '../../utils/spec-manager'; + +describe('Redoc components', () => { + beforeEach(() => { + TestBed.configureTestingModule({ declarations: [ TestAppComponent ] }); + }); + describe('EndpointLink Component', () => { + let builder; + let component: EndpointLink; + let specMgr: SpecManager; + + beforeEach(async(inject([SpecManager], (_specMgr) => { + specMgr = _specMgr; + }))); + + beforeEach(() => { + specMgr.apiUrl = 'http://test.com/v1'; + specMgr._schema = { + info: {}, + host: 'petstore.swagger.io', + baseName: '/v2', + schemes: ['https', 'http'], + 'x-servers': [ + { + url: '//test.com/v2' + }, + { + url: 'ws://test.com/v3', + description: 'test' + } + ] + }; + specMgr.init(); + + component = new EndpointLink(specMgr); + }); + + it('should replace // with appropriate protocol', () => { + component.ngOnInit(); + component.servers[0].url.should.be.equal('https://test.com/v2'); + }); + + + it('should preserve other protocols', () => { + component.ngOnInit(); + component.servers[1].url.should.be.equal('ws://test.com/v3'); + }); + + it('should fallback to host + basePath + schemas if no x-servers', () => { + specMgr._schema['x-servers'] = null; + specMgr.init(); + component.ngOnInit(); + component.servers.should.be.lengthOf(1); + component.servers[0].url.should.be.equal('https://petstore.swagger.io'); + }); + }); +}); + + +/** Test component that contains a Method. */ +@Component({ + selector: 'test-app', + template: + `` +}) +class TestAppComponent { +} diff --git a/lib/components/EndpointLink/endpoint-link.ts b/lib/components/EndpointLink/endpoint-link.ts new file mode 100644 index 0000000000..263e11af0e --- /dev/null +++ b/lib/components/EndpointLink/endpoint-link.ts @@ -0,0 +1,69 @@ +'use strict'; +import { Component, ChangeDetectionStrategy, Input, OnInit, HostListener, HostBinding} from '@angular/core'; +import { BaseComponent, SpecManager } from '../base'; +import { trigger, state, animate, transition, style } from '@angular/core'; + +export interface ServerInfo { + description: string; + url: string; +} + +@Component({ + selector: 'endpoint-link', + styleUrls: ['./endpoint-link.css'], + templateUrl: './endpoint-link.html', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('overlayExpand', [ + state('collapsed, void', + style({ height: '0px' })), + state('expanded', + style({ height: '*' })), + transition('collapsed <=> expanded', [ + animate('200ms ease') + ]) + ]) + ] +}) +export class EndpointLink implements OnInit { + @Input() path:string; + @Input() verb:string; + + apiUrl: string; + servers: ServerInfo[]; + @HostBinding('class.expanded') expanded: boolean = false; + + // @HostListener('click') + handleClick() { + this.expanded = !this.expanded; + } + + constructor(public specMgr:SpecManager) { + this.expanded = false; + } + + init() { + let servers:ServerInfo[] = this.specMgr.schema['x-servers']; + if (servers) { + this.servers = servers.map(({url, description}) => ({ + description, + url: url.startsWith('//') ? `${this.specMgr.apiProtocol}:${url}` : url + })); + } else { + this.servers = [ + { + description: 'Server URL', + url: this.getBaseUrl() + } + ]; + } + } + + getBaseUrl():string { + return this.specMgr.apiUrl; + } + + ngOnInit() { + this.init(); + } +} diff --git a/lib/components/Method/method.html b/lib/components/Method/method.html index c031ca744b..5bc116ce40 100644 --- a/lib/components/Method/method.html +++ b/lib/components/Method/method.html @@ -16,12 +16,7 @@

Definition

-
-
{{method.httpMethod}}
- {{method.apiUrl}}{{method.path}} -
+
diff --git a/lib/components/Method/method.scss b/lib/components/Method/method.scss index 0306b61e39..6194a628b4 100644 --- a/lib/components/Method/method.scss +++ b/lib/components/Method/method.scss @@ -18,45 +18,6 @@ margin-bottom: calc(1em - 6px); } -.method-endpoint { - //margin: 0 0 2px 0; - padding: 10px 20px; - border-radius: $border-radius*2; - background-color: darken($black, 2%); - display: block; - font-weight: $light; - white-space: nowrap; - overflow-x: auto; -} - -.method-endpoint > .method-params-subheader { - padding-top: 1px; - padding-bottom: 0; - margin: 0; - font-size: 12/14em; - color: $black; - vertical-align: middle; - display: inline-block; - border-radius: $border-radius; -} - -.method-api-url { - color: rgba(#ffffff, .6); - margin-left: 10px; - margin-top: 2px; - position: relative; - top: 1px; - font-family: $headers-font, $headers-font-family; - font-size: 0.929em; - - &-path { - font-family: $headers-font, $headers-font-family; - position: relative; - top: 1px; - color: #ffffff; - } -} - .method-tags { margin-top: 20px; @@ -121,15 +82,6 @@ margin: 0; } -.http-method { - color: $black; - background: #ffffff; - padding: 3px 10px; - text-transform: uppercase; - display: inline-block; - margin: 0; -} - [select-on-click] { cursor: pointer; } diff --git a/lib/components/Method/method.spec.ts b/lib/components/Method/method.spec.ts index d754067ce3..0d4eb5fe7c 100644 --- a/lib/components/Method/method.spec.ts +++ b/lib/components/Method/method.spec.ts @@ -19,7 +19,7 @@ describe('Redoc components', () => { }); describe('Method Component', () => { let builder; - let component; + let component: Method; let specMgr; beforeEach(async(inject([SpecManager, LazyTasksService], (_specMgr, lazyTasks) => { @@ -43,8 +43,7 @@ describe('Redoc components', () => { }); it('should init basic component data', () => { - component.method.apiUrl.should.be.equal('http://petstore.swagger.io/v2'); - component.method.httpMethod.should.be.equal('put'); + component.method.verb.should.be.equal('put'); component.method.path.should.be.equal('/user/{username}'); }); diff --git a/lib/components/Method/method.ts b/lib/components/Method/method.ts index 852290bb2e..3da98f1334 100644 --- a/lib/components/Method/method.ts +++ b/lib/components/Method/method.ts @@ -7,8 +7,7 @@ import { OptionsService } from '../../services/'; interface MethodInfo { - apiUrl: string; - httpMethod: string; + verb: string; path: string; info: { tags: string[]; @@ -45,7 +44,7 @@ export class Method extends BaseComponent implements OnInit { this.operationId = this.componentSchema.operationId; this.method = { - httpMethod: JsonPointer.baseName(this.pointer), + verb: JsonPointer.baseName(this.pointer), path: JsonPointer.baseName(this.pointer, 2), info: { description: this.componentSchema.description, @@ -53,7 +52,6 @@ export class Method extends BaseComponent implements OnInit { }, bodyParam: this.findBodyParam(), summary: SchemaHelper.methodSummary(this.componentSchema), - apiUrl: this.getBaseUrl(), anchor: this.buildAnchor(), externalDocs: this.componentSchema.externalDocs }; @@ -67,14 +65,6 @@ export class Method extends BaseComponent implements OnInit { } } - getBaseUrl():string { - if (this.optionsService.options.hideHostname) { - return this.specMgr.basePath; - } else { - return this.specMgr.apiUrl; - } - } - filterMainTags(tags) { var tagsMap = this.specMgr.getTagsMap(); if (!tags) return []; diff --git a/lib/components/index.ts b/lib/components/index.ts index f26d2f10d3..4ab8706687 100644 --- a/lib/components/index.ts +++ b/lib/components/index.ts @@ -17,15 +17,16 @@ import { SecurityDefinitions } from './SecurityDefinitions/security-definitions' import { LoadingBar } from './LoadingBar/loading-bar'; import { RedocSearch } from './Search/redoc-search'; import { ExternalDocs } from './ExternalDocs/external-docs'; +import { EndpointLink } from './EndpointLink/endpoint-link'; import { Redoc } from './Redoc/redoc'; export const REDOC_DIRECTIVES = [ ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList, ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions, - LoadingBar, SideMenuItems, RedocSearch, ExternalDocs + LoadingBar, SideMenuItems, RedocSearch, ExternalDocs, EndpointLink ]; export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList, ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions, -LoadingBar, SideMenuItems, RedocSearch, ExternalDocs } +LoadingBar, SideMenuItems, RedocSearch, ExternalDocs, EndpointLink } diff --git a/lib/shared/styles/_variables.scss b/lib/shared/styles/_variables.scss index e31b8a6540..62e5ba0e82 100644 --- a/lib/shared/styles/_variables.scss +++ b/lib/shared/styles/_variables.scss @@ -7,6 +7,7 @@ $green: #00aa13; $yellow: #f1c400; $red: #e53935; $background-color: #fff; +$border-color: #ccc; $em-size: 14px; diff --git a/lib/utils/spec-manager.ts b/lib/utils/spec-manager.ts index 516872c374..951a18c098 100644 --- a/lib/utils/spec-manager.ts +++ b/lib/utils/spec-manager.ts @@ -24,10 +24,12 @@ export interface DescendantInfo { export class SpecManager { public _schema: any = {}; public apiUrl: string; + public apiProtocol: string; + public swagger: string; public basePath: string; public spec = new BehaviorSubject(null); - private _url: string; + public _specUrl: string; private parser: any; load(urlOrObject: string|Object) { @@ -36,7 +38,7 @@ export class SpecManager { this.parser.bundle(urlOrObject, {http: {withCredentials: false}}) .then(schema => { if (typeof urlOrObject === 'string') { - this._url = urlOrObject; + this._specUrl = urlOrObject; } this._schema = snapshot(schema); try { @@ -54,7 +56,7 @@ export class SpecManager { /* calculate common used values */ init() { - let urlParts = this._url ? urlParse(urlResolve(window.location.href, this._url)) : {}; + let urlParts = this._specUrl ? urlParse(urlResolve(window.location.href, this._specUrl)) : {}; let schemes = this._schema.schemes; let protocol; if (!schemes || !schemes.length) { @@ -70,6 +72,7 @@ export class SpecManager { let host = this._schema.host || urlParts.host; this.basePath = this._schema.basePath || ''; this.apiUrl = protocol + '://' + host + this.basePath; + this.apiProtocol = protocol; if (this.apiUrl.endsWith('/')) { this.apiUrl = this.apiUrl.substr(0, this.apiUrl.length - 1); } @@ -79,6 +82,9 @@ export class SpecManager { preprocess() { let mdRender = new MdRenderer(); + if (!this._schema.info) { + throw 'Required field "info" is not specified at the spec top level'; + } if (!this._schema.info.description) this._schema.info.description = ''; if (this._schema.securityDefinitions) { let SecurityDefinitions = require('../components/').SecurityDefinitions; @@ -168,7 +174,7 @@ export class SpecManager { return tagsMap; } - findDerivedDefinitions(defPointer: string, schema): DescendantInfo[] { + findDerivedDefinitions(defPointer: string, schema?: any): DescendantInfo[] { let definition = schema || this.byPointer(defPointer); if (!definition) throw new Error(`Can't load schema at ${defPointer}`); if (!definition.discriminator && !definition['x-extendedDiscriminator']) return []; diff --git a/tests/unit/SpecManager.spec.ts b/tests/unit/SpecManager.spec.ts index d5488c350a..f79e1409e8 100644 --- a/tests/unit/SpecManager.spec.ts +++ b/tests/unit/SpecManager.spec.ts @@ -3,7 +3,7 @@ import { SpecManager } from '../../lib/utils/spec-manager'; describe('Utils', () => { describe('Schema manager', () => { - let specMgr; + let specMgr: SpecManager; beforeEach(() => { specMgr = new SpecManager(); @@ -51,21 +51,21 @@ describe('Utils', () => { it('should substitute api scheme when spec schemes are undefined', () => { specMgr._schema.schemes = undefined; - specMgr._url = 'https://petstore.swagger.io/v2'; + specMgr._specUrl = 'https://petstore.swagger.io/v2'; specMgr.init(); specMgr.apiUrl.should.be.equal('https://petstore.swagger.io/v2'); }); it('should substitute api host when spec host is undefined', () => { specMgr._schema.host = undefined; - specMgr._url = 'http://petstore.swagger.io/v2'; + specMgr._specUrl = 'http://petstore.swagger.io/v2'; specMgr.init(); specMgr.apiUrl.should.be.equal('http://petstore.swagger.io/v2'); }); it('should use empty basePath when basePath is not present', () => { specMgr._schema.basePath = undefined; - specMgr._url = 'https://petstore.swagger.io'; + specMgr._specUrl = 'https://petstore.swagger.io'; specMgr.init(); specMgr.basePath.should.be.equal(''); });