Skip to content

Commit

Permalink
feat: OpenAPI 3.1 support (#8367)
Browse files Browse the repository at this point in the history
- New top-level field - `webhooks`. This allows describing out-of-band webhooks that are available as part of the API.

- New top-level field - `jsonSchemaDialect`. This allows defining of a default `$schema` value for Schema Objects

- The Info Object has a new `summary` field.

- The License Object now has a new `identifier` field for SPDX licenses. This `identifier` field is mutually exclusive with the `url` field. Either can be used in OpenAPI 3.1 definitions.

- Components Object now has a new entry `pathItems`, to allow for reusable Path Item Objects to be defined within a valid OpenAPI document.

- `License` and `Contact` components are now exported and available via `getComponent`

- New version predicates and selectors for `isOpenAPI30` and `isOpenAPI31`. This avoids needing to change the usage of `isOAS3` selector.

- New OAS3 components: `Webhooks`

- New OAS3 wrapped components: `Info`, `License`
  • Loading branch information
tim-lai authored Feb 3, 2023
1 parent f3c6a25 commit 4557b24
Show file tree
Hide file tree
Showing 30 changed files with 1,564 additions and 164 deletions.
1,065 changes: 927 additions & 138 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"deps-license": "license-checker --production --csv --out $npm_package_config_deps_check_dir/licenses.csv && license-checker --development --csv --out $npm_package_config_deps_check_dir/licenses-dev.csv",
"deps-size": "webpack -p --config webpack/bundle.babel.js --json | webpack-bundle-size-analyzer >| $npm_package_config_deps_check_dir/sizes.txt",
"deps-check": "run-s deps-license deps-size",
"link:apidom": "npm link @swagger-api/apidom-core @swagger-api/apidom-reference @swagger-api/apidom-ns-openapi-3-1 @swagger-api/apidom-ns-openapi-3-0 @swagger-api/apidom-ns-json-schema-draft-4 @swagger-api/apidom-json-pointer",
"lint": "eslint --ext \".js,.jsx\" src test dev-helpers flavors",
"lint-errors": "eslint --quiet --ext \".js,.jsx\" src test dev-helpers flavors",
"lint-fix": "eslint --ext \".js,.jsx\" src test dev-helpers flavors --fix",
Expand Down Expand Up @@ -94,7 +95,7 @@
"reselect": "^4.1.5",
"serialize-error": "^8.1.0",
"sha.js": "^2.4.11",
"swagger-client": "^3.18.5",
"swagger-client": "=3.19.0-alpha.4",
"url-parse": "^1.5.8",
"xml": "=1.0.1",
"xml-but-prettier": "^1.0.1",
Expand Down Expand Up @@ -122,7 +123,7 @@
"autoprefixer": "^10.4.12",
"babel-loader": "^8.2.3",
"babel-plugin-lodash": "=3.3.4",
"babel-plugin-module-resolver": "=4.1.0",
"babel-plugin-module-resolver": "=5.0.0",
"babel-plugin-transform-react-remove-prop-types": "=0.4.24",
"body-parser": "^1.19.0",
"buffer": "^6.0.3",
Expand Down Expand Up @@ -187,6 +188,13 @@
"webpack-node-externals": "=3.0.0",
"webpack-stats-plugin": "=1.0.3"
},
"overrides": {
"swagger-client": {
"@swagger-api/apidom-reference": {
"axios": "npm:[email protected]"
}
}
},
"config": {
"deps_check_dir": ".deps_check"
}
Expand Down
6 changes: 3 additions & 3 deletions src/core/components/info.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class InfoBasePath extends React.Component {
}


class Contact extends React.Component {
export class Contact extends React.Component {
static propTypes = {
data: PropTypes.object,
getComponent: PropTypes.func.isRequired,
Expand Down Expand Up @@ -53,7 +53,7 @@ class Contact extends React.Component {
}
}

class License extends React.Component {
export class License extends React.Component {
static propTypes = {
license: PropTypes.object,
getComponent: PropTypes.func.isRequired,
Expand All @@ -64,7 +64,6 @@ class License extends React.Component {

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})
Expand Down Expand Up @@ -125,6 +124,7 @@ export default class Info extends React.Component {
const VersionStamp = getComponent("VersionStamp")
const InfoUrl = getComponent("InfoUrl")
const InfoBasePath = getComponent("InfoBasePath")
const License = getComponent("License")

return (
<div className="info">
Expand Down
9 changes: 9 additions & 0 deletions src/core/components/layouts/base.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default class BaseLayout extends React.Component {
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)
Expand All @@ -30,6 +31,7 @@ export default class BaseLayout extends React.Component {
const FilterContainer = getComponent("FilterContainer", true)
let isSwagger2 = specSelectors.isSwagger2()
let isOAS3 = specSelectors.isOAS3()
const isOpenAPI31 = specSelectors.selectIsOpenAPI31()

const isSpecEmpty = !specSelectors.specStr()

Expand Down Expand Up @@ -112,6 +114,13 @@ export default class BaseLayout extends React.Component {
<Operations/>
</Col>
</Row>
{ isOpenAPI31 &&
<Row className="webhooks-container">
<Col mobile={12} desktop={12} >
<Webhooks />
</Col>
</Row>
}
<Row>
<Col mobile={12} desktop={12} >
<Models/>
Expand Down
4 changes: 3 additions & 1 deletion src/core/plugins/oas3/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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,
Expand All @@ -15,5 +16,6 @@ export default {
ServersContainer,
RequestBodyEditor,
OperationServers,
operationLink: OperationLink
operationLink: OperationLink,
Webhooks
}
10 changes: 5 additions & 5 deletions src/core/plugins/oas3/components/request-body.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { getCommonExtensions, getSampleSchema, stringify, isEmptyValue } from "c
import { getKnownSyntaxHighlighterLanguage } from "core/utils/jsonParse"

export const getDefaultRequestBodyValue = (requestBody, mediaType, activeExamplesKey) => {
const mediaTypeValue = requestBody.getIn(["content", mediaType])
const schema = mediaTypeValue.get("schema").toJS()
const mediaTypeValue = requestBody?.getIn(["content", mediaType])
const schema = mediaTypeValue?.get("schema").toJS()

const hasExamplesKey = mediaTypeValue.get("examples") !== undefined
const exampleSchema = mediaTypeValue.get("example")
const hasExamplesKey = mediaTypeValue?.get("examples") !== undefined
const exampleSchema = mediaTypeValue?.get("example")
const mediaTypeExample = hasExamplesKey
? mediaTypeValue.getIn([
? mediaTypeValue?.getIn([
"examples",
activeExamplesKey,
"value"
Expand Down
60 changes: 60 additions & 0 deletions src/core/plugins/oas3/components/webhooks.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// OpenAPI 3.1 feature
import React from "react"
import PropTypes from "prop-types"
import { fromJS } from "immutable"
import ImPropTypes from "react-immutable-proptypes"

// Todo: nice to have: similar to operation-tags, could have an expand/collapse button
// to show/hide all webhook items
const Webhooks = (props) => {
const { specSelectors, getComponent, specPath } = props

const webhooksPathItems = specSelectors.selectWebhooks() // OrderedMap
if (!webhooksPathItems || webhooksPathItems?.size < 1) {
return null
}
const OperationContainer = getComponent("OperationContainer", true)

const pathItemsElements = webhooksPathItems.entrySeq().map(([pathItemName, pathItem], i) => {
const operationsElements = pathItem.entrySeq().map(([operationMethod, operation], j) => {
const op = fromJS({
operation
})
// using defaultProps for `specPath`; may want to remove from props
// and/or if extract to separate PathItem component, allow for use
// with both OAS3.1 "webhooks" and "components.pathItems" features
return <OperationContainer
{...props}
op={op}
key={`${pathItemName}--${operationMethod}--${j}`}
tag={""}
method={operationMethod}
path={pathItemName}
specPath={specPath.push("webhooks", pathItemName, operationMethod)}
allowTryItOut={false}
/>
})
return <div key={`${pathItemName}-${i}`}>
{operationsElements}
</div>
})

return (
<div className="webhooks">
<h2>Webhooks</h2>
{pathItemsElements}
</div>
)
}

Webhooks.propTypes = {
specSelectors: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired,
specPath: ImPropTypes.list,
}

Webhooks.defaultProps = {
specPath: fromJS([])
}

export default Webhooks
23 changes: 17 additions & 6 deletions src/core/plugins/oas3/helpers.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import React from "react"

export function isOAS3(jsSpec) {
export function isOpenAPI30(jsSpec) {
const oasVersion = jsSpec.get("openapi")
if(typeof oasVersion !== "string") {
if (typeof oasVersion !== "string") {
return false
}
return oasVersion.startsWith("3.0.") && oasVersion.length > 4
}

// we gate against `3.1` because we want to explicitly opt into supporting it
// at some point in the future -- KS, 7/2018
export function isOpenAPI31(jsSpec) {
const oasVersion = jsSpec.get("openapi")
if (typeof oasVersion !== "string") {
return false
}
return oasVersion.startsWith("3.1.") && oasVersion.length > 4
}

// starts with, but is not `3.0.` exactly
return oasVersion.startsWith("3.0.") && 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) {
Expand Down
24 changes: 23 additions & 1 deletion src/core/plugins/oas3/spec-extensions/selectors.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createSelector } from "reselect"
import { Map } from "immutable"
import { isOAS3 as isOAS3Helper, isSwagger2 as isSwagger2Helper } from "../helpers"
import { isOAS3 as isOAS3Helper, isOpenAPI31 as isOpenAPI31Helper, isSwagger2 as isSwagger2Helper } from "../helpers"


// Helpers

// 1/2023: as of now, more accurately, isAnyOAS3
function onlyOAS3(selector) {
return () => (system, ...args) => {
const spec = system.getSystem().specSelectors.specJson()
Expand All @@ -16,6 +17,17 @@ function onlyOAS3(selector) {
}
}

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()
}
Expand Down Expand Up @@ -48,3 +60,13 @@ export const isSwagger2 = (ori, system) => () => {
const spec = system.getSystem().specSelectors.specJson()
return isSwagger2Helper(spec)
}

export const selectIsOpenAPI31 = (ori, system) => () => {
const spec = system.getSystem().specSelectors.specJson()
return isOpenAPI31Helper(spec)
}

export const selectWebhooks = isOpenAPI31(createSelector(
spec,
spec => spec.getIn(["webhooks"]) || Map()
))
26 changes: 24 additions & 2 deletions src/core/plugins/oas3/spec-extensions/wrap-selectors.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createSelector } from "reselect"
import { specJsonWithResolvedSubtrees } from "../../spec/selectors"
import { Map } from "immutable"
import { isOAS3 as isOAS3Helper, isSwagger2 as isSwagger2Helper } from "../helpers"
import { isOAS3 as isOAS3Helper, isOpenAPI31 as isOpenAPI31Helper, isSwagger2 as isSwagger2Helper } from "../helpers"


// Helpers

// 1/2023: as of now, more accurately, isAnyOAS3
function onlyOAS3(selector) {
return (ori, system) => (...args) => {
const spec = system.getSystem().specSelectors.specJson()
Expand All @@ -17,6 +17,17 @@ function onlyOAS3(selector) {
}
}

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()
}
Expand Down Expand Up @@ -83,3 +94,14 @@ 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()
))

4 changes: 4 additions & 0 deletions src/core/plugins/oas3/wrap-components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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,
Expand All @@ -12,4 +14,6 @@ export default {
VersionStamp,
model: Model,
onlineValidatorBadge: OnlineValidatorBadge,
License,
info,
}
Loading

0 comments on commit 4557b24

Please sign in to comment.