feat: Add support for x-servers

This commit is contained in:
Roman Hotsiy 2017-02-25 23:38:19 +02:00
parent dcb97a5b96
commit fd49082db2
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
13 changed files with 294 additions and 79 deletions

View File

@ -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:

View File

@ -0,0 +1,17 @@
<div class="method-endpoint" (click)="handleClick()">
<h5 class="http-verb" [ngClass]="verb">{{verb}}</h5>
<span><!--
--><span class="method-api-url-path">{{path}}</span><!--
--></span>
<svg class="expand-icon" xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve">
<polygon fill="white" points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "/>
</svg>
</div>
<div class="servers-overlay" [@overlayExpand]="expanded ? 'expanded' : 'collapsed'">
<div *ngFor="let server of servers" class="server-item">
<div class="description" [innerHtml]="server.description | marked"></div>
<div select-on-click class="url">
<span class="method-api-url"> {{server.url}}</span>{{path}}
</div>
</div>
</div>

View File

@ -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);
}
}

View File

@ -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:
`<method pointer='#/paths/~1user~1{username}/put'></method>`
})
class TestAppComponent {
}

View File

@ -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();
}
}

View File

@ -16,12 +16,7 @@
<div class="method-samples">
<h4 class="method-params-subheader">Definition</h4>
<div class="method-endpoint">
<h5 class="http-method" [ngClass]="method.httpMethod">{{method.httpMethod}}</h5>
<span select-on-click><!--
--><span class="method-api-url">{{method.apiUrl}}</span><span class="method-api-url-path">{{method.path}}</span><!--
--></span>
</div>
<endpoint-link [verb]="method.verb" [path]="method.path"> </endpoint-link>
<div>
<request-samples [pointer]="pointer" [schemaPointer]="method.bodyParam?._pointer">

View File

@ -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;
}

View File

@ -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}');
});

View File

@ -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 [];

View File

@ -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 }

View File

@ -7,6 +7,7 @@ $green: #00aa13;
$yellow: #f1c400;
$red: #e53935;
$background-color: #fff;
$border-color: #ccc;
$em-size: 14px;

View File

@ -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<any|null>(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 [];

View File

@ -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('');
});