From 8b274414ab224f7a48ff37fe19f66d367d53d5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Thu, 16 Mar 2023 12:05:19 +0100 Subject: [PATCH] refactor(oas31): concentrate OpenAPI 3.1.0 code to separate plugin (#8475) Refs #8474 --- src/core/components/contact.jsx | 45 +++ src/core/components/info.jsx | 182 +++++------ src/core/components/layouts/base.jsx | 116 ++++--- src/core/components/license.jsx | 41 +++ .../oas3/auth-extensions/wrap-selectors.js | 4 +- src/core/plugins/oas3/components/index.js | 2 - src/core/plugins/oas3/helpers.jsx | 39 +-- src/core/plugins/oas3/index.js | 27 +- src/core/plugins/oas3/selectors.js | 302 ++++++++++-------- .../plugins/oas3/spec-extensions/selectors.js | 89 ++---- .../oas3/spec-extensions/wrap-selectors.js | 117 ++----- .../plugins/oas3/wrap-components/index.js | 4 - .../plugins/oas3/wrap-components/info.jsx | 78 ----- .../plugins/oas3/wrap-components/license.jsx | 49 --- src/core/plugins/oas31/components/info.jsx | 155 +++++++++ src/core/plugins/oas31/components/license.jsx | 52 +++ .../{oas3 => oas31}/components/webhooks.jsx | 7 +- src/core/plugins/oas31/helpers.js | 10 + src/core/plugins/oas31/index.js | 37 +++ .../oas31/spec-extensions/selectors.js | 29 ++ .../oas31/spec-extensions/wrap-selectors.js | 4 + .../plugins/oas31/wrap-components/info.jsx | 16 + .../plugins/oas31/wrap-components/license.jsx | 16 + src/core/presets/apis.js | 12 +- src/core/presets/base.js | 32 +- .../documents/features/info-openAPI2.yaml | 3 +- .../documents/features/license-openAPI2.yaml | 2 +- .../license-openAPI31-identifier.yaml | 3 +- test/e2e-cypress/tests/features/info.js | 12 +- test/e2e-cypress/tests/features/license.js | 88 ++--- test/unit/core/plugins/oas3/helpers.js | 10 +- .../core/plugins/oas3/state-integration.js | 27 +- .../core/plugins/oas3/wrap-auth-selectors.js | 1 + .../core/plugins/oas3/wrap-spec-selectors.js | 10 +- test/unit/xss/anchor-target-rel/info.jsx | 7 +- 35 files changed, 929 insertions(+), 699 deletions(-) create mode 100644 src/core/components/contact.jsx create mode 100644 src/core/components/license.jsx delete mode 100644 src/core/plugins/oas3/wrap-components/info.jsx delete mode 100644 src/core/plugins/oas3/wrap-components/license.jsx create mode 100644 src/core/plugins/oas31/components/info.jsx create mode 100644 src/core/plugins/oas31/components/license.jsx rename src/core/plugins/{oas3 => oas31}/components/webhooks.jsx (94%) create mode 100644 src/core/plugins/oas31/helpers.js create mode 100644 src/core/plugins/oas31/index.js create mode 100644 src/core/plugins/oas31/spec-extensions/selectors.js create mode 100644 src/core/plugins/oas31/spec-extensions/wrap-selectors.js create mode 100644 src/core/plugins/oas31/wrap-components/info.jsx create mode 100644 src/core/plugins/oas31/wrap-components/license.jsx diff --git a/src/core/components/contact.jsx b/src/core/components/contact.jsx new file mode 100644 index 00000000..e7ce6c68 --- /dev/null +++ b/src/core/components/contact.jsx @@ -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 ( +
+ {url && ( +
+ + {name} - Website + +
+ )} + {email && ( + + {url ? `Send email to ${name}` : `Contact ${name}`} + + )} +
+ ) + } +} + +export default Contact diff --git a/src/core/components/info.jsx b/src/core/components/info.jsx index 1922239a..b2327ed5 100644 --- a/src/core/components/info.jsx +++ b/src/core/components/info.jsx @@ -1,102 +1,53 @@ +/** + * @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 + basePath: PropTypes.string, } render() { - let { host, basePath } = this.props + const { host, basePath } = this.props return (
-        [ Base URL: {host}{basePath} ]
+        [ Base URL: {host}
+        {basePath} ]
       
) } } - -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 ( -
- { url &&
{ name } - Website
} - { email && - - { url ? `Send email to ${name}` : `Contact ${name}`} - - } -
- ) - } -} - -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 ( -
- { - url ? { name } - : { name } - } -
- ) - } -} - export class InfoUrl extends React.PureComponent { static propTypes = { url: PropTypes.string.isRequired, - getComponent: PropTypes.func.isRequired + getComponent: PropTypes.func.isRequired, } - render() { const { url, getComponent } = this.props - const Link = getComponent("Link") - return { url } + return ( + + {url} + + ) } } -export default class Info extends React.Component { +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, @@ -108,16 +59,32 @@ export default class Info extends React.Component { } render() { - let { info, url, host, basePath, getComponent, externalDocs, selectedServer, url: specUrl } = this.props - let version = info.get("version") - let description = info.get("description") - let title = info.get("title") - let termsOfServiceUrl = safeBuildUrl(info.get("termsOfService"), specUrl, {selectedServer}) - let contact = info.get("contact") - let license = info.get("license") - let rawExternalDocsUrl = externalDocs && externalDocs.get("url") - let externalDocsUrl = safeBuildUrl(rawExternalDocsUrl, specUrl, {selectedServer}) - let externalDocsDescription = externalDocs && externalDocs.get("description") + const { + info, + url, + host, + basePath, + getComponent, + externalDocs, + selectedServer, + url: specUrl, + } = 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 Link = getComponent("Link") @@ -125,42 +92,61 @@ export default class Info extends React.Component { const InfoUrl = getComponent("InfoUrl") const InfoBasePath = getComponent("InfoBasePath") const License = getComponent("License") + const Contact = getComponent("Contact") return (
-

{ title } - { version && } +

+ {title} + {version && }

- { host || basePath ? : null } - { url && } + {host || basePath ? ( + + ) : null} + {url && }
- +
- { - termsOfServiceUrl &&
- Terms of service + {termsOfServiceUrl && ( +
+ + Terms of service +
- } - - {contact && contact.size ? : null } - {license && license.size ? : null } - { externalDocsUrl ? - {externalDocsDescription || externalDocsUrl} - : null } + )} + {contactData?.size > 0 && ( + + )} + {licenseData?.size > 0 && ( + + )} + {externalDocsUrl ? ( + + {externalDocsDescription || externalDocsUrl} + + ) : null}
) } - } -Info.propTypes = { - title: PropTypes.any, - description: PropTypes.any, - version: PropTypes.any, - url: PropTypes.string -} +export default Info diff --git a/src/core/components/layouts/base.jsx b/src/core/components/layouts/base.jsx index c6f0a7bf..2df9edbe 100644 --- a/src/core/components/layouts/base.jsx +++ b/src/core/components/layouts/base.jsx @@ -1,37 +1,39 @@ +/** + * @prettier + */ import React from "react" import PropTypes from "prop-types" export default class BaseLayout extends React.Component { - static propTypes = { errSelectors: PropTypes.object.isRequired, errActions: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired, oas3Selectors: PropTypes.object.isRequired, oas3Actions: PropTypes.object.isRequired, - getComponent: PropTypes.func.isRequired + getComponent: PropTypes.func.isRequired, } render() { - let {errSelectors, specSelectors, getComponent} = this.props + const { errSelectors, specSelectors, getComponent } = this.props - let SvgAssets = getComponent("SvgAssets") - let InfoContainer = getComponent("InfoContainer", true) - let VersionPragmaFilter = getComponent("VersionPragmaFilter") - let Operations = getComponent("operations", true) - let Models = getComponent("Models", true) - let Webhooks = getComponent("Webhooks", true) - let Row = getComponent("Row") - let Col = getComponent("Col") - let Errors = getComponent("errors", true) + const SvgAssets = getComponent("SvgAssets") + const InfoContainer = getComponent("InfoContainer", true) + const VersionPragmaFilter = getComponent("VersionPragmaFilter") + const Operations = getComponent("operations", true) + const Models = getComponent("Models", true) + const Webhooks = getComponent("Webhooks", true) + const Row = getComponent("Row") + const Col = getComponent("Col") + const Errors = getComponent("errors", true) const ServersContainer = getComponent("ServersContainer", true) const SchemesContainer = getComponent("SchemesContainer", true) const AuthorizeBtnContainer = getComponent("AuthorizeBtnContainer", true) const FilterContainer = getComponent("FilterContainer", true) - let isSwagger2 = specSelectors.isSwagger2() - let isOAS3 = specSelectors.isOAS3() - const isOpenAPI31 = specSelectors.selectIsOpenAPI31() + const isSwagger2 = specSelectors.isSwagger2() + const isOAS3 = specSelectors.isOAS3() + const isOAS31 = specSelectors.isOAS31() const isSpecEmpty = !specSelectors.specStr() @@ -39,44 +41,50 @@ export default class BaseLayout extends React.Component { let loadingMessage = null - if(loadingStatus === "loading") { - loadingMessage =
-
-
+ if (loadingStatus === "loading") { + loadingMessage = ( +
+
+
+
-
+ ) } - if(loadingStatus === "failed") { - loadingMessage =
-
-

Failed to load API definition.

- + if (loadingStatus === "failed") { + loadingMessage = ( +
+
+

Failed to load API definition.

+ +
-
+ ) } if (loadingStatus === "failedConfig") { const lastErr = errSelectors.lastError() const lastErrMsg = lastErr ? lastErr.get("message") : "" - loadingMessage =
-
-

Failed to load remote configuration.

-

{lastErrMsg}

+ loadingMessage = ( +
+
+

Failed to load remote configuration.

+

{lastErrMsg}

+
-
+ ) } - if(!loadingMessage && isSpecEmpty) { + if (!loadingMessage && isSpecEmpty) { loadingMessage =

No API definition provided.

} - if(loadingMessage) { - return
-
- {loadingMessage} + if (loadingMessage) { + return ( +
+
{loadingMessage}
-
+ ) } const servers = specSelectors.servers() @@ -87,43 +95,47 @@ export default class BaseLayout extends React.Component { const hasSecurityDefinitions = !!specSelectors.securityDefinitions() return ( -
+
- }> - + } + > + - + {hasServers || hasSchemes || hasSecurityDefinitions ? (
- {hasServers ? () : null} - {hasSchemes ? () : null} - {hasSecurityDefinitions ? () : null} + {hasServers ? : null} + {hasSchemes ? : null} + {hasSecurityDefinitions ? : null}
) : null} - + - - + + - { isOpenAPI31 && + {isOAS31 && ( - + - } + )} - - + +
diff --git a/src/core/components/license.jsx b/src/core/components/license.jsx new file mode 100644 index 00000000..d5b3a015 --- /dev/null +++ b/src/core/components/license.jsx @@ -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 ( +
+ {url ? ( +
+ + {name} + +
+ ) : ( + {name} + )} +
+ ) + } +} + +export default License diff --git a/src/core/plugins/oas3/auth-extensions/wrap-selectors.js b/src/core/plugins/oas3/auth-extensions/wrap-selectors.js index f7b2ea75..e09b4fdf 100644 --- a/src/core/plugins/oas3/auth-extensions/wrap-selectors.js +++ b/src/core/plugins/oas3/auth-extensions/wrap-selectors.js @@ -1,6 +1,5 @@ import { createSelector } from "reselect" import { List, Map, fromJS } from "immutable" -import { isOAS3 as isOAS3Helper } from "../helpers" // Helpers @@ -9,8 +8,7 @@ const state = state => state function onlyOAS3(selector) { return (ori, system) => (...args) => { - const spec = system.getSystem().specSelectors.specJson() - if(isOAS3Helper(spec)) { + if(system.getSystem().specSelectors.isOAS3()) { // Pass the spec plugin state to Reselect to trigger on securityDefinitions update let resolvedSchemes = system.getState().getIn(["spec", "resolvedSubtrees", "components", "securitySchemes"]) diff --git a/src/core/plugins/oas3/components/index.js b/src/core/plugins/oas3/components/index.js index 49cf8460..ff180b7e 100644 --- a/src/core/plugins/oas3/components/index.js +++ b/src/core/plugins/oas3/components/index.js @@ -6,7 +6,6 @@ import ServersContainer from "./servers-container" import RequestBodyEditor from "./request-body-editor" import HttpAuth from "./http-auth" import OperationServers from "./operation-servers" -import Webhooks from "./webhooks" export default { Callbacks, @@ -17,5 +16,4 @@ export default { RequestBodyEditor, OperationServers, operationLink: OperationLink, - Webhooks } diff --git a/src/core/plugins/oas3/helpers.jsx b/src/core/plugins/oas3/helpers.jsx index 253973a5..79b95722 100644 --- a/src/core/plugins/oas3/helpers.jsx +++ b/src/core/plugins/oas3/helpers.jsx @@ -1,44 +1,27 @@ +/** + * @prettier + */ import React from "react" -export function isOpenAPI30(jsSpec) { +export function isOAS30(jsSpec) { const oasVersion = jsSpec.get("openapi") - if (typeof oasVersion !== "string") { - return false - } - return oasVersion.startsWith("3.0.") && oasVersion.length > 4 -} -export function isOpenAPI31(jsSpec) { - const oasVersion = jsSpec.get("openapi") - if (typeof oasVersion !== "string") { - 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) + return ( + typeof oasVersion === "string" && + /^3\.0\.([0123])(?:-rc[012])?$/.test(oasVersion) + ) } export function isSwagger2(jsSpec) { 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) { return (Ori, system) => (props) => { - if(system && system.specSelectors && system.specSelectors.specJson) { - const spec = system.specSelectors.specJson() - - if(isOAS3(spec)) { + if (typeof system.specSelectors?.isOAS3 === "function") { + if (system.specSelectors.isOAS3()) { return } else { return diff --git a/src/core/plugins/oas3/index.js b/src/core/plugins/oas3/index.js index 29170de4..c96fac6f 100644 --- a/src/core/plugins/oas3/index.js +++ b/src/core/plugins/oas3/index.js @@ -1,31 +1,32 @@ -// import reducers from "./reducers" -// import * as actions from "./actions" +/** + * @prettier + */ import * as specWrapSelectors from "./spec-extensions/wrap-selectors" import * as authWrapSelectors from "./auth-extensions/wrap-selectors" import * as specSelectors from "./spec-extensions/selectors" import components from "./components" import wrapComponents from "./wrap-components" -import * as oas3Actions from "./actions" -import * as oas3Selectors from "./selectors" -import oas3Reducers from "./reducers" +import * as actions from "./actions" +import * as selectors from "./selectors" +import reducers from "./reducers" -export default function() { +export default function () { return { components, wrapComponents, statePlugins: { spec: { wrapSelectors: specWrapSelectors, - selectors: specSelectors + selectors: specSelectors, }, auth: { - wrapSelectors: authWrapSelectors + wrapSelectors: authWrapSelectors, }, oas3: { - actions: oas3Actions, - reducers: oas3Reducers, - selectors: oas3Selectors, - } - } + actions, + reducers, + selectors, + }, + }, } } diff --git a/src/core/plugins/oas3/selectors.js b/src/core/plugins/oas3/selectors.js index 792b844e..ba2e7d2e 100644 --- a/src/core/plugins/oas3/selectors.js +++ b/src/core/plugins/oas3/selectors.js @@ -1,41 +1,53 @@ +/** + * @prettier + */ import { OrderedMap, Map, List } from "immutable" -import { isOAS3 as isOAS3Helper } from "./helpers" import { getDefaultRequestBodyValue } from "./components/request-body" import { stringify } from "../../utils" // Helpers function onlyOAS3(selector) { - return (...args) => (system) => { - const spec = system.getSystem().specSelectors.specJson() - if(isOAS3Helper(spec)) { - return selector(...args) - } else { - return null + return (...args) => + (system) => { + if (system.getSystem().specSelectors.isOAS3()) { + return selector(...args) + } else { + return null + } } - } } function validateRequestBodyIsRequired(selector) { - return (...args) => (system) => { - const specJson = system.getSystem().specSelectors.specJson() - const argsList = [...args] - // expect argsList[0] = state - let pathMethod = argsList[1] || [] - let isOas3RequestBodyRequired = specJson.getIn(["paths", ...pathMethod, "requestBody", "required"]) + return (...args) => + (system) => { + const specJson = system.getSystem().specSelectors.specJson() + const argsList = [...args] + // expect argsList[0] = state + let pathMethod = argsList[1] || [] + let isOas3RequestBodyRequired = specJson.getIn([ + "paths", + ...pathMethod, + "requestBody", + "required", + ]) - if (isOas3RequestBodyRequired) { - return selector(...args) - } else { - // validation pass b/c not required - return true + if (isOas3RequestBodyRequired) { + return selector(...args) + } else { + // validation pass b/c not required + return true + } } - } } const validateRequestBodyValueExists = (state, 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 if (!oas3RequestBodyValue) { return false @@ -44,68 +56,85 @@ const validateRequestBodyValueExists = (state, pathMethod) => { return true } - export const selectedServer = onlyOAS3((state, namespace) => { - const path = namespace ? [namespace, "selectedServer"] : ["selectedServer"] - return state.getIn(path) || "" - } -) + const path = namespace ? [namespace, "selectedServer"] : ["selectedServer"] + return state.getIn(path) || "" +}) 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) => { - return state.getIn(["requestData", path, method, "retainBodyValue"]) || false - } -) + return state.getIn(["requestData", path, method, "retainBodyValue"]) || false +}) -export const selectDefaultRequestBodyValue = (state, path, method) => (system) => { - const {oas3Selectors, specSelectors} = system.getSystem() - const spec = specSelectors.specJson() - if(isOAS3Helper(spec)) { - const currentMediaType = oas3Selectors.requestContentType(path, method) - if (currentMediaType) { - return getDefaultRequestBodyValue( - specSelectors.specResolvedSubtree(["paths", path, method, "requestBody"]), - currentMediaType, - oas3Selectors.activeExamplesMember( - path, method, - "requestBody", - "requestBody", +export const selectDefaultRequestBodyValue = + (state, path, method) => (system) => { + const { oas3Selectors, specSelectors } = system.getSystem() + + if (specSelectors.isOAS3()) { + const currentMediaType = oas3Selectors.requestContentType(path, method) + if (currentMediaType) { + return getDefaultRequestBodyValue( + specSelectors.specResolvedSubtree([ + "paths", + path, + method, + "requestBody", + ]), + currentMediaType, + oas3Selectors.activeExamplesMember( + path, + method, + "requestBody", + "requestBody" + ) ) - ) + } } + return null } - return null -} export const hasUserEditedBody = (state, path, method) => (system) => { - const {oas3Selectors, specSelectors} = system.getSystem() - const spec = specSelectors.specJson() - if(isOAS3Helper(spec)) { + const { oas3Selectors, specSelectors } = system.getSystem() + + if (specSelectors.isOAS3()) { let userHasEditedBody = false const currentMediaType = oas3Selectors.requestContentType(path, method) let userEditedRequestBody = oas3Selectors.requestBodyValue(path, method) if (Map.isMap(userEditedRequestBody)) { // 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) } if (currentMediaType) { const currentMediaTypeDefaultBodyValue = getDefaultRequestBodyValue( - specSelectors.specResolvedSubtree(["paths", path, method, "requestBody"]), + specSelectors.specResolvedSubtree([ + "paths", + path, + method, + "requestBody", + ]), currentMediaType, oas3Selectors.activeExamplesMember( - path, method, - "requestBody", + path, + method, "requestBody", + "requestBody" ) ) - userHasEditedBody = !!userEditedRequestBody && userEditedRequestBody !== currentMediaTypeDefaultBodyValue + userHasEditedBody = + !!userEditedRequestBody && + userEditedRequestBody !== currentMediaTypeDefaultBodyValue } return userHasEditedBody } else { @@ -114,106 +143,114 @@ export const hasUserEditedBody = (state, path, method) => (system) => { } 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) => { - 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) => { - return state.getIn(["examples", path, method, type, name, "activeExample"]) || null +export const activeExamplesMember = onlyOAS3( + (state, path, method, type, name) => { + return ( + state.getIn(["examples", path, method, type, name, "activeExample"]) || + null + ) } ) 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) => { - return state.getIn(["requestData", path, method, "responseContentType"]) || null - } -) + return ( + state.getIn(["requestData", path, method, "responseContentType"]) || null + ) +}) export const serverVariableValue = onlyOAS3((state, locationData, key) => { - let path + let path - // locationData may take one of two forms, for backwards compatibility - // Object: ({server, namespace?}) or String:(server) - if(typeof locationData !== "string") { - const { server, namespace } = locationData - if(namespace) { - path = [namespace, "serverVariableValues", server, key] - } else { - path = ["serverVariableValues", server, key] - } + // locationData may take one of two forms, for backwards compatibility + // Object: ({server, namespace?}) or String:(server) + if (typeof locationData !== "string") { + const { server, namespace } = locationData + if (namespace) { + path = [namespace, "serverVariableValues", server, key] } else { - const server = locationData path = ["serverVariableValues", server, key] } - - return state.getIn(path) || null + } else { + const server = locationData + path = ["serverVariableValues", server, key] } -) + + return state.getIn(path) || null +}) export const serverVariables = onlyOAS3((state, locationData) => { - let path + let path - // locationData may take one of two forms, for backwards compatibility - // Object: ({server, namespace?}) or String:(server) - if(typeof locationData !== "string") { - const { server, namespace } = locationData - if(namespace) { - path = [namespace, "serverVariableValues", server] - } else { - path = ["serverVariableValues", server] - } + // locationData may take one of two forms, for backwards compatibility + // Object: ({server, namespace?}) or String:(server) + if (typeof locationData !== "string") { + const { server, namespace } = locationData + if (namespace) { + path = [namespace, "serverVariableValues", server] } else { - const server = locationData path = ["serverVariableValues", server] } - - return state.getIn(path) || OrderedMap() + } else { + const server = locationData + path = ["serverVariableValues", server] } -) + + return state.getIn(path) || OrderedMap() +}) export const serverEffectiveValue = onlyOAS3((state, locationData) => { - var varValues, serverValue + var varValues, serverValue - // locationData may take one of two forms, for backwards compatibility - // Object: ({server, namespace?}) or String:(server) - if(typeof locationData !== "string") { - const { server, namespace } = locationData - serverValue = server - if(namespace) { - varValues = state.getIn([namespace, "serverVariableValues", serverValue]) - } else { - varValues = state.getIn(["serverVariableValues", serverValue]) - } + // locationData may take one of two forms, for backwards compatibility + // Object: ({server, namespace?}) or String:(server) + if (typeof locationData !== "string") { + const { server, namespace } = locationData + serverValue = server + if (namespace) { + varValues = state.getIn([namespace, "serverVariableValues", serverValue]) } else { - serverValue = locationData varValues = state.getIn(["serverVariableValues", serverValue]) } - - varValues = varValues || OrderedMap() - let str = serverValue - - varValues.map((val, key) => { - str = str.replace(new RegExp(`{${key}}`, "g"), val) - }) - - return str + } else { + serverValue = locationData + varValues = state.getIn(["serverVariableValues", serverValue]) } -) + + varValues = varValues || OrderedMap() + let str = serverValue + + varValues.map((val, key) => { + str = str.replace(new RegExp(`{${key}}`, "g"), val) + }) + + return str +}) export const validateBeforeExecute = validateRequestBodyIsRequired( (state, pathMethod) => validateRequestBodyValueExists(state, pathMethod) ) -export const validateShallowRequired = (state, { oas3RequiredRequestBodyContentType, oas3RequestContentType, oas3RequestBodyValue} ) => { +export const validateShallowRequired = ( + state, + { + oas3RequiredRequestBodyContentType, + oas3RequestContentType, + oas3RequestBodyValue, + } +) => { let missingRequiredKeys = [] // context: json => String; urlencoded, form-data => Map if (!Map.isMap(oas3RequestBodyValue)) { @@ -221,16 +258,19 @@ export const validateShallowRequired = (state, { oas3RequiredRequestBodyContentT } let requiredKeys = [] // Cycle through list of possible contentTypes for matching contentType and defined requiredKeys - Object.keys(oas3RequiredRequestBodyContentType.requestContentType).forEach((contentType) => { - if (contentType === oas3RequestContentType) { - let contentTypeVal = oas3RequiredRequestBodyContentType.requestContentType[contentType] - contentTypeVal.forEach((requiredKey) => { - if (requiredKeys.indexOf(requiredKey) < 0 ) { - requiredKeys.push(requiredKey) - } - }) + Object.keys(oas3RequiredRequestBodyContentType.requestContentType).forEach( + (contentType) => { + if (contentType === oas3RequestContentType) { + let contentTypeVal = + oas3RequiredRequestBodyContentType.requestContentType[contentType] + contentTypeVal.forEach((requiredKey) => { + if (requiredKeys.indexOf(requiredKey) < 0) { + requiredKeys.push(requiredKey) + } + }) + } } - }) + ) requiredKeys.forEach((key) => { let requiredKeyValue = oas3RequestBodyValue.getIn([key, "value"]) if (!requiredKeyValue) { diff --git a/src/core/plugins/oas3/spec-extensions/selectors.js b/src/core/plugins/oas3/spec-extensions/selectors.js index b7c85ee0..587a11a9 100644 --- a/src/core/plugins/oas3/spec-extensions/selectors.js +++ b/src/core/plugins/oas3/spec-extensions/selectors.js @@ -1,72 +1,39 @@ -import { createSelector } from "reselect" 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 -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) => () => { +export const isSwagger2 = () => (system) => { const spec = system.getSystem().specSelectors.specJson() return isSwagger2Helper(spec) } -export const selectIsOpenAPI31 = (ori, system) => () => { +export const isOAS30 = () => (system) => { const spec = system.getSystem().specSelectors.specJson() - return isOpenAPI31Helper(spec) + return isOAS30Helper(spec) } -export const selectWebhooks = isOpenAPI31(createSelector( - spec, - spec => spec.getIn(["webhooks"]) || Map() -)) +export const isOAS3 = () => (system) => { + return system.getSystem().specSelectors.isOAS30() +} + +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() diff --git a/src/core/plugins/oas3/spec-extensions/wrap-selectors.js b/src/core/plugins/oas3/spec-extensions/wrap-selectors.js index 8e82572d..8572fc1e 100644 --- a/src/core/plugins/oas3/spec-extensions/wrap-selectors.js +++ b/src/core/plugins/oas3/spec-extensions/wrap-selectors.js @@ -1,107 +1,54 @@ +/** + * @prettier + */ import { createSelector } from "reselect" import { specJsonWithResolvedSubtrees } from "../../spec/selectors" import { Map } from "immutable" -import { isOAS3 as isOAS3Helper, isOpenAPI31 as isOpenAPI31Helper, isSwagger2 as isSwagger2Helper } from "../helpers" - -// Helpers -// 1/2023: as of now, more accurately, isAnyOAS3 +/** + * Helpers + */ function onlyOAS3(selector) { - return (ori, system) => (...args) => { - const spec = system.getSystem().specSelectors.specJson() - if(isOAS3Helper(spec)) { - return selector(...args) - } else { - return ori(...args) + return (ori, system) => + (...args) => { + if (system.getSystem().specSelectors.isOAS3()) { + const result = selector(...args) + return typeof result === "function" ? result(system, ...args) : result + } else { + 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 OAS3NullSelector = onlyOAS3(nullSelector) -const specJson = createSelector( - state, - spec => spec.get("json", Map()) -) +/** + * Wrappers + */ -const specResolved = createSelector( - state, - spec => spec.get("resolved", Map()) -) +export const definitions = onlyOAS3(() => (system) => { + const spec = system.getSystem().specSelectors.specJson() + const schemas = spec.getIn(["components", "schemas"]) + return Map.isMap(schemas) ? schemas : definitions.mapConst +}) +definitions.mapConst = Map() -const spec = state => { - let res = specResolved(state) - if(res.count() < 1) - 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 hasHost = onlyOAS3(() => (system) => { + const spec = system.getSystem().specSelectors.specJson() + return spec.hasIn(["servers", 0]) }) -export const securityDefinitions = onlyOAS3(createSelector( - specJsonWithResolvedSubtrees, - spec => spec.getIn(["components", "securitySchemes"]) || null -)) +export const securityDefinitions = onlyOAS3( + createSelector( + specJsonWithResolvedSubtrees, + (spec) => spec.getIn(["components", "securitySchemes"]) || null + ) +) export const host = OAS3NullSelector export const basePath = OAS3NullSelector export const consumes = OAS3NullSelector export const produces = 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() -)) - diff --git a/src/core/plugins/oas3/wrap-components/index.js b/src/core/plugins/oas3/wrap-components/index.js index cd0c627a..2c421add 100644 --- a/src/core/plugins/oas3/wrap-components/index.js +++ b/src/core/plugins/oas3/wrap-components/index.js @@ -4,8 +4,6 @@ import VersionStamp from "./version-stamp" import OnlineValidatorBadge from "./online-validator-badge" import Model from "./model" import JsonSchema_string from "./json-schema-string" -import License from "./license" -import info from "./info" export default { Markdown, @@ -14,6 +12,4 @@ export default { VersionStamp, model: Model, onlineValidatorBadge: OnlineValidatorBadge, - License, - info, } diff --git a/src/core/plugins/oas3/wrap-components/info.jsx b/src/core/plugins/oas3/wrap-components/info.jsx deleted file mode 100644 index 42112b97..00000000 --- a/src/core/plugins/oas3/wrap-components/info.jsx +++ /dev/null @@ -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 ( -
-
-

{title} - {version && } -

- {host || basePath ? : null} - {url && } -
- - { - isOpenAPI31 && summary &&
- -
- } -
- -
- - { - termsOfServiceUrl &&
- Terms of service -
- } - - {contact && contact.size ? : null} - {license && license.size ? : null} - {externalDocsUrl ? - {externalDocsDescription || externalDocsUrl} - : null} - -
- ) -} - -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) \ No newline at end of file diff --git a/src/core/plugins/oas3/wrap-components/license.jsx b/src/core/plugins/oas3/wrap-components/license.jsx deleted file mode 100644 index 7560a6b8..00000000 --- a/src/core/plugins/oas3/wrap-components/license.jsx +++ /dev/null @@ -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 ( -
- { - !isOpenAPI31 && url &&
{name}
- } - { - isOpenAPI31 && url && !identifier &&
{name}
- } - { - isOpenAPI31 && identifier && !url &&
SPDX License: {identifier}
- } - {/* { - isOpenAPI31 && identifier && url &&
Render Error: License.url and License.identifier are mutually exclusive fields
- } */} -
- ) -} - -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) \ No newline at end of file diff --git a/src/core/plugins/oas31/components/info.jsx b/src/core/plugins/oas31/components/info.jsx new file mode 100644 index 00000000..048be5dd --- /dev/null +++ b/src/core/plugins/oas31/components/info.jsx @@ -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 ( +
+        [ Base URL: {host}
+        {basePath} ]
+      
+ ) + } +} + +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 ( + + {url} + + ) + } +} + +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 ( +
+
+

+ {title} + {version && } +

+ {host || basePath ? ( + + ) : null} + {url && } +
+ {summary && ( +
+ +
+ )} +
+ +
+ {termsOfServiceUrl && ( +
+ + Terms of service + +
+ )} + {contactData?.size > 0 && ( + + )} + {licenseData?.size > 0 && ( + + )} + {externalDocsUrl ? ( + + {externalDocsDescription || externalDocsUrl} + + ) : null} +
+ ) + } +} + +export default Info diff --git a/src/core/plugins/oas31/components/license.jsx b/src/core/plugins/oas31/components/license.jsx new file mode 100644 index 00000000..ae620e79 --- /dev/null +++ b/src/core/plugins/oas31/components/license.jsx @@ -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 ( +
+ {identifier && ( +
+ + {name} + +
+ )} + + {url && !identifier && ( +
+ + {name} + +
+ )} + + {!url && !identifier && {name}} +
+ ) + } +} + +export default License diff --git a/src/core/plugins/oas3/components/webhooks.jsx b/src/core/plugins/oas31/components/webhooks.jsx similarity index 94% rename from src/core/plugins/oas3/components/webhooks.jsx rename to src/core/plugins/oas31/components/webhooks.jsx index 9bd3e8e6..b4e7f7cc 100644 --- a/src/core/plugins/oas3/components/webhooks.jsx +++ b/src/core/plugins/oas31/components/webhooks.jsx @@ -1,4 +1,3 @@ -// OpenAPI 3.1 feature import React from "react" import PropTypes from "prop-types" import { fromJS } from "immutable" @@ -8,8 +7,8 @@ import ImPropTypes from "react-immutable-proptypes" // to show/hide all webhook items const Webhooks = (props) => { const { specSelectors, getComponent, specPath } = props - - const webhooksPathItems = specSelectors.selectWebhooks() // OrderedMap + + const webhooksPathItems = specSelectors.webhooks() if (!webhooksPathItems || webhooksPathItems?.size < 1) { return null } @@ -21,7 +20,7 @@ const Webhooks = (props) => { operation }) // using defaultProps for `specPath`; may want to remove from props - // and/or if extract to separate PathItem component, allow for use + // and/or if extract to separate PathItem component, allow for use // with both OAS3.1 "webhooks" and "components.pathItems" features return { + const oasVersion = jsSpec.get("openapi") + + return ( + typeof oasVersion === "string" && /^3\.1\.(?:[1-9]\d*|0)$/.test(oasVersion) + ) +} diff --git a/src/core/plugins/oas31/index.js b/src/core/plugins/oas31/index.js new file mode 100644 index 00000000..11cc4d1b --- /dev/null +++ b/src/core/plugins/oas31/index.js @@ -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 diff --git a/src/core/plugins/oas31/spec-extensions/selectors.js b/src/core/plugins/oas31/spec-extensions/selectors.js new file mode 100644 index 00000000..dadd946f --- /dev/null +++ b/src/core/plugins/oas31/spec-extensions/selectors.js @@ -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() diff --git a/src/core/plugins/oas31/spec-extensions/wrap-selectors.js b/src/core/plugins/oas31/spec-extensions/wrap-selectors.js new file mode 100644 index 00000000..482bdcb0 --- /dev/null +++ b/src/core/plugins/oas31/spec-extensions/wrap-selectors.js @@ -0,0 +1,4 @@ +export const isOAS3 = (oriSelector, system) => (state, ...args) => { + const isOAS31 = system.specSelectors.isOAS31() + return isOAS31 || oriSelector(...args) +} diff --git a/src/core/plugins/oas31/wrap-components/info.jsx b/src/core/plugins/oas31/wrap-components/info.jsx new file mode 100644 index 00000000..3530044c --- /dev/null +++ b/src/core/plugins/oas31/wrap-components/info.jsx @@ -0,0 +1,16 @@ +/** + * @prettier + */ +import React from "react" + +const InfoWrapper = (Original, system) => (props) => { + if (system.specSelectors.isOAS31()) { + const OAS31Info = system.getComponent("OAS31Info") + + return + } + + return +} + +export default InfoWrapper diff --git a/src/core/plugins/oas31/wrap-components/license.jsx b/src/core/plugins/oas31/wrap-components/license.jsx new file mode 100644 index 00000000..200da560 --- /dev/null +++ b/src/core/plugins/oas31/wrap-components/license.jsx @@ -0,0 +1,16 @@ +/** + * @prettier + */ +import React from "react" + +const LicenseWrapper = (Original, system) => (props) => { + if (system.specSelectors.isOAS31()) { + const OAS31License = system.getComponent("OAS31License") + + return + } + + return +} + +export default LicenseWrapper diff --git a/src/core/presets/apis.js b/src/core/presets/apis.js index 14afc627..bc2bdf44 100644 --- a/src/core/presets/apis.js +++ b/src/core/presets/apis.js @@ -1,12 +1,10 @@ +/** + * @prettier + */ import BasePreset from "./base" import OAS3Plugin from "../plugins/oas3" - -// Just the base, for now. +import OAS31Plugin from "../plugins/oas31" export default function PresetApis() { - - return [ - BasePreset, - OAS3Plugin - ] + return [BasePreset, OAS3Plugin, OAS31Plugin] } diff --git a/src/core/presets/base.js b/src/core/presets/base.js index 43662c49..ca33bb51 100644 --- a/src/core/presets/base.js +++ b/src/core/presets/base.js @@ -1,3 +1,6 @@ +/** + * @prettier + */ import err from "core/plugins/err" import layout from "core/plugins/layout" import spec from "core/plugins/spec" @@ -57,13 +60,10 @@ import Errors from "core/components/errors" import ContentType from "core/components/content-type" import Overview from "core/components/overview" import InitializedInput from "core/components/initialized-input" -import Info, { - InfoUrl, - InfoBasePath, - License, - Contact, -} from "core/components/info" +import Info, { InfoUrl, InfoBasePath } from "core/components/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 CopyToClipboardBtn from "core/components/copy-to-clipboard-btn" 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 DeepLink from "core/components/deep-link" import SvgAssets from "core/components/svg-assets" - import Markdown from "core/components/providers/markdown" - import BaseLayout from "core/components/layouts/base" - import * as LayoutUtils from "core/components/layout-utils" import * as JsonSchemaComponents from "core/json-schema-components" -export default function() { - +export default function () { let coreComponents = { components: { App, @@ -115,6 +111,10 @@ export default function() { InitializedInput, info: Info, InfoContainer, + InfoUrl, + InfoBasePath, + Contact, + License, JumpToPath, CopyToClipboardBtn, onlineValidatorBadge: OnlineValidatorBadge, @@ -163,23 +163,19 @@ export default function() { OperationTag, OperationContainer, DeepLink, - InfoUrl, - InfoBasePath, - License, - Contact, SvgAssets, Example, ExamplesSelect, ExamplesSelectValueRetainer, - } + }, } let formComponents = { - components: LayoutUtils + components: LayoutUtils, } let jsonSchemaComponents = { - components: JsonSchemaComponents + components: JsonSchemaComponents, } return [ diff --git a/test/e2e-cypress/static/documents/features/info-openAPI2.yaml b/test/e2e-cypress/static/documents/features/info-openAPI2.yaml index 05d62de0..6363c983 100644 --- a/test/e2e-cypress/static/documents/features/info-openAPI2.yaml +++ b/test/e2e-cypress/static/documents/features/info-openAPI2.yaml @@ -1,4 +1,5 @@ -swagger: 2.0.0 +swagger: "2.0" + info: title: OpenAPI 2.0 Info Object version: 1.0.0 diff --git a/test/e2e-cypress/static/documents/features/license-openAPI2.yaml b/test/e2e-cypress/static/documents/features/license-openAPI2.yaml index 4ef77050..be0f7b0c 100644 --- a/test/e2e-cypress/static/documents/features/license-openAPI2.yaml +++ b/test/e2e-cypress/static/documents/features/license-openAPI2.yaml @@ -1,4 +1,4 @@ -swagger: 2.0.0 +swagger: "2.0" info: title: OpenAPI 2.0 License with only url present version: 1.0.0 diff --git a/test/e2e-cypress/static/documents/features/license-openAPI31-identifier.yaml b/test/e2e-cypress/static/documents/features/license-openAPI31-identifier.yaml index 9a777a5a..b88bc161 100644 --- a/test/e2e-cypress/static/documents/features/license-openAPI31-identifier.yaml +++ b/test/e2e-cypress/static/documents/features/license-openAPI31-identifier.yaml @@ -5,5 +5,4 @@ info: description: This is a sample server for a pet store. license: name: Apache 2.0 - # url: https://www.apache.org/licenses/LICENSE-2.0.html - 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) + identifier: Apache-2.0 diff --git a/test/e2e-cypress/tests/features/info.js b/test/e2e-cypress/tests/features/info.js index 499d4c28..9884f197 100644 --- a/test/e2e-cypress/tests/features/info.js +++ b/test/e2e-cypress/tests/features/info.js @@ -1,11 +1,13 @@ describe("Render Info Component", () => { - describe("OpenAPI 2.x", () => { + describe("OpenAPI 2.0", () => { const baseUrl = "/?url=/documents/features/info-openAPI2.yaml" + it("should render Info Description", () => { cy.visit(baseUrl) .get(".info .description") .should("contains.text", "This is a sample") }) + it("should render Info Main anchor target xss link with safe `rel` attributes", () => { cy.visit(baseUrl) .get(".info .main > a") @@ -16,19 +18,23 @@ describe("Render Info Component", () => { .should("have.attr", "target") .and("equal", "_blank") }) + it("should not render Info Summary (an OpenAPI 3.1 field)", () => { cy.visit(baseUrl) .get(".info__summary") .should("not.exist") }) }) + describe("OpenAPI 3.0.x", () => { const baseUrl = "/?url=/documents/features/info-openAPI30.yaml" + it("should render Info Description", () => { cy.visit(baseUrl) .get(".info .description") .should("contains.text", "This is a sample") }) + it("should render Info Main anchor target xss link with safe `rel` attributes", () => { cy.visit(baseUrl) .get(".info .main > a") @@ -39,19 +45,23 @@ describe("Render Info Component", () => { .should("have.attr", "target") .and("equal", "_blank") }) + it("should not render Info Summary (an OpenAPI 3.1 field)", () => { cy.visit(baseUrl) .get(".info__summary") .should("not.exist") }) }) + describe("OpenAPI 3.1.x", () => { const baseUrl = "/?url=/documents/features/info-openAPI31.yaml" + it("should render Info Description", () => { cy.visit(baseUrl) .get(".info .description") .should("contains.text", "This is a sample") }) + it("should render Info Main anchor target xss link with safe `rel` attributes", () => { cy.visit(baseUrl) .get(".info .main > a") diff --git a/test/e2e-cypress/tests/features/license.js b/test/e2e-cypress/tests/features/license.js index 009cb101..8d48f95f 100644 --- a/test/e2e-cypress/tests/features/license.js +++ b/test/e2e-cypress/tests/features/license.js @@ -1,18 +1,20 @@ +/** + * @prettier + */ describe("Render License Component", () => { - describe("OpenAPI 2", () =>{ + describe("OpenAPI 2.0", () => { const baseUrl = "/?url=/documents/features/license-openAPI2.yaml" + it("should render License URL", () => { cy.visit(baseUrl) .get(".info__license") .should("exist") .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) - .get(".info__license > .link") + .get(".info__license__url > .link") .should("have.attr", "rel") .and("include", "noopener") .and("include", "noreferrer") @@ -21,18 +23,18 @@ describe("Render License Component", () => { .and("equal", "_blank") }) }) + describe("OpenAPI 3.0.x", () => { const baseUrl = "/?url=/documents/features/license-openAPI30.yaml" + it("should render License URL", () => { cy.visit(baseUrl) .get(".info__license__url") .should("exist") .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) .get(".info__license__url > a") .should("have.attr", "rel") @@ -43,19 +45,22 @@ describe("Render License Component", () => { .and("equal", "_blank") }) }) - describe("OpenAPI 3.1.x", () => { - describe("only URL", () => { + + describe("OpenAPI 3.1.x", () => { + describe("given URL field", () => { const baseUrl = "/?url=/documents/features/license-openAPI31-url.yaml" + it("should render URL", () => { cy.visit(baseUrl) .get(".info__license__url") .should("exist") .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 ", () => { + .get(".info__license__url > a") + .should("have.attr", "href") + .and("equal", "https://www.apache.org/licenses/LICENSE-2.0.html") + }) + + it("should render URL anchor target xss link with safe `rel` attributes", () => { cy.visit(baseUrl) .get(".info__license__url > a") .should("have.attr", "rel") @@ -66,40 +71,43 @@ describe("Render License Component", () => { .and("equal", "_blank") }) }) - describe("only SPDX Identifier", () => { - const baseUrl = "/?url=/documents/features/license-openAPI31-identifier.yaml" - it("should render SPDX Identifier", () => { + + describe("given identifier field", () => { + const baseUrl = + "/?url=/documents/features/license-openAPI31-identifier.yaml" + + it("should render URL using identifier", () => { cy.visit(baseUrl) - .get(".info__license__identifier") - .should("exist") - .should("contains.text", "Apache-2.0") - .should("contains.text", "SPDX License") .get(".info__license__url") - .should("not.exist") - }) - it("should render SPDX and Identifier anchor target xss links with safe `rel` attributes ", () => { + .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 URL anchor target xss links with safe `rel` attributes", () => { cy.visit(baseUrl) - .get(".info__license__identifier > a") - .each(($el) => { - cy.get($el) - .should("have.attr", "rel") - .and("include", "noopener") - .and("include", "noreferrer") - cy.get($el) - .should("have.attr", "target") - .and("equal", "_blank") - }) + .get(".info__license__url > a") + .should("have.attr", "rel") + .and("include", "noopener") + .and("include", "noreferrer") + .get(".info .main > a") + .should("have.attr", "target") + .and("equal", "_blank") }) }) + describe("URL and SPX are mutually exclusive", () => { 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) .get(".info__license__identifier") .should("not.exist") .get(".info__license__url") .should("not.exist") - }) + }) }) }) }) diff --git a/test/unit/core/plugins/oas3/helpers.js b/test/unit/core/plugins/oas3/helpers.js index 14e31c97..7eef6d4f 100644 --- a/test/unit/core/plugins/oas3/helpers.js +++ b/test/unit/core/plugins/oas3/helpers.js @@ -1,7 +1,7 @@ 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 })) @@ -13,7 +13,7 @@ describe("isOAS3", function () { it("should recognize valid OAS3 version values", function () { expect(isOAS3Shorthand("3.0.0")).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) }) @@ -31,7 +31,7 @@ describe("isOAS3", function () { }) it("should gracefully fail when `openapi` field is missing", function () { - expect(isOAS3(fromJS({ + expect(isOAS30(fromJS({ openApi: "3.0.0" }))).toEqual(false) expect(isOAS3Shorthand(null)).toEqual(false) @@ -41,7 +41,7 @@ describe("isOAS3", function () { describe("isSwagger2", function () { it("should recognize valid Swagger 2.0 version values", function () { 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 () { diff --git a/test/unit/core/plugins/oas3/state-integration.js b/test/unit/core/plugins/oas3/state-integration.js index e80918d9..62b1f620 100644 --- a/test/unit/core/plugins/oas3/state-integration.js +++ b/test/unit/core/plugins/oas3/state-integration.js @@ -26,7 +26,8 @@ describe("OAS3 plugin - state", function() { specSelectors: { specJson: () => { return fromJS({ openapi: "3.0.0" }) - } + }, + isOAS3: () => true, } } } @@ -55,7 +56,8 @@ describe("OAS3 plugin - state", function() { specSelectors: { specJson: () => { return fromJS({ openapi: "3.0.0" }) - } + }, + isOAS3: () => true, } } } @@ -88,7 +90,8 @@ describe("OAS3 plugin - state", function() { specSelectors: { specJson: () => { return fromJS({ openapi: "3.0.0" }) - } + }, + isOAS3: () => true, } } } @@ -124,7 +127,8 @@ describe("OAS3 plugin - state", function() { specSelectors: { specJson: () => { return fromJS({ openapi: "3.0.0" }) - } + }, + isOAS3: () => true, } } } @@ -167,7 +171,8 @@ describe("OAS3 plugin - state", function() { specSelectors: { specJson: () => { return fromJS({ openapi: "3.0.0" }) - } + }, + isOAS3: () => true, } } } @@ -207,7 +212,8 @@ describe("OAS3 plugin - state", function() { specSelectors: { specJson: () => { return fromJS({ openapi: "3.0.0" }) - } + }, + isOAS3: () => true, } } } @@ -261,7 +267,8 @@ describe("OAS3 plugin - state", function() { specSelectors: { specJson: () => { return fromJS({ openapi: "3.0.0" }) - } + }, + isOAS3: () => true, } } } @@ -314,7 +321,8 @@ describe("OAS3 plugin - state", function() { specSelectors: { specJson: () => { return fromJS({ openapi: "3.0.0" }) - } + }, + isOAS3: () => true, } } } @@ -342,7 +350,8 @@ describe("OAS3 plugin - state", function() { specSelectors: { specJson: () => { return fromJS({ openapi: "3.0.0" }) - } + }, + isOAS3: () => true, } } } diff --git a/test/unit/core/plugins/oas3/wrap-auth-selectors.js b/test/unit/core/plugins/oas3/wrap-auth-selectors.js index 59fd8a90..4d3ef3bf 100644 --- a/test/unit/core/plugins/oas3/wrap-auth-selectors.js +++ b/test/unit/core/plugins/oas3/wrap-auth-selectors.js @@ -17,6 +17,7 @@ describe("oas3 plugin - auth extensions - wrapSelectors", function(){ specJson: () => fromJS({ openapi: "3.0.0" }), + isOAS3: () => true, securityDefinitions: () => { return fromJS({ "oauth2AuthorizationCode": { diff --git a/test/unit/core/plugins/oas3/wrap-spec-selectors.js b/test/unit/core/plugins/oas3/wrap-spec-selectors.js index 55b196e9..35bdcafc 100644 --- a/test/unit/core/plugins/oas3/wrap-spec-selectors.js +++ b/test/unit/core/plugins/oas3/wrap-spec-selectors.js @@ -1,8 +1,5 @@ - import { fromJS } from "immutable" -import { - definitions -} from "corePlugins/oas3/spec-extensions/wrap-selectors" +import { definitions } from "corePlugins/oas3/spec-extensions/wrap-selectors" describe("oas3 plugin - spec extensions - wrapSelectors", function(){ @@ -28,6 +25,7 @@ describe("oas3 plugin - spec extensions - wrapSelectors", function(){ getSystem: () => system, specSelectors: { specJson: () => spec, + isOAS3: () => true, } } @@ -57,6 +55,7 @@ describe("oas3 plugin - spec extensions - wrapSelectors", function(){ getSystem: () => system, specSelectors: { specJson: () => spec, + isOAS3: () => true, } } @@ -82,7 +81,8 @@ describe("oas3 plugin - spec extensions - wrapSelectors", function(){ getSystem: () => system, specSelectors: { specJson: () => spec, - } + isOAS3: () => true, + }, } // When diff --git a/test/unit/xss/anchor-target-rel/info.jsx b/test/unit/xss/anchor-target-rel/info.jsx index 75f2f15a..628f22d0 100644 --- a/test/unit/xss/anchor-target-rel/info.jsx +++ b/test/unit/xss/anchor-target-rel/info.jsx @@ -1,7 +1,9 @@ import React from "react" import { render } from "enzyme" 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 Markdown from "components/providers/markdown" @@ -11,7 +13,8 @@ describe(" Anchor Target Safety", function(){ Markdown, InfoUrl, License, - Link + Contact, + Link, } const baseProps = { getComponent: c => components[c] || dummyComponent,