feat(json-schema-2020-12): add initial rendering engine

Refs #8513
This commit is contained in:
Vladimir Gorej
2023-04-12 17:09:35 +02:00
committed by Vladimír Gorej
parent dbd8931161
commit ab1842083d
17 changed files with 311 additions and 1 deletions

View File

@@ -0,0 +1,27 @@
/**
* @prettier
*/
import React from "react"
import PropTypes from "prop-types"
import { booleanSchema } from "../../prop-types"
const BooleanJSONSchema = ({ schema, name }) => {
return (
<article className="json-schema-2020-12 json-schema-2020-12--boolean">
<span>{name}</span>
<span>{schema ? "true" : "false"}</span>
</article>
)
}
BooleanJSONSchema.propTypes = {
schema: booleanSchema.isRequired,
name: PropTypes.string,
}
BooleanJSONSchema.defaultProps = {
name: "",
}
export default BooleanJSONSchema

View File

@@ -0,0 +1,41 @@
/**
* @prettier
*/
import React from "react"
import PropTypes from "prop-types"
import * as propTypes from "../../prop-types"
import { useComponent, useFn } from "../../hooks"
const JSONSchema = ({ schema, name }) => {
const fn = useFn()
const BooleanJSONSchema = useComponent("BooleanJSONSchema")
if (fn.isBooleanJSONSchema(schema)) {
return <BooleanJSONSchema schema={schema} name={name} />
}
return (
<article className="json-schema-2020-12 model-container">
<div className="model-box">
<div className="model">
<div className="json-schema-2020-12__title model-title">
{name || fn.getTitle(schema)}
</div>
</div>
</div>
</article>
)
}
JSONSchema.propTypes = {
name: PropTypes.string,
schema: propTypes.schema.isRequired,
}
JSONSchema.defaultProps = {
name: "",
}
export default JSONSchema

View File

@@ -0,0 +1,2 @@
@import './BooleanJSONSchema/boolean-json-schema';
@import './JSONSchema/json-schema';

View File

@@ -0,0 +1,9 @@
/**
* @prettier
*/
import { createContext } from "react"
export const JSONSchemaContext = createContext(null)
JSONSchemaContext.displayName = "JSONSchemaContext"
export default JSONSchemaContext

View File

@@ -0,0 +1,20 @@
/**
* @prettier
*/
export const upperFirst = (value) => {
if (typeof value === "string") {
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`
}
return value
}
export const getTitle = (schema) => {
if (schema.title) return upperFirst(schema.title)
if (schema.$anchor) return upperFirst(schema.$anchor)
if (schema.$id) return schema.$id
return ""
}
export const isBooleanJSONSchema = (schema) => typeof schema === "boolean"

View File

@@ -0,0 +1,41 @@
/**
* @prettier
*/
import React from "react"
import JSONSchema from "./components/JSONSchema/JSONSchema"
import BooleanJSONSchema from "./components/BooleanJSONSchema/BooleanJSONSchema"
import JSONSchemaContext from "./context"
import { getTitle, isBooleanJSONSchema, upperFirst } from "./fn"
export const withJSONSchemaContext = (Component, overrides = {}) => {
const value = {
components: {
JSONSchema,
BooleanJSONSchema,
...overrides.components,
},
config: {
default$schema: "https://json-schema.org/draft/2020-12/schema",
...overrides.config,
},
fn: {
upperFirst,
getTitle,
isBooleanJSONSchema,
...overrides.fn,
},
}
const HOC = (props) => (
<JSONSchemaContext.Provider value={value}>
<Component {...props} />
</JSONSchemaContext.Provider>
)
HOC.contexts = {
JSONSchemaContext,
}
HOC.displayName = Component.displayName
return HOC
}

View File

@@ -0,0 +1,22 @@
/**
* @prettier
*/
import { useContext } from "react"
import JSONSchemaContext from "./context"
export const useConfig = () => {
const { config } = useContext(JSONSchemaContext)
return config
}
export const useComponent = (componentName) => {
const { components } = useContext(JSONSchemaContext)
return components[componentName] || null
}
export const useFn = (fnName = undefined) => {
const { fn } = useContext(JSONSchemaContext)
return typeof fnName !== "undefined" ? fn[fnName] : fn
}

View File

@@ -0,0 +1,20 @@
/**
* @prettier
*/
import JSONSchema from "./components/JSONSchema/JSONSchema"
import BooleanJSONSchema from "./components/BooleanJSONSchema/BooleanJSONSchema"
import { upperFirst } from "./fn"
import { withJSONSchemaContext } from "./hoc"
const JSONSchema202012Plugin = () => ({
components: {
JSONSchema202012: JSONSchema,
BooleanJSONSchema202012: BooleanJSONSchema,
withJSONSchema202012Context: withJSONSchemaContext,
},
fn: {
upperFirst,
},
})
export default JSONSchema202012Plugin

View File

@@ -0,0 +1,10 @@
/**
* @prettier
*/
import PropTypes from "prop-types"
export const objectSchema = PropTypes.object
export const booleanSchema = PropTypes.bool
export const schema = PropTypes.oneOfType([objectSchema, booleanSchema])

View File

@@ -0,0 +1,72 @@
/**
* @prettier
*/
import React, { useCallback } from "react"
import PropTypes from "prop-types"
import classNames from "classnames"
const Models = ({
specSelectors,
layoutSelectors,
layoutActions,
getComponent,
getConfigs,
fn,
}) => {
const schemas = specSelectors.selectSchemas()
const schemasPath = ["components", "schemas"]
const { docExpansion, defaultModelsExpandDepth } = getConfigs()
const isOpenDefault = defaultModelsExpandDepth > 0 && docExpansion !== "none"
const isOpen = layoutSelectors.isShown(schemasPath, isOpenDefault)
const Collapse = getComponent("Collapse")
const JSONSchema202012 = getComponent("JSONSchema202012")
const handleCollapse = useCallback(() => {
layoutActions.show(schemasPath, !isOpen)
}, [layoutActions, schemasPath, isOpen])
return (
<section className={classNames("models", { "is-open": isOpen })}>
<h4>
<button
aria-expanded={isOpen}
className="models-control"
onClick={handleCollapse}
>
<span>Schemas</span>
<svg width="20" height="20" aria-hidden="true" focusable="false">
<use xlinkHref={isOpen ? "#large-arrow-up" : "#large-arrow-down"} />
</svg>
</button>
</h4>
<Collapse isOpened={isOpen}>
{Object.entries(schemas).map(([schemaName, schema]) => (
<JSONSchema202012
key={schemaName}
schema={schema}
name={fn.upperFirst(schemaName)}
/>
))}
</Collapse>
</section>
)
}
Models.propTypes = {
getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.isRequired,
specSelectors: PropTypes.shape({
selectSchemas: PropTypes.func.isRequired,
}).isRequired,
layoutSelectors: PropTypes.shape({
isShown: PropTypes.func.isRequired,
}).isRequired,
layoutActions: PropTypes.shape({
show: PropTypes.func.isRequired,
}).isRequired,
fn: PropTypes.shape({
upperFirst: PropTypes.func.isRequired,
}).isRequired,
}
export default Models

View File

@@ -7,9 +7,11 @@ import Contact from "./components/contact"
import Info from "./components/info"
import JsonSchemaDialect from "./components/json-schema-dialect"
import VersionPragmaFilter from "./components/version-pragma-filter"
import Models from "./components/models"
import LicenseWrapper from "./wrap-components/license"
import ContactWrapper from "./wrap-components/contact"
import InfoWrapper from "./wrap-components/info"
import ModelsWrapper from "./wrap-components/models"
import VersionPragmaFilterWrapper from "./wrap-components/version-pragma-filter"
import VersionStampWrapper from "./wrap-components/version-stamp"
import {
@@ -36,6 +38,7 @@ import {
selectWebhooksOperations,
selectJsonSchemaDialectField,
selectJsonSchemaDialectDefault,
selectSchemas,
} from "./spec-extensions/selectors"
import {
isOAS3 as isOAS3SelectorWrapper,
@@ -65,6 +68,7 @@ const OAS31Plugin = ({ fn }) => {
OAS31License: License,
OAS31Contact: Contact,
OAS31VersionPragmaFilter: VersionPragmaFilter,
OAS31Models: Models,
},
wrapComponents: {
InfoContainer: InfoWrapper,
@@ -72,6 +76,7 @@ const OAS31Plugin = ({ fn }) => {
Contact: ContactWrapper,
VersionPragmaFilter: VersionPragmaFilterWrapper,
VersionStamp: VersionStampWrapper,
Models: ModelsWrapper,
},
statePlugins: {
spec: {
@@ -105,6 +110,8 @@ const OAS31Plugin = ({ fn }) => {
selectJsonSchemaDialectField,
selectJsonSchemaDialectDefault,
selectSchemas: createSystemSelector(selectSchemas),
},
wrapSelectors: {
isOAS3: isOAS3SelectorWrapper,

View File

@@ -166,3 +166,10 @@ export const selectJsonSchemaDialectField = () => (system) => {
export const selectJsonSchemaDialectDefault = () =>
"https://spec.openapis.org/oas/3.1/dialect/base"
export const selectSchemas = createSelector(
(state, system) => system.specSelectors.definitions(),
(schemas) => {
return Map.isMap(schemas) ? schemas.toJS() : {}
}
)

View File

@@ -0,0 +1,30 @@
/**
* @prettier
*/
import React from "react"
import { createOnlyOAS31ComponentWrapper } from "../fn"
const ModelsWrapper = createOnlyOAS31ComponentWrapper(({ getSystem }) => {
const { getComponent, fn } = getSystem()
const Models = getComponent("OAS31Models", true)
const JSONSchema = getComponent("JSONSchema202012")
const BooleanJSONSchema = getComponent("BooleanJSONSchema202012")
const withSchemaContext = getComponent("withJSONSchema202012Context")
const ModelsWithJSONContext = withSchemaContext(Models, {
config: {
default$schema: "https://spec.openapis.org/oas/3.1/dialect/base",
},
components: {
JSONSchema,
BooleanJSONSchema,
},
fn: {
upperFirst: fn.upperFirst,
},
})
return <ModelsWithJSONContext />
})
export default ModelsWrapper

View File

@@ -4,7 +4,8 @@
import BasePreset from "./base"
import OAS3Plugin from "../plugins/oas3"
import OAS31Plugin from "../plugins/oas31"
import JSONSchema202012Plugin from "../plugins/json-schema-2020-12"
export default function PresetApis() {
return [BasePreset, OAS3Plugin, OAS31Plugin]
return [BasePreset, OAS3Plugin, OAS31Plugin, JSONSchema202012Plugin]
}

View File

@@ -15,6 +15,7 @@
@import 'information';
@import 'authorize';
@import 'errors';
@import '../core/plugins/json-schema-2020-12/components/all';
@include text_body();
@import 'split-pane-mode';
@import 'markdown';