Make ReDoc render sync + separate standalone componenet

- refactored usage of store so now <Redoc> is sync
- other minor refactors
This commit is contained in:
Roman Hotsiy 2017-11-14 17:46:50 +02:00
parent 0061a87496
commit 109d135959
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
30 changed files with 310 additions and 343 deletions

View File

@ -1,3 +1,3 @@
{
"editor.formatOnSave": false
"editor.formatOnSave": true
}

View File

@ -71,14 +71,12 @@
"preact": "^8.2.5",
"preact-compat": "^3.17.0",
"prismjs": "^1.8.1",
"prop-types": "^15.6.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-dropdown": "^1.3.0",
"react-markdown": "^2.5.0",
"react-perfect-scrollbar": "^0.2.2",
"react-tabs": "^2.0.0",
"recompose": "^0.25.1",
"remarkable": "^1.7.1",
"slugify": "^1.2.1",
"styled-components": "^2.2.1"

View File

@ -11,3 +11,12 @@ export const RightPanel = styled.div`
bckground-color: ${props => props.theme.rightPanel.backgroundColor};
padding: ${props => props.theme.spacingUnit * 2}px;
`;
export const DarkRightPanel = styled(RightPanel)`
background-color: ${props => props.theme.rightPanel.backgroundColor};
`;
export const Row = styled.div`
display: flex;
width: 100%;
`;

View File

@ -7,8 +7,9 @@ import { OpenAPIExternalDocumentation } from '../../types';
import { ApiInfoModel } from '../../services/models';
import { SecurityDefs } from '../SecurityDefs/SecurityDefs';
import { MiddlePanel, DarkRightPanel, Row } from '../../common-elements/';
import {
ApiInfoWrap,
ApiHeader,
DownloadButton,
InfoSpan,
@ -18,7 +19,7 @@ import {
interface ApiInfoProps {
info: ApiInfoModel;
externalDocs: OpenAPIExternalDocumentation;
externalDocs?: OpenAPIExternalDocumentation;
}
@observer
@ -65,7 +66,8 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
null;
return (
<ApiInfoWrap className="api-info">
<Row>
<MiddlePanel className="api-info">
<ApiHeader>
{info.title} <span>({info.version})</span>
</ApiHeader>
@ -101,7 +103,9 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
components={{ 'security-definitions': SecurityDefs }}
/>
</div>
</ApiInfoWrap>
</MiddlePanel>
<DarkRightPanel />
</Row>
);
}
}

View File

@ -1,19 +0,0 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import * as PropTypes from 'prop-types';
import { getContext } from 'recompose';
import { BaseContainerProps } from '../../types/components';
import { ApiInfo } from './ApiInfo';
@observer
export class ApiInfoContainer extends React.Component<BaseContainerProps> {
render() {
const { info, externalDocs } = this.props.store.spec;
return <ApiInfo info={info!} externalDocs={externalDocs!} />;
}
}
export default getContext<BaseContainerProps>({
store: PropTypes.object,
})(ApiInfoContainer);

View File

@ -1,18 +1,14 @@
import { OpenAPIInfo } from '../../types';
import * as React from 'react';
import { observer } from 'mobx-react';
import * as PropTypes from 'prop-types';
import { getContext } from 'recompose';
import { BaseContainerProps } from '../../types/components';
import { LogoImgEl } from './styled.elements';
const LinkWrap = url => Component => <a href={url}>{Component}</a>;
@observer
class ApiLogo extends React.Component<BaseContainerProps> {
export class ApiLogo extends React.Component<{ info: OpenAPIInfo }> {
render() {
const { spec } = this.props.store;
const info = spec.info!;
const { info } = this.props;
const logoInfo = info['x-logo'];
if (!logoInfo || !logoInfo.url) return null;
@ -22,7 +18,3 @@ class ApiLogo extends React.Component<BaseContainerProps> {
return info.contact && info.contact.url ? LinkWrap(info.contact.url)(logo) : logo;
}
}
export default getContext<BaseContainerProps>({
store: PropTypes.object,
})(ApiLogo);

View File

@ -1,19 +0,0 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { getContext } from 'recompose';
import { observer } from 'mobx-react';
import { BaseContainerProps } from '../../types/components';
import { ContentItems } from './ContentItems';
@observer
export class ContentContainer extends React.Component<BaseContainerProps> {
render() {
const items = this.props.store.menu.items;
return <ContentItems items={items as any} />;
}
}
export default getContext<BaseContainerProps>({
store: PropTypes.object,
})(ContentContainer);

View File

@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
import { SECTION_ATTR } from '../../services/MenuStore';
import { Markdown } from '../Markdown/Markdown';
import { H1, MiddlePanel, ShareLink } from '../../common-elements';
import { DarkRightPanel, H1, MiddlePanel, ShareLink, Row } from '../../common-elements';
import { Operation } from '../Operation/Operation';
import { ContentItemModel } from '../../services/MenuBuilder';
import { OperationModel } from '../../services/models';
@ -60,13 +60,16 @@ export class TagItem extends React.Component<ContentItemProps> {
render() {
const { name, description } = this.props.item;
return (
<MiddlePanel>
<Row>
<MiddlePanel key="middle">
<H1>
<ShareLink href={'#' + this.props.item.getHash()} />
{name}
</H1>
{description !== undefined && <Markdown source={description} />}
</MiddlePanel>
<DarkRightPanel key="right" />
</Row>
);
}
}

View File

@ -1,27 +0,0 @@
import * as React from 'react';
import ApiInfoContainer from '../ApiInfo/ApiInfoContainer';
import { RedocWrap, MenuContent, ApiContent, Background } from './elements';
import ApiLogo from '../ApiLogo/ApiLogo';
import SideMenu from '../SideMenu/SideMenu';
import ContentContainer from '../ContentItems/ContentContainer';
export class ContentRoot extends React.Component {
render() {
return (
<RedocWrap className="redoc-wrap">
<MenuContent className="menu-content">
<ApiLogo />
<SideMenu />
</MenuContent>
<Background className="background-wrap">
<div className="redoc-background" />
</Background>
<ApiContent className="api-content">
<ApiInfoContainer />
<ContentContainer />
</ApiContent>
</RedocWrap>
);
}
}

View File

@ -1,11 +1,7 @@
import * as PropTypes from 'prop-types';
import * as React from 'react';
import { Children } from 'react';
import { getContext } from 'recompose';
import { observer } from 'mobx-react';
import styled from '../../styled-components';
import { AppStore } from '../../services';
import { Spinner } from './Spinner.svg';
const LoadingMessage = styled.div`
@ -17,10 +13,9 @@ const LoadingMessage = styled.div`
color: ${props => props.theme.colors.main};
`;
@observer
class LoadingWrap extends React.Component<{ store: AppStore }> {
export class LoadingWrap extends React.Component<{ loading: boolean }> {
render() {
if (this.props.store.spec.loaded) {
if (this.props.loading) {
return Children.only(this.props.children);
}
return (
@ -31,7 +26,3 @@ class LoadingWrap extends React.Component<{ store: AppStore }> {
);
}
}
export default getContext<{ store: AppStore }>({
store: PropTypes.object,
})(LoadingWrap);

View File

@ -3,7 +3,7 @@ import styled from '../../styled-components';
import { observer } from 'mobx-react';
import { H2, MiddlePanel, RightPanel, Badge } from '../../common-elements';
import { H2, MiddlePanel, DarkRightPanel, Badge, Row } from '../../common-elements';
import { Markdown } from '../Markdown/Markdown';
import { Parameters } from '../Parameters/Parameters';
@ -15,14 +15,19 @@ import { Endpoint } from '../Endpoint/Endpoint';
import { OperationModel as OperationType } from '../../services/models';
const OperationRow = styled.div`
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
margin-bottom: 30px;
padding-bottom: 30px;
const OperationRow = styled(Row)`
transform: translateZ(0);
display: flex;
overflow: hidden;
positioin: relative;
&:after {
position: absolute;
bottom: 0;
width: 100%;
display: block;
content: '';
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
}
`;
interface OperationProps {
@ -47,11 +52,11 @@ export class Operation extends React.Component<OperationProps> {
<Parameters parameters={operation.parameters} body={operation.requestBody} />
<ResponsesList responses={operation.responses} />
</MiddlePanel>
<RightPanel>
<DarkRightPanel>
<Endpoint operation={operation} />
<RequestSamples operation={operation} />
<ResponseSamples operation={operation} />
</RightPanel>
</DarkRightPanel>
</OperationRow>
);
}

View File

@ -1,33 +0,0 @@
import * as React from 'react';
import { ContentRoot } from './ContentRoot/ContentRoot';
import { ThemeProvider } from '../styled-components';
import defaultTheme from '../theme';
import LoadingWrap from './LoadingWrap/LoadingWrap';
import { StoreProvider } from './StoreProvider';
import { ErrorBoundary } from './ErrorBoundary';
export interface RedocProps {
specUrl?: string;
spec?: object;
theme?: any;
store?: any;
}
export class Redoc extends React.Component<RedocProps> {
render() {
return (
<ErrorBoundary>
<StoreProvider {...this.props}>
<ThemeProvider theme={this.props.theme || defaultTheme}>
<LoadingWrap>
<ContentRoot />
</LoadingWrap>
</ThemeProvider>
</StoreProvider>
</ErrorBoundary>
);
}
}

View File

@ -0,0 +1,44 @@
import { ThemeInterface } from '../../theme';
import * as React from 'react';
import { ThemeProvider } from '../../styled-components';
import { ApiInfo } from '../ApiInfo/ApiInfo';
import { RedocWrap, MenuContent, ApiContent } from './elements';
import { ApiLogo } from '../ApiLogo/ApiLogo';
import { SideMenu } from '../SideMenu/SideMenu';
import { ContentItems } from '../ContentItems/ContentItems';
import { AppStore } from '../../services';
import defaultTheme from '../../theme';
interface RedocProps {
store: AppStore;
options?: {
theme?: ThemeInterface;
};
}
export class Redoc extends React.Component<RedocProps> {
componentDidMount() {
this.props.store.menu.updateOnHash();
}
render() {
const { store: { spec, menu }, options = {} } = this.props;
return (
<ThemeProvider theme={options.theme || defaultTheme}>
<RedocWrap className="redoc-wrap">
<MenuContent className="menu-content">
<ApiLogo info={spec.info} />
<SideMenu menu={menu} />
</MenuContent>
<ApiContent className="api-content">
<ApiInfo info={spec.info} externalDocs={spec.externalDocs} />
<ContentItems items={menu.items as any} />;
</ApiContent>
</RedocWrap>
</ThemeProvider>
);
}
}

View File

@ -49,21 +49,3 @@ export const ApiContent = styled.div`
z-index: 10;
position: relative;
`;
export const Background = styled.div`
position: absolute;
left: ${props => props.theme.menu.width};
top: 0;
bottom: 0;
right: 0;
z-index: 1;
.redoc-background {
background-color: ${props => props.theme.rightPanel.backgroundColor};
left: ${props => 100 - props.theme.rightPanel.width}%;
right: 0;
top: 0;
bottom: 0;
position: absolute;
}
`;

View File

@ -0,0 +1,41 @@
import * as React from 'react';
import { ThemeInterface } from '../theme';
import { LoadingWrap } from './LoadingWrap/LoadingWrap';
import { StoreProvider } from './StoreProvider';
import { ErrorBoundary } from './ErrorBoundary';
import { Redoc } from './Redoc/Redoc';
export interface RedocProps {
specOrSpecUrl: string | object;
options?: {
theme?: ThemeInterface;
};
}
export class RedocStandalone extends React.Component<RedocProps> {
render() {
const { specOrSpecUrl, options } = this.props;
let specUrl;
let spec;
if (typeof specOrSpecUrl === 'string') {
specUrl = specOrSpecUrl;
} else if (typeof specOrSpecUrl === 'object') {
spec = specOrSpecUrl;
}
return (
<ErrorBoundary>
<StoreProvider spec={spec} specUrl={specUrl}>
{({ loading, store }) => (
<LoadingWrap loading={loading}>
<Redoc store={store} options={options} />
</LoadingWrap>
)}
</StoreProvider>
</ErrorBoundary>
);
}
}

View File

@ -1,18 +1,15 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import * as PropTypes from 'prop-types';
import { getContext } from 'recompose';
import { BaseContainerProps } from '../../types/components';
import { IMenuItem } from '../../services/MenuStore';
import { PerfectScrollbar } from '../../common-elements/perfect-scrollbar';
import { MenuStore, IMenuItem } from '../../services/MenuStore';
import { MenuItems } from './MenuItems';
import { PerfectScrollbar } from '../../common-elements/perfect-scrollbar';
@observer
class SideMenu extends React.Component<BaseContainerProps> {
export class SideMenu extends React.Component<{ menu: MenuStore }> {
render() {
const store = this.props.store.menu;
const store = this.props.menu;
return (
<PerfectScrollbar>
<MenuItems items={store.items} onActivate={this.activate} />
@ -21,10 +18,6 @@ class SideMenu extends React.Component<BaseContainerProps> {
}
activate = (item: IMenuItem) => {
this.props.store.menu.activateAndScroll(item, true);
this.props.menu.activateAndScroll(item, true);
};
}
export default getContext<BaseContainerProps>({
store: PropTypes.object,
})(SideMenu);

View File

@ -1,34 +1,51 @@
import { Component, Children } from 'react';
import * as PropTypes from 'prop-types';
import { AppStore, HistoryService } from '../services/';
import { Component } from 'react';
import { AppStore } from '../services/';
import { loadSpec } from '../utils';
interface SpecProps {
specUrl?: string;
spec?: object;
store?: AppStore;
children?: any;
}
export class StoreProvider extends Component<SpecProps, { error?: Error }> {
store: AppStore;
interface SpecState {
error?: Error;
loading: boolean;
store?: AppStore;
}
static childContextTypes = {
store: PropTypes.object.isRequired,
};
export class StoreProvider extends Component<SpecProps, SpecState> {
store: AppStore;
constructor(props: SpecProps) {
super(props);
this.state = {};
this.store = props.store || new AppStore();
this.state = {
loading: false,
};
if (!this.store.spec.loaded) {
this.store.spec
.load(props.spec! || props.specUrl)
.then(() => {
HistoryService.emit();
this.setError();
})
.catch(e => this.setError(e));
this.load();
}
async load() {
let { specUrl, spec } = this.props;
this.setState({
loading: true,
});
try {
const resolvedSpec = await loadSpec(spec || specUrl!);
this.setState({
loading: false,
store: new AppStore(resolvedSpec, specUrl),
});
} catch (e) {
this.setState({
error: e,
});
}
}
@ -38,12 +55,8 @@ export class StoreProvider extends Component<SpecProps, { error?: Error }> {
});
}
getChildContext() {
return { store: this.props.store || this.store };
}
render() {
if (this.state.error) throw this.state.error;
return Children.only(this.props.children);
return this.props.children(this.state);
}
}

View File

@ -1 +1,6 @@
export * from './Redoc';
export * from './RedocStandalone';
export * from './Redoc/Redoc';
export * from './Redoc/elements';
export * from './Schema/';
export * from './Operation/Operation';
// re-export the rest of components

View File

@ -1,12 +1,13 @@
import * as React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
// import DevTools from 'mobx-react-devtools';
import { AppContainer } from 'react-hot-loader';
import { Redoc, RedocProps } from './components/Redoc';
import { Redoc } from './components/Redoc/Redoc';
import { AppStore } from './services/AppStore';
import { loadSpec } from './utils/loadSpec';
const renderRoot = (Component: typeof Redoc, props: RedocProps) =>
const renderRoot = (Component: typeof Redoc, props: { store: AppStore }) =>
render(
<div>
<AppContainer>
@ -17,26 +18,32 @@ const renderRoot = (Component: typeof Redoc, props: RedocProps) =>
);
const big = window.location.search.indexOf('big') > -1;
const props = {
specUrl: big ? 'big-swagger.json' : 'swagger.yaml',
store: new AppStore(),
};
renderRoot(Redoc, props);
const specUrl = big ? 'big-swagger.json' : 'swagger.yaml';
let store;
async function init() {
const spec = await loadSpec(specUrl);
store = new AppStore(spec, specUrl);
renderRoot(Redoc, { store: store });
}
init();
if (module.hot) {
const reload = (reloadStore = false) => () => {
if (reloadStore) {
// create a new Store
props.store.dispose();
store.dispose();
const state = props.store.toJS();
props.store = AppStore.fromJS(state);
const state = store.toJS();
store = AppStore.fromJS(state);
}
renderRoot(Redoc, props);
renderRoot(Redoc, { store: store });
};
module.hot.accept(['./components/Redoc'], reload());
module.hot.accept(['./components/Redoc/Redoc'], reload());
module.hot.accept(['./services/AppStore'], reload(true));
}

View File

@ -1,2 +1,20 @@
import { render } from 'react-dom';
import * as React from 'react';
import { RedocStandalone } from './components/Redoc';
export * from './components';
export * from './services';
export function init(specOrSpecUrl: string | any, options?: any, element?: Element) {
render(
React.createElement(
RedocStandalone,
{
specOrSpecUrl,
},
[],
),
element || document.querySelector('redoc'),
);
}

View File

@ -1,18 +1,27 @@
import { OpenAPISpec } from '../types';
import { SpecStore } from './models';
import { MenuStore } from './MenuStore';
import { ScrollService } from './ScrollService';
type StoreData = {
menu: {
activeItemIdx: number;
};
spec: {
url: string;
data: any;
};
};
export class AppStore {
menu: MenuStore;
scroll: ScrollService;
spec: SpecStore;
static i = 25;
// TODO: store serialization ???
private scroll: ScrollService;
constructor() {
constructor(spec: OpenAPISpec, specUrl?: string) {
this.scroll = new ScrollService();
this.spec = new SpecStore();
this.spec = new SpecStore(spec, specUrl);
this.menu = new MenuStore(this.spec, this.scroll);
}
@ -26,16 +35,14 @@ export class AppStore {
* **SUPER HACKY AND NOT OPTIMAL IMPLEMENTATION**
*/
// TODO:
toJS() {
toJS(): StoreData {
return {
menu: {
activeMenuIdx: this.menu.activeItemIdx,
activeItemIdx: this.menu.activeItemIdx,
},
spec: {
parser: {
specUrl: this.spec.parser.specUrl,
spec: this.spec.parser.spec,
},
url: this.spec.parser.specUrl,
data: this.spec.parser.spec,
},
};
}
@ -44,10 +51,8 @@ export class AppStore {
* **SUPER HACKY AND NOT OPTIMAL IMPLEMENTATION**
*/
// TODO:
static fromJS(state): AppStore {
const inst = new AppStore();
inst.spec.parser.specUrl = state.spec.parser.specUrl;
inst.spec.parser.spec = state.spec.parser.spec;
static fromJS(state: StoreData): AppStore {
const inst = new AppStore(state.spec.data, state.spec.url);
inst.menu.activeItemIdx = state.menu.activeItemIdx || 0;
inst.menu.activate(inst.menu.flatItems[inst.menu.activeItemIdx]);
return inst;

View File

@ -30,7 +30,7 @@ export class MenuBuilder {
* Builds page content structure based on tags
*/
static buildStructure(parser: OpenAPIParser): ContentItemModel[] {
const spec = parser.spec!;
const spec = parser.spec;
const items: ContentItemModel[] = [];
const tagsMap = MenuBuilder.getTagsWithOperations(spec);

View File

@ -102,7 +102,7 @@ export class MenuStore {
* @param hash current hash
*/
@action.bound
updateOnHash(hash: string): boolean {
updateOnHash(hash: string = HistoryService.hash): boolean {
if (!hash) return false;
let item: IMenuItem | undefined;
hash = hash.substr(1);
@ -155,9 +155,8 @@ export class MenuStore {
get flatItems(): IMenuItem[] {
if (!this._flatItems) {
this._flatItems = flattenByProp(this.items, 'items');
}
this._flatItems.forEach((item, idx) => (item.absoluteIdx = idx));
}
return this._flatItems;
}

View File

@ -1,5 +1,4 @@
import { action, computed, observable } from 'mobx';
import * as JsonSchemaRefParser from 'json-schema-ref-parser';
import { observable } from 'mobx';
import { resolve as urlResolve } from 'url';
import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
@ -38,44 +37,21 @@ class RefCounter {
*/
export class OpenAPIParser {
@observable specUrl: string;
@observable.ref spec?: OpenAPISpec;
private _parser: JsonSchemaRefParser;
private _refCounter: RefCounter = new RefCounter();
@computed
get loaded(): boolean {
return this.spec !== undefined;
}
/**
* loads and bundles the spec via url to spec or by providing spec itself.
* Async as bundling is async as spec may contain extrenal refs.
* @param urlOrObject url to the spec or the spec itself
*/
@action
async load(urlOrObject: string | object): Promise<OpenAPISpec> {
if (this.loaded) {
return this.spec!;
}
this._parser = new JsonSchemaRefParser();
if (typeof urlOrObject === 'string') {
this.specUrl = urlResolve(window.location.href, urlOrObject);
} else {
this.specUrl = window.location.href;
}
const spec = await this._parser.bundle(urlOrObject, {
resolve: { http: { withCredentials: false } },
} as object);
@observable.ref spec: OpenAPISpec;
constructor(spec: OpenAPISpec, specUrl?: string) {
this.validate(spec);
this.spec = spec;
return this.spec!;
if (typeof specUrl === 'string') {
this.specUrl = urlResolve(window.location.href, specUrl);
} else {
this.specUrl = window.location.href;
}
}
private _refCounter: RefCounter = new RefCounter();
validate(spec: any) {
// TODO: validate
@ -90,17 +66,12 @@ export class OpenAPIParser {
byRef = <T extends any = any>(ref: string): T | undefined => {
let res;
if (this.spec === undefined) return;
if (ref.charAt(0) !== '#') ref = '#' + ref;
try {
res = JsonPointer.get(this.spec, decodeURIComponent(ref));
} catch (e) {
// if resolved from outer files simple jsonpointer.get fails to get correct schema
if (ref.charAt(0) !== '#') ref = '#' + ref;
try {
res = this._parser.$refs.get(decodeURIComponent(ref));
} catch (e) {
// do nothing
}
}
return res;
};
@ -227,7 +198,7 @@ export class OpenAPIParser {
*/
findDerived($refs: string[]): Dict<string> {
const res: Dict<string> = {};
const schemas = (this.spec!.components && this.spec!.components!.schemas) || {};
const schemas = (this.spec.components && this.spec.components.schemas) || {};
for (let defName in schemas) {
const def = this.deref(schemas[defName]);
if (

View File

@ -1,3 +1,4 @@
import { OpenAPISpec } from '../types';
import { observable, computed } from 'mobx';
// import { OpenAPIExternalDocumentation, OpenAPIInfo } from '../types';
@ -12,34 +13,22 @@ import { ApiInfoModel } from './models/ApiInfo';
export class SpecStore {
@observable.ref parser: OpenAPIParser;
constructor() {
this.parser = new OpenAPIParser();
}
load(specOrUrl: string | object) {
return this.parser.load(specOrUrl);
constructor(spec: OpenAPISpec, specUrl?: string) {
this.parser = new OpenAPIParser(spec, specUrl);
}
@computed
get loaded() {
return this.parser.loaded;
}
@computed
get info() {
if (!this.parser.loaded) return;
get info(): ApiInfoModel {
return new ApiInfoModel(this.parser);
}
@computed
get externalDocs() {
if (this.parser.loaded) return;
return this.parser.spec!.externalDocs;
return this.parser.spec.externalDocs;
}
@computed
get operationGroups() {
if (!this.parser.loaded) return [];
return MenuBuilder.buildStructure(this.parser);
}

View File

@ -11,7 +11,7 @@ export class ApiInfoModel implements OpenAPIInfo {
license?: OpenAPILicense;
constructor(public parser: OpenAPIParser) {
Object.assign(this, parser.spec!.info);
Object.assign(this, parser.spec.info);
}
get downloadLink() {

View File

@ -79,13 +79,15 @@ export class OperationModel implements IMenuItem {
if (parseInt(code) >= 100 && parseInt(code) <= 399) {
hasSuccessResponses = true;
}
return isNumeric(code) || code === 'default'
return isNumeric(code) || code === 'default';
}) // filter out other props (e.g. x-props)
.map(code => new ResponseModel(parser, code, hasSuccessResponses, operationSpec.responses[code]));
.map(
code => new ResponseModel(parser, code, hasSuccessResponses, operationSpec.responses[code]),
);
this.servers = normalizeServers(
parser.specUrl,
operationSpec.servers || parser.spec!.servers || [],
operationSpec.servers || parser.spec.servers || [],
);
}

View File

@ -4,3 +4,4 @@ export * from './styled';
export * from './openapi';
export * from './helpers';
export * from './highlight';
export * from './loadSpec';

10
src/utils/loadSpec.ts Normal file
View File

@ -0,0 +1,10 @@
import { OpenAPISpec } from '../types';
import * as JsonSchemaRefParser from 'json-schema-ref-parser';
export async function loadSpec(specUrlOrObject: object | string): Promise<OpenAPISpec> {
const _parser = new JsonSchemaRefParser();
return await _parser.bundle(specUrlOrObject, {
resolve: { http: { withCredentials: false } },
} as object);
}

View File

@ -1036,10 +1036,6 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0:
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
change-emitter@^0.1.2:
version "0.1.6"
resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515"
cheerio@^1.0.0-rc.2:
version "1.0.0-rc.2"
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
@ -2431,7 +2427,7 @@ fb-watchman@^2.0.0:
dependencies:
bser "^2.0.0"
fbjs@^0.8.1, fbjs@^0.8.16, fbjs@^0.8.5, fbjs@^0.8.9:
fbjs@^0.8.16, fbjs@^0.8.5, fbjs@^0.8.9:
version "0.8.16"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
dependencies:
@ -5927,15 +5923,6 @@ readdirp@^2.0.0:
readable-stream "^2.0.2"
set-immediate-shim "^1.0.1"
recompose@^0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.25.1.tgz#5eb9d6cf6e25a9ffad73cbbae5658b5b55d6e728"
dependencies:
change-emitter "^0.1.2"
fbjs "^0.8.1"
hoist-non-react-statics "^2.3.1"
symbol-observable "^1.0.4"
recursive-readdir@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99"
@ -6835,10 +6822,6 @@ svgo@^0.7.0:
sax "~1.2.1"
whet.extend "~0.9.9"
symbol-observable@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
symbol-tree@^3.2.1:
version "3.2.2"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"