diff --git a/.vscode/settings.json b/.vscode/settings.json index 8d9c9bc0..ad92582b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "editor.formatOnSave": false -} \ No newline at end of file + "editor.formatOnSave": true +} diff --git a/package.json b/package.json index 9b283a25..15227895 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/common-elements/panels.ts b/src/common-elements/panels.ts index 6891ba77..be92d448 100644 --- a/src/common-elements/panels.ts +++ b/src/common-elements/panels.ts @@ -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%; +`; diff --git a/src/components/ApiInfo/ApiInfo.tsx b/src/components/ApiInfo/ApiInfo.tsx index 581a7c7c..38a00697 100644 --- a/src/components/ApiInfo/ApiInfo.tsx +++ b/src/components/ApiInfo/ApiInfo.tsx @@ -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,43 +66,46 @@ export class ApiInfo extends React.Component { null; return ( - - - {info.title} ({info.version}) - - {downloadLink && ( -

- Download OpenAPI specification: - - Download - -

- )} + + + + {info.title} ({info.version}) + + {downloadLink && ( +

+ Download OpenAPI specification: + + Download + +

+ )} - {((info.license || info.contact || info.termsOfService) && ( - - - {email} {website} {license} {terms} - - - )) || - null} + {((info.license || info.contact || info.termsOfService) && ( + + + {email} {website} {license} {terms} + + + )) || + null} - {(externalDocs && ( -

- {externalDocs.description || externalDocs.url} -

- )) || - null} + {(externalDocs && ( +

+ {externalDocs.description || externalDocs.url} +

+ )) || + null} -
- -
-
+
+ +
+ + + ); } } diff --git a/src/components/ApiInfo/ApiInfoContainer.tsx b/src/components/ApiInfo/ApiInfoContainer.tsx deleted file mode 100644 index 2f0a03aa..00000000 --- a/src/components/ApiInfo/ApiInfoContainer.tsx +++ /dev/null @@ -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 { - render() { - const { info, externalDocs } = this.props.store.spec; - return ; - } -} - -export default getContext({ - store: PropTypes.object, -})(ApiInfoContainer); diff --git a/src/components/ApiLogo/ApiLogo.tsx b/src/components/ApiLogo/ApiLogo.tsx index dd65f8de..8a431e39 100644 --- a/src/components/ApiLogo/ApiLogo.tsx +++ b/src/components/ApiLogo/ApiLogo.tsx @@ -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 => {Component}; @observer -class ApiLogo extends React.Component { +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 { return info.contact && info.contact.url ? LinkWrap(info.contact.url)(logo) : logo; } } - -export default getContext({ - store: PropTypes.object, -})(ApiLogo); diff --git a/src/components/ContentItems/ContentContainer.tsx b/src/components/ContentItems/ContentContainer.tsx deleted file mode 100644 index 71947332..00000000 --- a/src/components/ContentItems/ContentContainer.tsx +++ /dev/null @@ -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 { - render() { - const items = this.props.store.menu.items; - return ; - } -} - -export default getContext({ - store: PropTypes.object, -})(ContentContainer); diff --git a/src/components/ContentItems/ContentItems.tsx b/src/components/ContentItems/ContentItems.tsx index f0510bbd..f71e6b6f 100644 --- a/src/components/ContentItems/ContentItems.tsx +++ b/src/components/ContentItems/ContentItems.tsx @@ -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 { render() { const { name, description } = this.props.item; return ( - -

- - {name} -

- {description !== undefined && } -
+ + +

+ + {name} +

+ {description !== undefined && } +
+ +
); } } diff --git a/src/components/ContentRoot/ContentRoot.tsx b/src/components/ContentRoot/ContentRoot.tsx deleted file mode 100644 index 75770694..00000000 --- a/src/components/ContentRoot/ContentRoot.tsx +++ /dev/null @@ -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 ( - - - - - - -
- - - - - - - ); - } -} diff --git a/src/components/LoadingWrap/LoadingWrap.tsx b/src/components/LoadingWrap/LoadingWrap.tsx index 7c35558f..b276e9ca 100644 --- a/src/components/LoadingWrap/LoadingWrap.tsx +++ b/src/components/LoadingWrap/LoadingWrap.tsx @@ -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); diff --git a/src/components/Operation/Operation.tsx b/src/components/Operation/Operation.tsx index 04a8cda5..a2b1c596 100644 --- a/src/components/Operation/Operation.tsx +++ b/src/components/Operation/Operation.tsx @@ -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 { - + - + ); } diff --git a/src/components/Redoc.tsx b/src/components/Redoc.tsx deleted file mode 100644 index 5073583e..00000000 --- a/src/components/Redoc.tsx +++ /dev/null @@ -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 { - render() { - return ( - - - - - - - - - - ); - } -} diff --git a/src/components/Redoc/Redoc.tsx b/src/components/Redoc/Redoc.tsx new file mode 100644 index 00000000..48b1602e --- /dev/null +++ b/src/components/Redoc/Redoc.tsx @@ -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 { + componentDidMount() { + this.props.store.menu.updateOnHash(); + } + + render() { + const { store: { spec, menu }, options = {} } = this.props; + + return ( + + + + + + + + + ; + + + + ); + } +} diff --git a/src/components/ContentRoot/elements.tsx b/src/components/Redoc/elements.tsx similarity index 78% rename from src/components/ContentRoot/elements.tsx rename to src/components/Redoc/elements.tsx index 4fd498da..facaead6 100644 --- a/src/components/ContentRoot/elements.tsx +++ b/src/components/Redoc/elements.tsx @@ -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; - } -`; diff --git a/src/components/RedocStandalone.tsx b/src/components/RedocStandalone.tsx new file mode 100644 index 00000000..97a725e2 --- /dev/null +++ b/src/components/RedocStandalone.tsx @@ -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 { + render() { + const { specOrSpecUrl, options } = this.props; + let specUrl; + let spec; + + if (typeof specOrSpecUrl === 'string') { + specUrl = specOrSpecUrl; + } else if (typeof specOrSpecUrl === 'object') { + spec = specOrSpecUrl; + } + + return ( + + + {({ loading, store }) => ( + + + + )} + + + ); + } +} diff --git a/src/components/SideMenu/SideMenu.tsx b/src/components/SideMenu/SideMenu.tsx index 2c8255e0..dae55cbd 100644 --- a/src/components/SideMenu/SideMenu.tsx +++ b/src/components/SideMenu/SideMenu.tsx @@ -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 { +export class SideMenu extends React.Component<{ menu: MenuStore }> { render() { - const store = this.props.store.menu; + const store = this.props.menu; return ( @@ -21,10 +18,6 @@ class SideMenu extends React.Component { } activate = (item: IMenuItem) => { - this.props.store.menu.activateAndScroll(item, true); + this.props.menu.activateAndScroll(item, true); }; } - -export default getContext({ - store: PropTypes.object, -})(SideMenu); diff --git a/src/components/StoreProvider.ts b/src/components/StoreProvider.ts index b2558e52..b0b41991 100644 --- a/src/components/StoreProvider.ts +++ b/src/components/StoreProvider.ts @@ -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 { - store: AppStore; +interface SpecState { + error?: Error; + loading: boolean; + store?: AppStore; +} - static childContextTypes = { - store: PropTypes.object.isRequired, - }; +export class StoreProvider extends Component { + 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 { }); } - 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); } } diff --git a/src/components/index.ts b/src/components/index.ts index 333da738..7934b6c1 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -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 diff --git a/src/hmr-playground.tsx b/src/hmr-playground.tsx index a24f860c..acd81425 100644 --- a/src/hmr-playground.tsx +++ b/src/hmr-playground.tsx @@ -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(
@@ -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)); } diff --git a/src/index.ts b/src/index.ts index 80e3bea6..2ee9aeb9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'), + ); +} diff --git a/src/services/AppStore.ts b/src/services/AppStore.ts index 00ba6633..e9a8993d 100644 --- a/src/services/AppStore.ts +++ b/src/services/AppStore.ts @@ -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; diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts index 7f8fa65b..35892b76 100644 --- a/src/services/MenuBuilder.ts +++ b/src/services/MenuBuilder.ts @@ -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); diff --git a/src/services/MenuStore.ts b/src/services/MenuStore.ts index eeb2bb28..01e2f841 100644 --- a/src/services/MenuStore.ts +++ b/src/services/MenuStore.ts @@ -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,10 +155,9 @@ export class MenuStore { get flatItems(): IMenuItem[] { if (!this._flatItems) { this._flatItems = flattenByProp(this.items, 'items'); + this._flatItems.forEach((item, idx) => (item.absoluteIdx = idx)); } - this._flatItems.forEach((item, idx) => (item.absoluteIdx = idx)); - return this._flatItems; } diff --git a/src/services/OpenAPIParser.ts b/src/services/OpenAPIParser.ts index d5827dc2..c87be913 100644 --- a/src/services/OpenAPIParser.ts +++ b/src/services/OpenAPIParser.ts @@ -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,45 +37,22 @@ 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 { - 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 if (spec.openapi === undefined) { @@ -90,16 +66,11 @@ export class OpenAPIParser { byRef = (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 - } + // do nothing } return res; }; @@ -227,7 +198,7 @@ export class OpenAPIParser { */ findDerived($refs: string[]): Dict { const res: Dict = {}; - 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 ( diff --git a/src/services/SpecStore.ts b/src/services/SpecStore.ts index b5afbf92..2ff040f8 100644 --- a/src/services/SpecStore.ts +++ b/src/services/SpecStore.ts @@ -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); } diff --git a/src/services/models/ApiInfo.ts b/src/services/models/ApiInfo.ts index 3919f598..9e87cdce 100644 --- a/src/services/models/ApiInfo.ts +++ b/src/services/models/ApiInfo.ts @@ -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() { diff --git a/src/services/models/Operation.ts b/src/services/models/Operation.ts index 5c650f0b..cab0600c 100644 --- a/src/services/models/Operation.ts +++ b/src/services/models/Operation.ts @@ -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 || [], ); } diff --git a/src/utils/index.ts b/src/utils/index.ts index 4c5d506e..c13c47a1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -4,3 +4,4 @@ export * from './styled'; export * from './openapi'; export * from './helpers'; export * from './highlight'; +export * from './loadSpec'; diff --git a/src/utils/loadSpec.ts b/src/utils/loadSpec.ts new file mode 100644 index 00000000..34de82e7 --- /dev/null +++ b/src/utils/loadSpec.ts @@ -0,0 +1,10 @@ +import { OpenAPISpec } from '../types'; + +import * as JsonSchemaRefParser from 'json-schema-ref-parser'; + +export async function loadSpec(specUrlOrObject: object | string): Promise { + const _parser = new JsonSchemaRefParser(); + return await _parser.bundle(specUrlOrObject, { + resolve: { http: { withCredentials: false } }, + } as object); +} diff --git a/yarn.lock b/yarn.lock index 31a0ed69..4dda6933 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"