refactor(oas31): concentrate OpenAPI 3.1.0 code to separate plugin (#8475)

Refs #8474
This commit is contained in:
Vladimír Gorej
2023-03-16 12:05:19 +01:00
committed by GitHub
parent ceccb218d3
commit 8b274414ab
35 changed files with 929 additions and 699 deletions

View File

@@ -0,0 +1,45 @@
/**
* @prettier
*/
import React from "react"
import PropTypes from "prop-types"
import { safeBuildUrl } from "core/utils/url"
import { sanitizeUrl } from "core/utils"
class Contact extends React.Component {
static propTypes = {
data: PropTypes.object,
getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
selectedServer: PropTypes.string,
url: PropTypes.string.isRequired,
}
render() {
const { data, getComponent, selectedServer, url: specUrl } = this.props
const name = data.get("name", "the developer")
const url = safeBuildUrl(data.get("url"), specUrl, { selectedServer })
const email = data.get("email")
const Link = getComponent("Link")
return (
<div className="info__contact">
{url && (
<div>
<Link href={sanitizeUrl(url)} target="_blank">
{name} - Website
</Link>
</div>
)}
{email && (
<Link href={sanitizeUrl(`mailto:${email}`)}>
{url ? `Send email to ${name}` : `Contact ${name}`}
</Link>
)}
</div>
)
}
}
export default Contact

View File

@@ -1,102 +1,53 @@
/**
* @prettier
*/
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes" import ImPropTypes from "react-immutable-proptypes"
import { sanitizeUrl } from "core/utils" import { sanitizeUrl } from "core/utils"
import { safeBuildUrl } from "core/utils/url" import { safeBuildUrl } from "core/utils/url"
export class InfoBasePath extends React.Component { export class InfoBasePath extends React.Component {
static propTypes = { static propTypes = {
host: PropTypes.string, host: PropTypes.string,
basePath: PropTypes.string basePath: PropTypes.string,
} }
render() { render() {
let { host, basePath } = this.props const { host, basePath } = this.props
return ( return (
<pre className="base-url"> <pre className="base-url">
[ Base URL: {host}{basePath} ] [ Base URL: {host}
{basePath} ]
</pre> </pre>
) )
} }
} }
export class Contact extends React.Component {
static propTypes = {
data: PropTypes.object,
getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
selectedServer: PropTypes.string,
url: PropTypes.string.isRequired,
}
render(){
let { data, getComponent, selectedServer, url: specUrl} = this.props
let name = data.get("name") || "the developer"
let url = safeBuildUrl(data.get("url"), specUrl, {selectedServer})
let email = data.get("email")
const Link = getComponent("Link")
return (
<div className="info__contact">
{ url && <div><Link href={ sanitizeUrl(url) } target="_blank">{ name } - Website</Link></div> }
{ email &&
<Link href={sanitizeUrl(`mailto:${email}`)}>
{ url ? `Send email to ${name}` : `Contact ${name}`}
</Link>
}
</div>
)
}
}
export class License extends React.Component {
static propTypes = {
license: PropTypes.object,
getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
selectedServer: PropTypes.string,
url: PropTypes.string.isRequired,
}
render(){
let { license, getComponent, selectedServer, url: specUrl } = this.props
const Link = getComponent("Link")
let name = license.get("name") || "License"
let url = safeBuildUrl(license.get("url"), specUrl, {selectedServer})
return (
<div className="info__license">
{
url ? <Link target="_blank" href={ sanitizeUrl(url) }>{ name }</Link>
: <span>{ name }</span>
}
</div>
)
}
}
export class InfoUrl extends React.PureComponent { export class InfoUrl extends React.PureComponent {
static propTypes = { static propTypes = {
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
getComponent: PropTypes.func.isRequired getComponent: PropTypes.func.isRequired,
} }
render() { render() {
const { url, getComponent } = this.props const { url, getComponent } = this.props
const Link = getComponent("Link") const Link = getComponent("Link")
return <Link target="_blank" href={ sanitizeUrl(url) }><span className="url"> { url }</span></Link> return (
<Link target="_blank" href={sanitizeUrl(url)}>
<span className="url"> {url}</span>
</Link>
)
} }
} }
export default class Info extends React.Component { class Info extends React.Component {
static propTypes = { static propTypes = {
title: PropTypes.any,
description: PropTypes.any,
version: PropTypes.any,
info: PropTypes.object, info: PropTypes.object,
url: PropTypes.string, url: PropTypes.string,
host: PropTypes.string, host: PropTypes.string,
@@ -108,16 +59,32 @@ export default class Info extends React.Component {
} }
render() { render() {
let { info, url, host, basePath, getComponent, externalDocs, selectedServer, url: specUrl } = this.props const {
let version = info.get("version") info,
let description = info.get("description") url,
let title = info.get("title") host,
let termsOfServiceUrl = safeBuildUrl(info.get("termsOfService"), specUrl, {selectedServer}) basePath,
let contact = info.get("contact") getComponent,
let license = info.get("license") externalDocs,
let rawExternalDocsUrl = externalDocs && externalDocs.get("url") selectedServer,
let externalDocsUrl = safeBuildUrl(rawExternalDocsUrl, specUrl, {selectedServer}) url: specUrl,
let externalDocsDescription = externalDocs && externalDocs.get("description") } = this.props
const version = info.get("version")
const description = info.get("description")
const title = info.get("title")
const termsOfServiceUrl = safeBuildUrl(
info.get("termsOfService"),
specUrl,
{ selectedServer }
)
const contactData = info.get("contact")
const licenseData = info.get("license")
const rawExternalDocsUrl = externalDocs && externalDocs.get("url")
const externalDocsUrl = safeBuildUrl(rawExternalDocsUrl, specUrl, {
selectedServer,
})
const externalDocsDescription =
externalDocs && externalDocs.get("description")
const Markdown = getComponent("Markdown", true) const Markdown = getComponent("Markdown", true)
const Link = getComponent("Link") const Link = getComponent("Link")
@@ -125,14 +92,18 @@ export default class Info extends React.Component {
const InfoUrl = getComponent("InfoUrl") const InfoUrl = getComponent("InfoUrl")
const InfoBasePath = getComponent("InfoBasePath") const InfoBasePath = getComponent("InfoBasePath")
const License = getComponent("License") const License = getComponent("License")
const Contact = getComponent("Contact")
return ( return (
<div className="info"> <div className="info">
<hgroup className="main"> <hgroup className="main">
<h2 className="title" >{ title } <h2 className="title">
{title}
{version && <VersionStamp version={version}></VersionStamp>} {version && <VersionStamp version={version}></VersionStamp>}
</h2> </h2>
{ host || basePath ? <InfoBasePath host={ host } basePath={ basePath } /> : null } {host || basePath ? (
<InfoBasePath host={host} basePath={basePath} />
) : null}
{url && <InfoUrl getComponent={getComponent} url={url} />} {url && <InfoUrl getComponent={getComponent} url={url} />}
</hgroup> </hgroup>
@@ -140,27 +111,42 @@ export default class Info extends React.Component {
<Markdown source={description} /> <Markdown source={description} />
</div> </div>
{ {termsOfServiceUrl && (
termsOfServiceUrl && <div className="info__tos"> <div className="info__tos">
<Link target="_blank" href={ sanitizeUrl(termsOfServiceUrl) }>Terms of service</Link> <Link target="_blank" href={sanitizeUrl(termsOfServiceUrl)}>
Terms of service
</Link>
</div> </div>
} )}
{contact && contact.size ? <Contact getComponent={getComponent} data={ contact } selectedServer={selectedServer} url={url} /> : null }
{license && license.size ? <License getComponent={getComponent} license={ license } selectedServer={selectedServer} url={url}/> : null }
{ externalDocsUrl ?
<Link className="info__extdocs" target="_blank" href={sanitizeUrl(externalDocsUrl)}>{externalDocsDescription || externalDocsUrl}</Link>
: null }
{contactData?.size > 0 && (
<Contact
getComponent={getComponent}
data={contactData}
selectedServer={selectedServer}
url={url}
/>
)}
{licenseData?.size > 0 && (
<License
getComponent={getComponent}
license={licenseData}
selectedServer={selectedServer}
url={url}
/>
)}
{externalDocsUrl ? (
<Link
className="info__extdocs"
target="_blank"
href={sanitizeUrl(externalDocsUrl)}
>
{externalDocsDescription || externalDocsUrl}
</Link>
) : null}
</div> </div>
) )
} }
} }
Info.propTypes = { export default Info
title: PropTypes.any,
description: PropTypes.any,
version: PropTypes.any,
url: PropTypes.string
}

View File

@@ -1,37 +1,39 @@
/**
* @prettier
*/
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
export default class BaseLayout extends React.Component { export default class BaseLayout extends React.Component {
static propTypes = { static propTypes = {
errSelectors: PropTypes.object.isRequired, errSelectors: PropTypes.object.isRequired,
errActions: PropTypes.object.isRequired, errActions: PropTypes.object.isRequired,
specSelectors: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired,
oas3Selectors: PropTypes.object.isRequired, oas3Selectors: PropTypes.object.isRequired,
oas3Actions: PropTypes.object.isRequired, oas3Actions: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired getComponent: PropTypes.func.isRequired,
} }
render() { render() {
let {errSelectors, specSelectors, getComponent} = this.props const { errSelectors, specSelectors, getComponent } = this.props
let SvgAssets = getComponent("SvgAssets") const SvgAssets = getComponent("SvgAssets")
let InfoContainer = getComponent("InfoContainer", true) const InfoContainer = getComponent("InfoContainer", true)
let VersionPragmaFilter = getComponent("VersionPragmaFilter") const VersionPragmaFilter = getComponent("VersionPragmaFilter")
let Operations = getComponent("operations", true) const Operations = getComponent("operations", true)
let Models = getComponent("Models", true) const Models = getComponent("Models", true)
let Webhooks = getComponent("Webhooks", true) const Webhooks = getComponent("Webhooks", true)
let Row = getComponent("Row") const Row = getComponent("Row")
let Col = getComponent("Col") const Col = getComponent("Col")
let Errors = getComponent("errors", true) const Errors = getComponent("errors", true)
const ServersContainer = getComponent("ServersContainer", true) const ServersContainer = getComponent("ServersContainer", true)
const SchemesContainer = getComponent("SchemesContainer", true) const SchemesContainer = getComponent("SchemesContainer", true)
const AuthorizeBtnContainer = getComponent("AuthorizeBtnContainer", true) const AuthorizeBtnContainer = getComponent("AuthorizeBtnContainer", true)
const FilterContainer = getComponent("FilterContainer", true) const FilterContainer = getComponent("FilterContainer", true)
let isSwagger2 = specSelectors.isSwagger2() const isSwagger2 = specSelectors.isSwagger2()
let isOAS3 = specSelectors.isOAS3() const isOAS3 = specSelectors.isOAS3()
const isOpenAPI31 = specSelectors.selectIsOpenAPI31() const isOAS31 = specSelectors.isOAS31()
const isSpecEmpty = !specSelectors.specStr() const isSpecEmpty = !specSelectors.specStr()
@@ -40,31 +42,37 @@ export default class BaseLayout extends React.Component {
let loadingMessage = null let loadingMessage = null
if (loadingStatus === "loading") { if (loadingStatus === "loading") {
loadingMessage = <div className="info"> loadingMessage = (
<div className="info">
<div className="loading-container"> <div className="loading-container">
<div className="loading"></div> <div className="loading"></div>
</div> </div>
</div> </div>
)
} }
if (loadingStatus === "failed") { if (loadingStatus === "failed") {
loadingMessage = <div className="info"> loadingMessage = (
<div className="info">
<div className="loading-container"> <div className="loading-container">
<h4 className="title">Failed to load API definition.</h4> <h4 className="title">Failed to load API definition.</h4>
<Errors /> <Errors />
</div> </div>
</div> </div>
)
} }
if (loadingStatus === "failedConfig") { if (loadingStatus === "failedConfig") {
const lastErr = errSelectors.lastError() const lastErr = errSelectors.lastError()
const lastErrMsg = lastErr ? lastErr.get("message") : "" const lastErrMsg = lastErr ? lastErr.get("message") : ""
loadingMessage = <div className="info failed-config"> loadingMessage = (
<div className="info failed-config">
<div className="loading-container"> <div className="loading-container">
<h4 className="title">Failed to load remote configuration.</h4> <h4 className="title">Failed to load remote configuration.</h4>
<p>{lastErrMsg}</p> <p>{lastErrMsg}</p>
</div> </div>
</div> </div>
)
} }
if (!loadingMessage && isSpecEmpty) { if (!loadingMessage && isSpecEmpty) {
@@ -72,11 +80,11 @@ export default class BaseLayout extends React.Component {
} }
if (loadingMessage) { if (loadingMessage) {
return <div className="swagger-ui"> return (
<div className="loading-container"> <div className="swagger-ui">
{loadingMessage} <div className="loading-container">{loadingMessage}</div>
</div>
</div> </div>
)
} }
const servers = specSelectors.servers() const servers = specSelectors.servers()
@@ -87,9 +95,13 @@ export default class BaseLayout extends React.Component {
const hasSecurityDefinitions = !!specSelectors.securityDefinitions() const hasSecurityDefinitions = !!specSelectors.securityDefinitions()
return ( return (
<div className='swagger-ui'> <div className="swagger-ui">
<SvgAssets /> <SvgAssets />
<VersionPragmaFilter isSwagger2={isSwagger2} isOAS3={isOAS3} alsoShow={<Errors/>}> <VersionPragmaFilter
isSwagger2={isSwagger2}
isOAS3={isOAS3}
alsoShow={<Errors />}
>
<Errors /> <Errors />
<Row className="information-container"> <Row className="information-container">
<Col mobile={12}> <Col mobile={12}>
@@ -100,9 +112,9 @@ export default class BaseLayout extends React.Component {
{hasServers || hasSchemes || hasSecurityDefinitions ? ( {hasServers || hasSchemes || hasSecurityDefinitions ? (
<div className="scheme-container"> <div className="scheme-container">
<Col className="schemes wrapper" mobile={12}> <Col className="schemes wrapper" mobile={12}>
{hasServers ? (<ServersContainer />) : null} {hasServers ? <ServersContainer /> : null}
{hasSchemes ? (<SchemesContainer />) : null} {hasSchemes ? <SchemesContainer /> : null}
{hasSecurityDefinitions ? (<AuthorizeBtnContainer />) : null} {hasSecurityDefinitions ? <AuthorizeBtnContainer /> : null}
</Col> </Col>
</div> </div>
) : null} ) : null}
@@ -114,13 +126,13 @@ export default class BaseLayout extends React.Component {
<Operations /> <Operations />
</Col> </Col>
</Row> </Row>
{ isOpenAPI31 && {isOAS31 && (
<Row className="webhooks-container"> <Row className="webhooks-container">
<Col mobile={12} desktop={12}> <Col mobile={12} desktop={12}>
<Webhooks /> <Webhooks />
</Col> </Col>
</Row> </Row>
} )}
<Row> <Row>
<Col mobile={12} desktop={12}> <Col mobile={12} desktop={12}>
<Models /> <Models />

View File

@@ -0,0 +1,41 @@
/**
* @prettier
*/
import React from "react"
import PropTypes from "prop-types"
import { safeBuildUrl } from "core/utils/url"
import { sanitizeUrl } from "core/utils"
class License extends React.Component {
static propTypes = {
license: PropTypes.object,
getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
selectedServer: PropTypes.string,
url: PropTypes.string.isRequired,
}
render() {
const { license, getComponent, selectedServer, url: specUrl } = this.props
const name = license.get("name", "License")
const url = safeBuildUrl(license.get("url"), specUrl, { selectedServer })
const Link = getComponent("Link")
return (
<div className="info__license">
{url ? (
<div className="info__license__url">
<Link target="_blank" href={sanitizeUrl(url)}>
{name}
</Link>
</div>
) : (
<span>{name}</span>
)}
</div>
)
}
}
export default License

View File

@@ -1,6 +1,5 @@
import { createSelector } from "reselect" import { createSelector } from "reselect"
import { List, Map, fromJS } from "immutable" import { List, Map, fromJS } from "immutable"
import { isOAS3 as isOAS3Helper } from "../helpers"
// Helpers // Helpers
@@ -9,8 +8,7 @@ const state = state => state
function onlyOAS3(selector) { function onlyOAS3(selector) {
return (ori, system) => (...args) => { return (ori, system) => (...args) => {
const spec = system.getSystem().specSelectors.specJson() if(system.getSystem().specSelectors.isOAS3()) {
if(isOAS3Helper(spec)) {
// Pass the spec plugin state to Reselect to trigger on securityDefinitions update // Pass the spec plugin state to Reselect to trigger on securityDefinitions update
let resolvedSchemes = system.getState().getIn(["spec", "resolvedSubtrees", let resolvedSchemes = system.getState().getIn(["spec", "resolvedSubtrees",
"components", "securitySchemes"]) "components", "securitySchemes"])

View File

@@ -6,7 +6,6 @@ import ServersContainer from "./servers-container"
import RequestBodyEditor from "./request-body-editor" import RequestBodyEditor from "./request-body-editor"
import HttpAuth from "./http-auth" import HttpAuth from "./http-auth"
import OperationServers from "./operation-servers" import OperationServers from "./operation-servers"
import Webhooks from "./webhooks"
export default { export default {
Callbacks, Callbacks,
@@ -17,5 +16,4 @@ export default {
RequestBodyEditor, RequestBodyEditor,
OperationServers, OperationServers,
operationLink: OperationLink, operationLink: OperationLink,
Webhooks
} }

View File

@@ -1,44 +1,27 @@
/**
* @prettier
*/
import React from "react" import React from "react"
export function isOpenAPI30(jsSpec) { export function isOAS30(jsSpec) {
const oasVersion = jsSpec.get("openapi") const oasVersion = jsSpec.get("openapi")
if (typeof oasVersion !== "string") {
return false
}
return oasVersion.startsWith("3.0.") && oasVersion.length > 4
}
export function isOpenAPI31(jsSpec) { return (
const oasVersion = jsSpec.get("openapi") typeof oasVersion === "string" &&
if (typeof oasVersion !== "string") { /^3\.0\.([0123])(?:-rc[012])?$/.test(oasVersion)
return false )
}
return oasVersion.startsWith("3.1.") && oasVersion.length > 4
}
export function isOAS3(jsSpec) {
const oasVersion = jsSpec.get("openapi")
if(typeof oasVersion !== "string") {
return false
}
return isOpenAPI30(jsSpec) || isOpenAPI31(jsSpec)
} }
export function isSwagger2(jsSpec) { export function isSwagger2(jsSpec) {
const swaggerVersion = jsSpec.get("swagger") const swaggerVersion = jsSpec.get("swagger")
if(typeof swaggerVersion !== "string") {
return false
}
return swaggerVersion.startsWith("2.0") return typeof swaggerVersion === "string" && swaggerVersion === "2.0"
} }
export function OAS3ComponentWrapFactory(Component) { export function OAS3ComponentWrapFactory(Component) {
return (Ori, system) => (props) => { return (Ori, system) => (props) => {
if(system && system.specSelectors && system.specSelectors.specJson) { if (typeof system.specSelectors?.isOAS3 === "function") {
const spec = system.specSelectors.specJson() if (system.specSelectors.isOAS3()) {
if(isOAS3(spec)) {
return <Component {...props} {...system} Ori={Ori}></Component> return <Component {...props} {...system} Ori={Ori}></Component>
} else { } else {
return <Ori {...props}></Ori> return <Ori {...props}></Ori>

View File

@@ -1,13 +1,14 @@
// import reducers from "./reducers" /**
// import * as actions from "./actions" * @prettier
*/
import * as specWrapSelectors from "./spec-extensions/wrap-selectors" import * as specWrapSelectors from "./spec-extensions/wrap-selectors"
import * as authWrapSelectors from "./auth-extensions/wrap-selectors" import * as authWrapSelectors from "./auth-extensions/wrap-selectors"
import * as specSelectors from "./spec-extensions/selectors" import * as specSelectors from "./spec-extensions/selectors"
import components from "./components" import components from "./components"
import wrapComponents from "./wrap-components" import wrapComponents from "./wrap-components"
import * as oas3Actions from "./actions" import * as actions from "./actions"
import * as oas3Selectors from "./selectors" import * as selectors from "./selectors"
import oas3Reducers from "./reducers" import reducers from "./reducers"
export default function () { export default function () {
return { return {
@@ -16,16 +17,16 @@ export default function() {
statePlugins: { statePlugins: {
spec: { spec: {
wrapSelectors: specWrapSelectors, wrapSelectors: specWrapSelectors,
selectors: specSelectors selectors: specSelectors,
}, },
auth: { auth: {
wrapSelectors: authWrapSelectors wrapSelectors: authWrapSelectors,
}, },
oas3: { oas3: {
actions: oas3Actions, actions,
reducers: oas3Reducers, reducers,
selectors: oas3Selectors, selectors,
} },
} },
} }
} }

View File

@@ -1,14 +1,16 @@
/**
* @prettier
*/
import { OrderedMap, Map, List } from "immutable" import { OrderedMap, Map, List } from "immutable"
import { isOAS3 as isOAS3Helper } from "./helpers"
import { getDefaultRequestBodyValue } from "./components/request-body" import { getDefaultRequestBodyValue } from "./components/request-body"
import { stringify } from "../../utils" import { stringify } from "../../utils"
// Helpers // Helpers
function onlyOAS3(selector) { function onlyOAS3(selector) {
return (...args) => (system) => { return (...args) =>
const spec = system.getSystem().specSelectors.specJson() (system) => {
if(isOAS3Helper(spec)) { if (system.getSystem().specSelectors.isOAS3()) {
return selector(...args) return selector(...args)
} else { } else {
return null return null
@@ -17,12 +19,18 @@ function onlyOAS3(selector) {
} }
function validateRequestBodyIsRequired(selector) { function validateRequestBodyIsRequired(selector) {
return (...args) => (system) => { return (...args) =>
(system) => {
const specJson = system.getSystem().specSelectors.specJson() const specJson = system.getSystem().specSelectors.specJson()
const argsList = [...args] const argsList = [...args]
// expect argsList[0] = state // expect argsList[0] = state
let pathMethod = argsList[1] || [] let pathMethod = argsList[1] || []
let isOas3RequestBodyRequired = specJson.getIn(["paths", ...pathMethod, "requestBody", "required"]) let isOas3RequestBodyRequired = specJson.getIn([
"paths",
...pathMethod,
"requestBody",
"required",
])
if (isOas3RequestBodyRequired) { if (isOas3RequestBodyRequired) {
return selector(...args) return selector(...args)
@@ -35,7 +43,11 @@ function validateRequestBodyIsRequired(selector) {
const validateRequestBodyValueExists = (state, pathMethod) => { const validateRequestBodyValueExists = (state, pathMethod) => {
pathMethod = pathMethod || [] pathMethod = pathMethod || []
let oas3RequestBodyValue = state.getIn(["requestData", ...pathMethod, "bodyValue"]) let oas3RequestBodyValue = state.getIn([
"requestData",
...pathMethod,
"bodyValue",
])
// context: bodyValue can be a String, or a Map // context: bodyValue can be a String, or a Map
if (!oas3RequestBodyValue) { if (!oas3RequestBodyValue) {
return false return false
@@ -44,36 +56,39 @@ const validateRequestBodyValueExists = (state, pathMethod) => {
return true return true
} }
export const selectedServer = onlyOAS3((state, namespace) => { export const selectedServer = onlyOAS3((state, namespace) => {
const path = namespace ? [namespace, "selectedServer"] : ["selectedServer"] const path = namespace ? [namespace, "selectedServer"] : ["selectedServer"]
return state.getIn(path) || "" return state.getIn(path) || ""
} })
)
export const requestBodyValue = onlyOAS3((state, path, method) => { export const requestBodyValue = onlyOAS3((state, path, method) => {
return state.getIn(["requestData", path, method, "bodyValue"]) || null return state.getIn(["requestData", path, method, "bodyValue"]) || null
} })
)
export const shouldRetainRequestBodyValue = onlyOAS3((state, path, method) => { export const shouldRetainRequestBodyValue = onlyOAS3((state, path, method) => {
return state.getIn(["requestData", path, method, "retainBodyValue"]) || false return state.getIn(["requestData", path, method, "retainBodyValue"]) || false
} })
)
export const selectDefaultRequestBodyValue = (state, path, method) => (system) => { export const selectDefaultRequestBodyValue =
(state, path, method) => (system) => {
const { oas3Selectors, specSelectors } = system.getSystem() const { oas3Selectors, specSelectors } = system.getSystem()
const spec = specSelectors.specJson()
if(isOAS3Helper(spec)) { if (specSelectors.isOAS3()) {
const currentMediaType = oas3Selectors.requestContentType(path, method) const currentMediaType = oas3Selectors.requestContentType(path, method)
if (currentMediaType) { if (currentMediaType) {
return getDefaultRequestBodyValue( return getDefaultRequestBodyValue(
specSelectors.specResolvedSubtree(["paths", path, method, "requestBody"]), specSelectors.specResolvedSubtree([
"paths",
path,
method,
"requestBody",
]),
currentMediaType, currentMediaType,
oas3Selectors.activeExamplesMember( oas3Selectors.activeExamplesMember(
path, method, path,
"requestBody", method,
"requestBody", "requestBody",
"requestBody"
) )
) )
} }
@@ -83,29 +98,43 @@ export const selectDefaultRequestBodyValue = (state, path, method) => (system) =
export const hasUserEditedBody = (state, path, method) => (system) => { export const hasUserEditedBody = (state, path, method) => (system) => {
const { oas3Selectors, specSelectors } = system.getSystem() const { oas3Selectors, specSelectors } = system.getSystem()
const spec = specSelectors.specJson()
if(isOAS3Helper(spec)) { if (specSelectors.isOAS3()) {
let userHasEditedBody = false let userHasEditedBody = false
const currentMediaType = oas3Selectors.requestContentType(path, method) const currentMediaType = oas3Selectors.requestContentType(path, method)
let userEditedRequestBody = oas3Selectors.requestBodyValue(path, method) let userEditedRequestBody = oas3Selectors.requestBodyValue(path, method)
if (Map.isMap(userEditedRequestBody)) { if (Map.isMap(userEditedRequestBody)) {
// context is not application/json media-type // context is not application/json media-type
userEditedRequestBody = stringify(userEditedRequestBody.mapEntries((kv) => Map.isMap(kv[1]) ? [kv[0], kv[1].get("value")] : kv).toJS()) userEditedRequestBody = stringify(
userEditedRequestBody
.mapEntries((kv) =>
Map.isMap(kv[1]) ? [kv[0], kv[1].get("value")] : kv
)
.toJS()
)
} }
if (List.isList(userEditedRequestBody)) { if (List.isList(userEditedRequestBody)) {
userEditedRequestBody = stringify(userEditedRequestBody) userEditedRequestBody = stringify(userEditedRequestBody)
} }
if (currentMediaType) { if (currentMediaType) {
const currentMediaTypeDefaultBodyValue = getDefaultRequestBodyValue( const currentMediaTypeDefaultBodyValue = getDefaultRequestBodyValue(
specSelectors.specResolvedSubtree(["paths", path, method, "requestBody"]), specSelectors.specResolvedSubtree([
"paths",
path,
method,
"requestBody",
]),
currentMediaType, currentMediaType,
oas3Selectors.activeExamplesMember( oas3Selectors.activeExamplesMember(
path, method, path,
"requestBody", method,
"requestBody", "requestBody",
"requestBody"
) )
) )
userHasEditedBody = !!userEditedRequestBody && userEditedRequestBody !== currentMediaTypeDefaultBodyValue userHasEditedBody =
!!userEditedRequestBody &&
userEditedRequestBody !== currentMediaTypeDefaultBodyValue
} }
return userHasEditedBody return userHasEditedBody
} else { } else {
@@ -115,28 +144,32 @@ export const hasUserEditedBody = (state, path, method) => (system) => {
export const requestBodyInclusionSetting = onlyOAS3((state, path, method) => { export const requestBodyInclusionSetting = onlyOAS3((state, path, method) => {
return state.getIn(["requestData", path, method, "bodyInclusion"]) || Map() return state.getIn(["requestData", path, method, "bodyInclusion"]) || Map()
} })
)
export const requestBodyErrors = onlyOAS3((state, path, method) => { export const requestBodyErrors = onlyOAS3((state, path, method) => {
return state.getIn(["requestData", path, method, "errors"]) || null return state.getIn(["requestData", path, method, "errors"]) || null
} })
)
export const activeExamplesMember = onlyOAS3((state, path, method, type, name) => { export const activeExamplesMember = onlyOAS3(
return state.getIn(["examples", path, method, type, name, "activeExample"]) || null (state, path, method, type, name) => {
return (
state.getIn(["examples", path, method, type, name, "activeExample"]) ||
null
)
} }
) )
export const requestContentType = onlyOAS3((state, path, method) => { export const requestContentType = onlyOAS3((state, path, method) => {
return state.getIn(["requestData", path, method, "requestContentType"]) || null return (
} state.getIn(["requestData", path, method, "requestContentType"]) || null
) )
})
export const responseContentType = onlyOAS3((state, path, method) => { export const responseContentType = onlyOAS3((state, path, method) => {
return state.getIn(["requestData", path, method, "responseContentType"]) || null return (
} state.getIn(["requestData", path, method, "responseContentType"]) || null
) )
})
export const serverVariableValue = onlyOAS3((state, locationData, key) => { export const serverVariableValue = onlyOAS3((state, locationData, key) => {
let path let path
@@ -156,8 +189,7 @@ export const serverVariableValue = onlyOAS3((state, locationData, key) => {
} }
return state.getIn(path) || null return state.getIn(path) || null
} })
)
export const serverVariables = onlyOAS3((state, locationData) => { export const serverVariables = onlyOAS3((state, locationData) => {
let path let path
@@ -177,8 +209,7 @@ export const serverVariables = onlyOAS3((state, locationData) => {
} }
return state.getIn(path) || OrderedMap() return state.getIn(path) || OrderedMap()
} })
)
export const serverEffectiveValue = onlyOAS3((state, locationData) => { export const serverEffectiveValue = onlyOAS3((state, locationData) => {
var varValues, serverValue var varValues, serverValue
@@ -206,14 +237,20 @@ export const serverEffectiveValue = onlyOAS3((state, locationData) => {
}) })
return str return str
} })
)
export const validateBeforeExecute = validateRequestBodyIsRequired( export const validateBeforeExecute = validateRequestBodyIsRequired(
(state, pathMethod) => validateRequestBodyValueExists(state, pathMethod) (state, pathMethod) => validateRequestBodyValueExists(state, pathMethod)
) )
export const validateShallowRequired = (state, { oas3RequiredRequestBodyContentType, oas3RequestContentType, oas3RequestBodyValue} ) => { export const validateShallowRequired = (
state,
{
oas3RequiredRequestBodyContentType,
oas3RequestContentType,
oas3RequestBodyValue,
}
) => {
let missingRequiredKeys = [] let missingRequiredKeys = []
// context: json => String; urlencoded, form-data => Map // context: json => String; urlencoded, form-data => Map
if (!Map.isMap(oas3RequestBodyValue)) { if (!Map.isMap(oas3RequestBodyValue)) {
@@ -221,16 +258,19 @@ export const validateShallowRequired = (state, { oas3RequiredRequestBodyContentT
} }
let requiredKeys = [] let requiredKeys = []
// Cycle through list of possible contentTypes for matching contentType and defined requiredKeys // Cycle through list of possible contentTypes for matching contentType and defined requiredKeys
Object.keys(oas3RequiredRequestBodyContentType.requestContentType).forEach((contentType) => { Object.keys(oas3RequiredRequestBodyContentType.requestContentType).forEach(
(contentType) => {
if (contentType === oas3RequestContentType) { if (contentType === oas3RequestContentType) {
let contentTypeVal = oas3RequiredRequestBodyContentType.requestContentType[contentType] let contentTypeVal =
oas3RequiredRequestBodyContentType.requestContentType[contentType]
contentTypeVal.forEach((requiredKey) => { contentTypeVal.forEach((requiredKey) => {
if (requiredKeys.indexOf(requiredKey) < 0) { if (requiredKeys.indexOf(requiredKey) < 0) {
requiredKeys.push(requiredKey) requiredKeys.push(requiredKey)
} }
}) })
} }
}) }
)
requiredKeys.forEach((key) => { requiredKeys.forEach((key) => {
let requiredKeyValue = oas3RequestBodyValue.getIn([key, "value"]) let requiredKeyValue = oas3RequestBodyValue.getIn([key, "value"])
if (!requiredKeyValue) { if (!requiredKeyValue) {

View File

@@ -1,72 +1,39 @@
import { createSelector } from "reselect"
import { Map } from "immutable" import { Map } from "immutable"
import { isOAS3 as isOAS3Helper, isOpenAPI31 as isOpenAPI31Helper, isSwagger2 as isSwagger2Helper } from "../helpers" import { isSwagger2 as isSwagger2Helper, isOAS30 as isOAS30Helper } from "../helpers"
// Helpers /**
* Helpers
*/
// 1/2023: as of now, more accurately, isAnyOAS3 export const isSwagger2 = () => (system) => {
function onlyOAS3(selector) {
return () => (system, ...args) => {
const spec = system.getSystem().specSelectors.specJson()
if(isOAS3Helper(spec)) {
return selector(...args)
} else {
return null
}
}
}
function isOpenAPI31(selector) {
return () => (system, ...args) => {
const spec = system.getSystem().specSelectors.specJson()
if (isOpenAPI31Helper(spec)) {
return selector(...args)
} else {
return null
}
}
}
const state = state => {
return state || Map()
}
const specJson = createSelector(
state,
spec => spec.get("json", Map())
)
const specResolved = createSelector(
state,
spec => spec.get("resolved", Map())
)
const spec = state => {
let res = specResolved(state)
if(res.count() < 1)
res = specJson(state)
return res
}
// New selectors
export const servers = onlyOAS3(createSelector(
spec,
spec => spec.getIn(["servers"]) || Map()
))
export const isSwagger2 = (ori, system) => () => {
const spec = system.getSystem().specSelectors.specJson() const spec = system.getSystem().specSelectors.specJson()
return isSwagger2Helper(spec) return isSwagger2Helper(spec)
} }
export const selectIsOpenAPI31 = (ori, system) => () => { export const isOAS30 = () => (system) => {
const spec = system.getSystem().specSelectors.specJson() const spec = system.getSystem().specSelectors.specJson()
return isOpenAPI31Helper(spec) return isOAS30Helper(spec)
} }
export const selectWebhooks = isOpenAPI31(createSelector( export const isOAS3 = () => (system) => {
spec, return system.getSystem().specSelectors.isOAS30()
spec => spec.getIn(["webhooks"]) || Map() }
))
function onlyOAS3(selector) {
return () => (system, ...args) => {
const spec = system.getSystem().specSelectors.specJson()
if(system.specSelectors.isOAS3(spec)) {
const result = selector(...args)
return typeof result === "function" ? result(system, ...args) : result
} else {
return null
}
}
}
export const servers = onlyOAS3(() => (system) => {
const spec = system.specSelectors.specJson()
return spec.get("servers", servers.mapConst)
})
servers.mapConst = Map()

View File

@@ -1,107 +1,54 @@
/**
* @prettier
*/
import { createSelector } from "reselect" import { createSelector } from "reselect"
import { specJsonWithResolvedSubtrees } from "../../spec/selectors" import { specJsonWithResolvedSubtrees } from "../../spec/selectors"
import { Map } from "immutable" import { Map } from "immutable"
import { isOAS3 as isOAS3Helper, isOpenAPI31 as isOpenAPI31Helper, isSwagger2 as isSwagger2Helper } from "../helpers"
/**
// Helpers * Helpers
// 1/2023: as of now, more accurately, isAnyOAS3 */
function onlyOAS3(selector) { function onlyOAS3(selector) {
return (ori, system) => (...args) => { return (ori, system) =>
const spec = system.getSystem().specSelectors.specJson() (...args) => {
if(isOAS3Helper(spec)) { if (system.getSystem().specSelectors.isOAS3()) {
return selector(...args) const result = selector(...args)
return typeof result === "function" ? result(system, ...args) : result
} else { } else {
return ori(...args) return ori(...args)
} }
} }
} }
function isOpenAPI31(selector) {
return (ori, system) => (...args) => {
const spec = system.getSystem().specSelectors.specJson()
if (isOpenAPI31Helper(spec)) {
return selector(...args)
} else {
return null
}
}
}
const state = state => {
return state || Map()
}
const nullSelector = createSelector(() => null) const nullSelector = createSelector(() => null)
const OAS3NullSelector = onlyOAS3(nullSelector) const OAS3NullSelector = onlyOAS3(nullSelector)
const specJson = createSelector( /**
state, * Wrappers
spec => spec.get("json", Map()) */
)
const specResolved = createSelector( export const definitions = onlyOAS3(() => (system) => {
state, const spec = system.getSystem().specSelectors.specJson()
spec => spec.get("resolved", Map()) const schemas = spec.getIn(["components", "schemas"])
) return Map.isMap(schemas) ? schemas : definitions.mapConst
})
definitions.mapConst = Map()
const spec = state => { export const hasHost = onlyOAS3(() => (system) => {
let res = specResolved(state) const spec = system.getSystem().specSelectors.specJson()
if(res.count() < 1) return spec.hasIn(["servers", 0])
res = specJson(state)
return res
}
// Wrappers
export const definitions = onlyOAS3(createSelector(
spec,
spec => {
const res = spec.getIn(["components", "schemas"])
return Map.isMap(res) ? res : Map()
}
))
export const hasHost = onlyOAS3((state) => {
return spec(state).hasIn(["servers", 0])
}) })
export const securityDefinitions = onlyOAS3(createSelector( export const securityDefinitions = onlyOAS3(
createSelector(
specJsonWithResolvedSubtrees, specJsonWithResolvedSubtrees,
spec => spec.getIn(["components", "securitySchemes"]) || null (spec) => spec.getIn(["components", "securitySchemes"]) || null
)) )
)
export const host = OAS3NullSelector export const host = OAS3NullSelector
export const basePath = OAS3NullSelector export const basePath = OAS3NullSelector
export const consumes = OAS3NullSelector export const consumes = OAS3NullSelector
export const produces = OAS3NullSelector export const produces = OAS3NullSelector
export const schemes = OAS3NullSelector export const schemes = OAS3NullSelector
// New selectors
export const servers = onlyOAS3(createSelector(
spec,
spec => spec.getIn(["servers"]) || Map()
))
export const isOAS3 = (ori, system) => () => {
const spec = system.getSystem().specSelectors.specJson()
return isOAS3Helper(Map.isMap(spec) ? spec : Map())
}
export const isSwagger2 = (ori, system) => () => {
const spec = system.getSystem().specSelectors.specJson()
return isSwagger2Helper(Map.isMap(spec) ? spec : Map())
}
export const selectIsOpenAPI31 = (ori, system) => () => {
const spec = system.getSystem().specSelectors.specJson()
return isOpenAPI31Helper(Map.isMap(spec) ? spec : Map())
}
export const selectWebhooks = isOpenAPI31(createSelector(
spec,
spec => spec.getIn(["webhooks"]) || Map()
))

View File

@@ -4,8 +4,6 @@ import VersionStamp from "./version-stamp"
import OnlineValidatorBadge from "./online-validator-badge" import OnlineValidatorBadge from "./online-validator-badge"
import Model from "./model" import Model from "./model"
import JsonSchema_string from "./json-schema-string" import JsonSchema_string from "./json-schema-string"
import License from "./license"
import info from "./info"
export default { export default {
Markdown, Markdown,
@@ -14,6 +12,4 @@ export default {
VersionStamp, VersionStamp,
model: Model, model: Model,
onlineValidatorBadge: OnlineValidatorBadge, onlineValidatorBadge: OnlineValidatorBadge,
License,
info,
} }

View File

@@ -1,78 +0,0 @@
import React from "react"
import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes"
import { sanitizeUrl } from "core/utils"
import { safeBuildUrl } from "core/utils/url"
import { OAS3ComponentWrapFactory } from "../helpers"
const Info = (props) => {
const { info, url, host, basePath, getComponent, specSelectors, externalDocs, selectedServer, url: specUrl } = props
const isOpenAPI31 = specSelectors.selectIsOpenAPI31()
const version = info.get("version")
const description = info.get("description")
const title = info.get("title")
const termsOfServiceUrl = safeBuildUrl(info.get("termsOfService"), specUrl, { selectedServer })
const contact = info.get("contact")
const license = info.get("license")
// note that ux may want to move summary to a sub-heading, as summary is a string that does not need to be Markdown
const summary = info.get("summary") // OAS3.1 field
const rawExternalDocsUrl = externalDocs && externalDocs.get("url")
const externalDocsUrl = safeBuildUrl(rawExternalDocsUrl, specUrl, { selectedServer })
const externalDocsDescription = externalDocs && externalDocs.get("description")
const Markdown = getComponent("Markdown", true)
const Link = getComponent("Link")
const VersionStamp = getComponent("VersionStamp")
const InfoUrl = getComponent("InfoUrl")
const InfoBasePath = getComponent("InfoBasePath")
const License = getComponent("License")
const Contact = getComponent("Contact")
return (
<div className="info">
<hgroup className="main">
<h2 className="title" >{title}
{version && <VersionStamp version={version}></VersionStamp>}
</h2>
{host || basePath ? <InfoBasePath host={host} basePath={basePath} /> : null}
{url && <InfoUrl getComponent={getComponent} url={url} />}
</hgroup>
{
isOpenAPI31 && summary && <div className="info__summary">
<Markdown source={summary} />
</div>
}
<div className="description">
<Markdown source={description} />
</div>
{
termsOfServiceUrl && <div className="info__tos">
<Link target="_blank" href={sanitizeUrl(termsOfServiceUrl)}>Terms of service</Link>
</div>
}
{contact && contact.size ? <Contact getComponent={getComponent} data={contact} selectedServer={selectedServer} url={url} /> : null}
{license && license.size ? <License getComponent={getComponent} license={license} selectedServer={selectedServer} url={url} /> : null}
{externalDocsUrl ?
<Link className="info__extdocs" target="_blank" href={sanitizeUrl(externalDocsUrl)}>{externalDocsDescription || externalDocsUrl}</Link>
: null}
</div>
)
}
Info.propTypes = {
info: PropTypes.object,
url: PropTypes.string,
host: PropTypes.string,
basePath: PropTypes.string,
externalDocs: ImPropTypes.map,
getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
oas3selectors: PropTypes.func,
selectedServer: PropTypes.string,
}
export default OAS3ComponentWrapFactory(Info)

View File

@@ -1,49 +0,0 @@
import React from "react"
import PropTypes from "prop-types"
import { sanitizeUrl } from "core/utils"
import { safeBuildUrl } from "core/utils/url"
import { OAS3ComponentWrapFactory } from "../helpers"
const baseSPDXurl = "https://spdx.org/licenses"
const createSPDXurl = (identifier) => {
return `${baseSPDXurl}/${identifier}.html`
}
const License = (props) => {
const { license, getComponent, selectedServer, url: specUrl, specSelectors } = props
const Link = getComponent("Link")
const name = license.get("name") || "License"
const url = safeBuildUrl(license.get("url"), specUrl, { selectedServer })
const identifier = license.get("identifier") || "" // OAS3.1 field
const identifierUrl = createSPDXurl(identifier)
const isOpenAPI31 = specSelectors.selectIsOpenAPI31()
return (
<div className="info__license">
{
!isOpenAPI31 && url && <div className="info__license__url"><Link target="_blank" href={sanitizeUrl(url)}>{name}</Link></div>
}
{
isOpenAPI31 && url && !identifier && <div className="info__license__url"><Link target="_blank" href={sanitizeUrl(url)}>{name}</Link></div>
}
{
isOpenAPI31 && identifier && !url && <div className="info__license__identifier"><Link target="_blank" href={sanitizeUrl(baseSPDXurl)}>SPDX License</Link>: <Link target="_blank" href={sanitizeUrl(identifierUrl)}>{identifier}</Link></div>
}
{/* {
isOpenAPI31 && identifier && url && <div className="info__license_error">Render Error: License.url and License.identifier are mutually exclusive fields</div>
} */}
</div>
)
}
License.propTypes = {
license: PropTypes.shape({
get: PropTypes.func,
}),
getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
selectedServer: PropTypes.string,
url: PropTypes.string.isRequired,
}
export default OAS3ComponentWrapFactory(License)

View File

@@ -0,0 +1,155 @@
/**
* @prettier
*/
import React from "react"
import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes"
import { sanitizeUrl } from "core/utils"
import { safeBuildUrl } from "core/utils/url"
export class InfoBasePath extends React.Component {
static propTypes = {
host: PropTypes.string,
basePath: PropTypes.string,
}
render() {
const { host, basePath } = this.props
return (
<pre className="base-url">
[ Base URL: {host}
{basePath} ]
</pre>
)
}
}
export class InfoUrl extends React.PureComponent {
static propTypes = {
url: PropTypes.string.isRequired,
getComponent: PropTypes.func.isRequired,
}
render() {
const { url, getComponent } = this.props
const Link = getComponent("Link")
return (
<Link target="_blank" href={sanitizeUrl(url)}>
<span className="url"> {url}</span>
</Link>
)
}
}
class Info extends React.Component {
static propTypes = {
title: PropTypes.any,
description: PropTypes.any,
version: PropTypes.any,
info: PropTypes.object,
url: PropTypes.string,
host: PropTypes.string,
basePath: PropTypes.string,
externalDocs: ImPropTypes.map,
getComponent: PropTypes.func.isRequired,
oas3selectors: PropTypes.func,
selectedServer: PropTypes.string,
}
render() {
const {
info,
url,
host,
basePath,
getComponent,
externalDocs,
selectedServer,
url: specUrl,
} = this.props
const version = info.get("version")
const summary = info.get("summary")
const description = info.get("description")
const title = info.get("title")
const termsOfServiceUrl = safeBuildUrl(
info.get("termsOfService"),
specUrl,
{ selectedServer }
)
const contactData = info.get("contact")
const licenseData = info.get("license")
const rawExternalDocsUrl = externalDocs && externalDocs.get("url")
const externalDocsUrl = safeBuildUrl(rawExternalDocsUrl, specUrl, {
selectedServer,
})
const externalDocsDescription =
externalDocs && externalDocs.get("description")
const Markdown = getComponent("Markdown", true)
const Link = getComponent("Link")
const VersionStamp = getComponent("VersionStamp")
const InfoUrl = getComponent("InfoUrl")
const InfoBasePath = getComponent("InfoBasePath")
const License = getComponent("License")
const Contact = getComponent("Contact")
return (
<div className="info">
<hgroup className="main">
<h2 className="title">
{title}
{version && <VersionStamp version={version}></VersionStamp>}
</h2>
{host || basePath ? (
<InfoBasePath host={host} basePath={basePath} />
) : null}
{url && <InfoUrl getComponent={getComponent} url={url} />}
</hgroup>
{summary && (
<div className="info__summary">
<Markdown source={summary} />
</div>
)}
<div className="description">
<Markdown source={description} />
</div>
{termsOfServiceUrl && (
<div className="info__tos">
<Link target="_blank" href={sanitizeUrl(termsOfServiceUrl)}>
Terms of service
</Link>
</div>
)}
{contactData?.size > 0 && (
<Contact
getComponent={getComponent}
data={contactData}
selectedServer={selectedServer}
url={url}
/>
)}
{licenseData?.size > 0 && (
<License
getComponent={getComponent}
license={licenseData}
selectedServer={selectedServer}
url={url}
/>
)}
{externalDocsUrl ? (
<Link
className="info__extdocs"
target="_blank"
href={sanitizeUrl(externalDocsUrl)}
>
{externalDocsDescription || externalDocsUrl}
</Link>
) : null}
</div>
)
}
}
export default Info

View File

@@ -0,0 +1,52 @@
/**
* @prettier
*/
import React from "react"
import PropTypes from "prop-types"
import { safeBuildUrl } from "core/utils/url"
import { sanitizeUrl } from "core/utils"
class License extends React.Component {
static propTypes = {
license: PropTypes.object,
getComponent: PropTypes.func.isRequired,
selectedServer: PropTypes.string,
url: PropTypes.string.isRequired,
}
render() {
const { license, getComponent, selectedServer, url: specUrl } = this.props
const name = license.get("name", "License")
const url = sanitizeUrl(
safeBuildUrl(license.get("url"), specUrl, { selectedServer })
)
const identifier = license.get("identifier", "")
const spdxURL = sanitizeUrl(`https://spdx.org/licenses/${identifier}.html`)
const Link = getComponent("Link")
return (
<div className="info__license">
{identifier && (
<div className="info__license__url">
<Link target="_blank" href={spdxURL}>
{name}
</Link>
</div>
)}
{url && !identifier && (
<div className="info__license__url">
<Link target="_blank" href={url}>
{name}
</Link>
</div>
)}
{!url && !identifier && <span>{name}</span>}
</div>
)
}
}
export default License

View File

@@ -1,4 +1,3 @@
// OpenAPI 3.1 feature
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import { fromJS } from "immutable" import { fromJS } from "immutable"
@@ -9,7 +8,7 @@ import ImPropTypes from "react-immutable-proptypes"
const Webhooks = (props) => { const Webhooks = (props) => {
const { specSelectors, getComponent, specPath } = props const { specSelectors, getComponent, specPath } = props
const webhooksPathItems = specSelectors.selectWebhooks() // OrderedMap const webhooksPathItems = specSelectors.webhooks()
if (!webhooksPathItems || webhooksPathItems?.size < 1) { if (!webhooksPathItems || webhooksPathItems?.size < 1) {
return null return null
} }

View File

@@ -0,0 +1,10 @@
/**
* @prettier
*/
export const isOAS31 = (jsSpec) => {
const oasVersion = jsSpec.get("openapi")
return (
typeof oasVersion === "string" && /^3\.1\.(?:[1-9]\d*|0)$/.test(oasVersion)
)
}

View File

@@ -0,0 +1,37 @@
/**
* @prettier
*/
import Webhooks from "./components/webhooks"
import License from "./components/license"
import Info from "./components/info"
import LicenseWrapper from "./wrap-components/license"
import InfoWrapper from "./wrap-components/info"
import { isOAS31, webhooks } from "./spec-extensions/selectors"
import { isOAS3 } from "./spec-extensions/wrap-selectors"
const OAS31Plugin = () => {
return {
components: {
Webhooks,
OAS31Info: Info,
OAS31License: License,
},
wrapComponents: {
License: LicenseWrapper,
info: InfoWrapper,
},
statePlugins: {
spec: {
selectors: {
isOAS31,
webhooks,
},
wrapSelectors: {
isOAS3,
},
},
},
}
}
export default OAS31Plugin

View File

@@ -0,0 +1,29 @@
/**
* @prettier
*/
import { Map } from "immutable"
import { isOAS31 as isOAS31Helper } from "../helpers"
export const isOAS31 = () => (system) => {
const spec = system.specSelectors.specJson()
return isOAS31Helper(spec)
}
const onlyOAS31 =
(selector) =>
() =>
(system, ...args) => {
if (system.getSystem().specSelectors.isOAS31()) {
const result = selector(...args)
return typeof result === "function" ? result(system, ...args) : result
} else {
return null
}
}
export const webhooks = onlyOAS31(() => (system) => {
const spec = system.specSelectors.specJson()
return spec.get("webhooks", webhooks.mapConst)
})
webhooks.mapConst = Map()

View File

@@ -0,0 +1,4 @@
export const isOAS3 = (oriSelector, system) => (state, ...args) => {
const isOAS31 = system.specSelectors.isOAS31()
return isOAS31 || oriSelector(...args)
}

View File

@@ -0,0 +1,16 @@
/**
* @prettier
*/
import React from "react"
const InfoWrapper = (Original, system) => (props) => {
if (system.specSelectors.isOAS31()) {
const OAS31Info = system.getComponent("OAS31Info")
return <OAS31Info {...props} />
}
return <Original {...props} />
}
export default InfoWrapper

View File

@@ -0,0 +1,16 @@
/**
* @prettier
*/
import React from "react"
const LicenseWrapper = (Original, system) => (props) => {
if (system.specSelectors.isOAS31()) {
const OAS31License = system.getComponent("OAS31License")
return <OAS31License {...props} />
}
return <Original {...props} />
}
export default LicenseWrapper

View File

@@ -1,12 +1,10 @@
/**
* @prettier
*/
import BasePreset from "./base" import BasePreset from "./base"
import OAS3Plugin from "../plugins/oas3" import OAS3Plugin from "../plugins/oas3"
import OAS31Plugin from "../plugins/oas31"
// Just the base, for now.
export default function PresetApis() { export default function PresetApis() {
return [BasePreset, OAS3Plugin, OAS31Plugin]
return [
BasePreset,
OAS3Plugin
]
} }

View File

@@ -1,3 +1,6 @@
/**
* @prettier
*/
import err from "core/plugins/err" import err from "core/plugins/err"
import layout from "core/plugins/layout" import layout from "core/plugins/layout"
import spec from "core/plugins/spec" import spec from "core/plugins/spec"
@@ -57,13 +60,10 @@ import Errors from "core/components/errors"
import ContentType from "core/components/content-type" import ContentType from "core/components/content-type"
import Overview from "core/components/overview" import Overview from "core/components/overview"
import InitializedInput from "core/components/initialized-input" import InitializedInput from "core/components/initialized-input"
import Info, { import Info, { InfoUrl, InfoBasePath } from "core/components/info"
InfoUrl,
InfoBasePath,
License,
Contact,
} from "core/components/info"
import InfoContainer from "core/containers/info" import InfoContainer from "core/containers/info"
import Contact from "core/components/contact"
import License from "core/components/license"
import JumpToPath from "core/components/jump-to-path" import JumpToPath from "core/components/jump-to-path"
import CopyToClipboardBtn from "core/components/copy-to-clipboard-btn" import CopyToClipboardBtn from "core/components/copy-to-clipboard-btn"
import Footer from "core/components/footer" import Footer from "core/components/footer"
@@ -87,16 +87,12 @@ import VersionPragmaFilter from "core/components/version-pragma-filter"
import VersionStamp from "core/components/version-stamp" import VersionStamp from "core/components/version-stamp"
import DeepLink from "core/components/deep-link" import DeepLink from "core/components/deep-link"
import SvgAssets from "core/components/svg-assets" import SvgAssets from "core/components/svg-assets"
import Markdown from "core/components/providers/markdown" import Markdown from "core/components/providers/markdown"
import BaseLayout from "core/components/layouts/base" import BaseLayout from "core/components/layouts/base"
import * as LayoutUtils from "core/components/layout-utils" import * as LayoutUtils from "core/components/layout-utils"
import * as JsonSchemaComponents from "core/json-schema-components" import * as JsonSchemaComponents from "core/json-schema-components"
export default function () { export default function () {
let coreComponents = { let coreComponents = {
components: { components: {
App, App,
@@ -115,6 +111,10 @@ export default function() {
InitializedInput, InitializedInput,
info: Info, info: Info,
InfoContainer, InfoContainer,
InfoUrl,
InfoBasePath,
Contact,
License,
JumpToPath, JumpToPath,
CopyToClipboardBtn, CopyToClipboardBtn,
onlineValidatorBadge: OnlineValidatorBadge, onlineValidatorBadge: OnlineValidatorBadge,
@@ -163,23 +163,19 @@ export default function() {
OperationTag, OperationTag,
OperationContainer, OperationContainer,
DeepLink, DeepLink,
InfoUrl,
InfoBasePath,
License,
Contact,
SvgAssets, SvgAssets,
Example, Example,
ExamplesSelect, ExamplesSelect,
ExamplesSelectValueRetainer, ExamplesSelectValueRetainer,
} },
} }
let formComponents = { let formComponents = {
components: LayoutUtils components: LayoutUtils,
} }
let jsonSchemaComponents = { let jsonSchemaComponents = {
components: JsonSchemaComponents components: JsonSchemaComponents,
} }
return [ return [

View File

@@ -1,4 +1,5 @@
swagger: 2.0.0 swagger: "2.0"
info: info:
title: OpenAPI 2.0 Info Object title: OpenAPI 2.0 Info Object
version: 1.0.0 version: 1.0.0

View File

@@ -1,4 +1,4 @@
swagger: 2.0.0 swagger: "2.0"
info: info:
title: OpenAPI 2.0 License with only url present title: OpenAPI 2.0 License with only url present
version: 1.0.0 version: 1.0.0

View File

@@ -5,5 +5,4 @@ info:
description: This is a sample server for a pet store. description: This is a sample server for a pet store.
license: license:
name: Apache 2.0 name: Apache 2.0
# url: https://www.apache.org/licenses/LICENSE-2.0.html identifier: Apache-2.0
identifier: Apache-2.0 # mutually exclusive of url; separately, for json_schema, consider const, prefix, array items can be of a different type (current assumption is all array items are the same)

View File

@@ -1,11 +1,13 @@
describe("Render Info Component", () => { describe("Render Info Component", () => {
describe("OpenAPI 2.x", () => { describe("OpenAPI 2.0", () => {
const baseUrl = "/?url=/documents/features/info-openAPI2.yaml" const baseUrl = "/?url=/documents/features/info-openAPI2.yaml"
it("should render Info Description", () => { it("should render Info Description", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info .description") .get(".info .description")
.should("contains.text", "This is a sample") .should("contains.text", "This is a sample")
}) })
it("should render Info Main anchor target xss link with safe `rel` attributes", () => { it("should render Info Main anchor target xss link with safe `rel` attributes", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info .main > a") .get(".info .main > a")
@@ -16,19 +18,23 @@ describe("Render Info Component", () => {
.should("have.attr", "target") .should("have.attr", "target")
.and("equal", "_blank") .and("equal", "_blank")
}) })
it("should not render Info Summary (an OpenAPI 3.1 field)", () => { it("should not render Info Summary (an OpenAPI 3.1 field)", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info__summary") .get(".info__summary")
.should("not.exist") .should("not.exist")
}) })
}) })
describe("OpenAPI 3.0.x", () => { describe("OpenAPI 3.0.x", () => {
const baseUrl = "/?url=/documents/features/info-openAPI30.yaml" const baseUrl = "/?url=/documents/features/info-openAPI30.yaml"
it("should render Info Description", () => { it("should render Info Description", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info .description") .get(".info .description")
.should("contains.text", "This is a sample") .should("contains.text", "This is a sample")
}) })
it("should render Info Main anchor target xss link with safe `rel` attributes", () => { it("should render Info Main anchor target xss link with safe `rel` attributes", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info .main > a") .get(".info .main > a")
@@ -39,19 +45,23 @@ describe("Render Info Component", () => {
.should("have.attr", "target") .should("have.attr", "target")
.and("equal", "_blank") .and("equal", "_blank")
}) })
it("should not render Info Summary (an OpenAPI 3.1 field)", () => { it("should not render Info Summary (an OpenAPI 3.1 field)", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info__summary") .get(".info__summary")
.should("not.exist") .should("not.exist")
}) })
}) })
describe("OpenAPI 3.1.x", () => { describe("OpenAPI 3.1.x", () => {
const baseUrl = "/?url=/documents/features/info-openAPI31.yaml" const baseUrl = "/?url=/documents/features/info-openAPI31.yaml"
it("should render Info Description", () => { it("should render Info Description", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info .description") .get(".info .description")
.should("contains.text", "This is a sample") .should("contains.text", "This is a sample")
}) })
it("should render Info Main anchor target xss link with safe `rel` attributes", () => { it("should render Info Main anchor target xss link with safe `rel` attributes", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info .main > a") .get(".info .main > a")

View File

@@ -1,18 +1,20 @@
/**
* @prettier
*/
describe("Render License Component", () => { describe("Render License Component", () => {
describe("OpenAPI 2", () =>{ describe("OpenAPI 2.0", () => {
const baseUrl = "/?url=/documents/features/license-openAPI2.yaml" const baseUrl = "/?url=/documents/features/license-openAPI2.yaml"
it("should render License URL", () => { it("should render License URL", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info__license") .get(".info__license")
.should("exist") .should("exist")
.should("contains.text", "Apache 2.0") .should("contains.text", "Apache 2.0")
.should("not.contains.text", "SPDX License")
.get(".info__license__identifier")
.should("not.exist")
}) })
it("should render License URL anchor target xss link with safe `rel` attributes", () => { it("should render License URL anchor target xss link with safe `rel` attributes", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info__license > .link") .get(".info__license__url > .link")
.should("have.attr", "rel") .should("have.attr", "rel")
.and("include", "noopener") .and("include", "noopener")
.and("include", "noreferrer") .and("include", "noreferrer")
@@ -21,17 +23,17 @@ describe("Render License Component", () => {
.and("equal", "_blank") .and("equal", "_blank")
}) })
}) })
describe("OpenAPI 3.0.x", () => { describe("OpenAPI 3.0.x", () => {
const baseUrl = "/?url=/documents/features/license-openAPI30.yaml" const baseUrl = "/?url=/documents/features/license-openAPI30.yaml"
it("should render License URL", () => { it("should render License URL", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info__license__url") .get(".info__license__url")
.should("exist") .should("exist")
.should("contains.text", "Apache 2.0") .should("contains.text", "Apache 2.0")
.should("not.contains.text", "SPDX License")
.get(".info__license__identifier")
.should("not.exist")
}) })
it("should render URL anchor target xss link with safe `rel` attributes", () => { it("should render URL anchor target xss link with safe `rel` attributes", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info__license__url > a") .get(".info__license__url > a")
@@ -43,18 +45,21 @@ describe("Render License Component", () => {
.and("equal", "_blank") .and("equal", "_blank")
}) })
}) })
describe("OpenAPI 3.1.x", () => { describe("OpenAPI 3.1.x", () => {
describe("only URL", () => { describe("given URL field", () => {
const baseUrl = "/?url=/documents/features/license-openAPI31-url.yaml" const baseUrl = "/?url=/documents/features/license-openAPI31-url.yaml"
it("should render URL", () => { it("should render URL", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info__license__url") .get(".info__license__url")
.should("exist") .should("exist")
.should("contains.text", "Apache 2.0") .should("contains.text", "Apache 2.0")
.should("not.contains.text", "SPDX License") .get(".info__license__url > a")
.get(".info__license__identifier") .should("have.attr", "href")
.should("not.exist") .and("equal", "https://www.apache.org/licenses/LICENSE-2.0.html")
}) })
it("should render URL anchor target xss link with safe `rel` attributes", () => { it("should render URL anchor target xss link with safe `rel` attributes", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info__license__url > a") .get(".info__license__url > a")
@@ -66,34 +71,37 @@ describe("Render License Component", () => {
.and("equal", "_blank") .and("equal", "_blank")
}) })
}) })
describe("only SPDX Identifier", () => {
const baseUrl = "/?url=/documents/features/license-openAPI31-identifier.yaml" describe("given identifier field", () => {
it("should render SPDX Identifier", () => { const baseUrl =
"/?url=/documents/features/license-openAPI31-identifier.yaml"
it("should render URL using identifier", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info__license__identifier")
.should("exist")
.should("contains.text", "Apache-2.0")
.should("contains.text", "SPDX License")
.get(".info__license__url") .get(".info__license__url")
.should("not.exist") .should("exist")
.should("contains.text", "Apache 2.0")
.get(".info__license__url > a")
.should("have.attr", "href")
.and("equal", "https://spdx.org/licenses/Apache-2.0.html")
}) })
it("should render SPDX and Identifier anchor target xss links with safe `rel` attributes ", () => {
it("should render URL anchor target xss links with safe `rel` attributes", () => {
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info__license__identifier > a") .get(".info__license__url > a")
.each(($el) => {
cy.get($el)
.should("have.attr", "rel") .should("have.attr", "rel")
.and("include", "noopener") .and("include", "noopener")
.and("include", "noreferrer") .and("include", "noreferrer")
cy.get($el) .get(".info .main > a")
.should("have.attr", "target") .should("have.attr", "target")
.and("equal", "_blank") .and("equal", "_blank")
}) })
}) })
})
describe("URL and SPX are mutually exclusive", () => { describe("URL and SPX are mutually exclusive", () => {
it("should render nothing if both URL & SPDX exists", () => { it("should render nothing if both URL & SPDX exists", () => {
const baseUrl = "/?url=/documents/features/license-openAPI31-error-both-identifier-and-url.yaml" const baseUrl =
"/?url=/documents/features/license-openAPI31-error-both-identifier-and-url.yaml"
cy.visit(baseUrl) cy.visit(baseUrl)
.get(".info__license__identifier") .get(".info__license__identifier")
.should("not.exist") .should("not.exist")

View File

@@ -1,7 +1,7 @@
import { fromJS } from "immutable" import { fromJS } from "immutable"
import { isOAS3, isSwagger2 } from "corePlugins/oas3/helpers" import { isOAS30, isSwagger2 } from "corePlugins/oas3/helpers"
const isOAS3Shorthand = (version) => isOAS3(fromJS({ const isOAS3Shorthand = (version) => isOAS30(fromJS({
openapi: version openapi: version
})) }))
@@ -13,7 +13,7 @@ describe("isOAS3", function () {
it("should recognize valid OAS3 version values", function () { it("should recognize valid OAS3 version values", function () {
expect(isOAS3Shorthand("3.0.0")).toEqual(true) expect(isOAS3Shorthand("3.0.0")).toEqual(true)
expect(isOAS3Shorthand("3.0.1")).toEqual(true) expect(isOAS3Shorthand("3.0.1")).toEqual(true)
expect(isOAS3Shorthand("3.0.11111")).toEqual(true) expect(isOAS3Shorthand("3.0.11111")).toEqual(false)
expect(isOAS3Shorthand("3.0.0-rc0")).toEqual(true) expect(isOAS3Shorthand("3.0.0-rc0")).toEqual(true)
}) })
@@ -31,7 +31,7 @@ describe("isOAS3", function () {
}) })
it("should gracefully fail when `openapi` field is missing", function () { it("should gracefully fail when `openapi` field is missing", function () {
expect(isOAS3(fromJS({ expect(isOAS30(fromJS({
openApi: "3.0.0" openApi: "3.0.0"
}))).toEqual(false) }))).toEqual(false)
expect(isOAS3Shorthand(null)).toEqual(false) expect(isOAS3Shorthand(null)).toEqual(false)
@@ -41,7 +41,7 @@ describe("isOAS3", function () {
describe("isSwagger2", function () { describe("isSwagger2", function () {
it("should recognize valid Swagger 2.0 version values", function () { it("should recognize valid Swagger 2.0 version values", function () {
expect(isSwagger2Shorthand("2.0")).toEqual(true) expect(isSwagger2Shorthand("2.0")).toEqual(true)
expect(isSwagger2Shorthand("2.0-rc0")).toEqual(true) expect(isSwagger2Shorthand("2.0-rc0")).toEqual(false)
}) })
it("should fail for invalid Swagger 2.0 version values", function () { it("should fail for invalid Swagger 2.0 version values", function () {

View File

@@ -26,7 +26,8 @@ describe("OAS3 plugin - state", function() {
specSelectors: { specSelectors: {
specJson: () => { specJson: () => {
return fromJS({ openapi: "3.0.0" }) return fromJS({ openapi: "3.0.0" })
} },
isOAS3: () => true,
} }
} }
} }
@@ -55,7 +56,8 @@ describe("OAS3 plugin - state", function() {
specSelectors: { specSelectors: {
specJson: () => { specJson: () => {
return fromJS({ openapi: "3.0.0" }) return fromJS({ openapi: "3.0.0" })
} },
isOAS3: () => true,
} }
} }
} }
@@ -88,7 +90,8 @@ describe("OAS3 plugin - state", function() {
specSelectors: { specSelectors: {
specJson: () => { specJson: () => {
return fromJS({ openapi: "3.0.0" }) return fromJS({ openapi: "3.0.0" })
} },
isOAS3: () => true,
} }
} }
} }
@@ -124,7 +127,8 @@ describe("OAS3 plugin - state", function() {
specSelectors: { specSelectors: {
specJson: () => { specJson: () => {
return fromJS({ openapi: "3.0.0" }) return fromJS({ openapi: "3.0.0" })
} },
isOAS3: () => true,
} }
} }
} }
@@ -167,7 +171,8 @@ describe("OAS3 plugin - state", function() {
specSelectors: { specSelectors: {
specJson: () => { specJson: () => {
return fromJS({ openapi: "3.0.0" }) return fromJS({ openapi: "3.0.0" })
} },
isOAS3: () => true,
} }
} }
} }
@@ -207,7 +212,8 @@ describe("OAS3 plugin - state", function() {
specSelectors: { specSelectors: {
specJson: () => { specJson: () => {
return fromJS({ openapi: "3.0.0" }) return fromJS({ openapi: "3.0.0" })
} },
isOAS3: () => true,
} }
} }
} }
@@ -261,7 +267,8 @@ describe("OAS3 plugin - state", function() {
specSelectors: { specSelectors: {
specJson: () => { specJson: () => {
return fromJS({ openapi: "3.0.0" }) return fromJS({ openapi: "3.0.0" })
} },
isOAS3: () => true,
} }
} }
} }
@@ -314,7 +321,8 @@ describe("OAS3 plugin - state", function() {
specSelectors: { specSelectors: {
specJson: () => { specJson: () => {
return fromJS({ openapi: "3.0.0" }) return fromJS({ openapi: "3.0.0" })
} },
isOAS3: () => true,
} }
} }
} }
@@ -342,7 +350,8 @@ describe("OAS3 plugin - state", function() {
specSelectors: { specSelectors: {
specJson: () => { specJson: () => {
return fromJS({ openapi: "3.0.0" }) return fromJS({ openapi: "3.0.0" })
} },
isOAS3: () => true,
} }
} }
} }

View File

@@ -17,6 +17,7 @@ describe("oas3 plugin - auth extensions - wrapSelectors", function(){
specJson: () => fromJS({ specJson: () => fromJS({
openapi: "3.0.0" openapi: "3.0.0"
}), }),
isOAS3: () => true,
securityDefinitions: () => { securityDefinitions: () => {
return fromJS({ return fromJS({
"oauth2AuthorizationCode": { "oauth2AuthorizationCode": {

View File

@@ -1,8 +1,5 @@
import { fromJS } from "immutable" import { fromJS } from "immutable"
import { import { definitions } from "corePlugins/oas3/spec-extensions/wrap-selectors"
definitions
} from "corePlugins/oas3/spec-extensions/wrap-selectors"
describe("oas3 plugin - spec extensions - wrapSelectors", function(){ describe("oas3 plugin - spec extensions - wrapSelectors", function(){
@@ -28,6 +25,7 @@ describe("oas3 plugin - spec extensions - wrapSelectors", function(){
getSystem: () => system, getSystem: () => system,
specSelectors: { specSelectors: {
specJson: () => spec, specJson: () => spec,
isOAS3: () => true,
} }
} }
@@ -57,6 +55,7 @@ describe("oas3 plugin - spec extensions - wrapSelectors", function(){
getSystem: () => system, getSystem: () => system,
specSelectors: { specSelectors: {
specJson: () => spec, specJson: () => spec,
isOAS3: () => true,
} }
} }
@@ -82,7 +81,8 @@ describe("oas3 plugin - spec extensions - wrapSelectors", function(){
getSystem: () => system, getSystem: () => system,
specSelectors: { specSelectors: {
specJson: () => spec, specJson: () => spec,
} isOAS3: () => true,
},
} }
// When // When

View File

@@ -1,7 +1,9 @@
import React from "react" import React from "react"
import { render } from "enzyme" import { render } from "enzyme"
import { fromJS } from "immutable" import { fromJS } from "immutable"
import Info, { InfoUrl, License } from "components/info" import Info, { InfoUrl } from "components/info"
import Contact from "components/contact"
import License from "components/license"
import { Link } from "components/layout-utils" import { Link } from "components/layout-utils"
import Markdown from "components/providers/markdown" import Markdown from "components/providers/markdown"
@@ -11,7 +13,8 @@ describe("<Info/> Anchor Target Safety", function(){
Markdown, Markdown,
InfoUrl, InfoUrl,
License, License,
Link Contact,
Link,
} }
const baseProps = { const baseProps = {
getComponent: c => components[c] || dummyComponent, getComponent: c => components[c] || dummyComponent,