feat(json-schema-2020-12): add support for dependentRequired keyword (#8633)

Refs #8513
This commit is contained in:
Vladimír Gorej
2023-05-08 15:13:23 +02:00
committed by GitHub
parent 03a561f1d9
commit ee868aa7ea
10 changed files with 276 additions and 173 deletions

View File

@@ -21,181 +21,193 @@ import {
JSONSchemaCyclesContext, JSONSchemaCyclesContext,
} from "../../context" } from "../../context"
const JSONSchema = forwardRef(({ schema, name, onExpand }, ref) => { const JSONSchema = forwardRef(
const fn = useFn() ({ schema, name, dependentRequired, onExpand }, ref) => {
const isExpandedDeeply = useIsExpandedDeeply() const fn = useFn()
const [expanded, setExpanded] = useState(isExpandedDeeply) const isExpandedDeeply = useIsExpandedDeeply()
const [expandedDeeply, setExpandedDeeply] = useState(isExpandedDeeply) const [expanded, setExpanded] = useState(isExpandedDeeply)
const [level, nextLevel] = useLevel() const [expandedDeeply, setExpandedDeeply] = useState(isExpandedDeeply)
const isEmbedded = useIsEmbedded() const [level, nextLevel] = useLevel()
const isExpandable = fn.isExpandable(schema) const isEmbedded = useIsEmbedded()
const isCircular = useIsCircular(schema) const isExpandable = fn.isExpandable(schema) || dependentRequired.length > 0
const renderedSchemas = useRenderedSchemas(schema) const isCircular = useIsCircular(schema)
const constraints = fn.stringifyConstraints(schema) const renderedSchemas = useRenderedSchemas(schema)
const Accordion = useComponent("Accordion") const constraints = fn.stringifyConstraints(schema)
const Keyword$schema = useComponent("Keyword$schema") const Accordion = useComponent("Accordion")
const Keyword$vocabulary = useComponent("Keyword$vocabulary") const Keyword$schema = useComponent("Keyword$schema")
const Keyword$id = useComponent("Keyword$id") const Keyword$vocabulary = useComponent("Keyword$vocabulary")
const Keyword$anchor = useComponent("Keyword$anchor") const Keyword$id = useComponent("Keyword$id")
const Keyword$dynamicAnchor = useComponent("Keyword$dynamicAnchor") const Keyword$anchor = useComponent("Keyword$anchor")
const Keyword$ref = useComponent("Keyword$ref") const Keyword$dynamicAnchor = useComponent("Keyword$dynamicAnchor")
const Keyword$dynamicRef = useComponent("Keyword$dynamicRef") const Keyword$ref = useComponent("Keyword$ref")
const Keyword$defs = useComponent("Keyword$defs") const Keyword$dynamicRef = useComponent("Keyword$dynamicRef")
const Keyword$comment = useComponent("Keyword$comment") const Keyword$defs = useComponent("Keyword$defs")
const KeywordAllOf = useComponent("KeywordAllOf") const Keyword$comment = useComponent("Keyword$comment")
const KeywordAnyOf = useComponent("KeywordAnyOf") const KeywordAllOf = useComponent("KeywordAllOf")
const KeywordOneOf = useComponent("KeywordOneOf") const KeywordAnyOf = useComponent("KeywordAnyOf")
const KeywordNot = useComponent("KeywordNot") const KeywordOneOf = useComponent("KeywordOneOf")
const KeywordIf = useComponent("KeywordIf") const KeywordNot = useComponent("KeywordNot")
const KeywordThen = useComponent("KeywordThen") const KeywordIf = useComponent("KeywordIf")
const KeywordElse = useComponent("KeywordElse") const KeywordThen = useComponent("KeywordThen")
const KeywordDependentSchemas = useComponent("KeywordDependentSchemas") const KeywordElse = useComponent("KeywordElse")
const KeywordPrefixItems = useComponent("KeywordPrefixItems") const KeywordDependentSchemas = useComponent("KeywordDependentSchemas")
const KeywordItems = useComponent("KeywordItems") const KeywordPrefixItems = useComponent("KeywordPrefixItems")
const KeywordContains = useComponent("KeywordContains") const KeywordItems = useComponent("KeywordItems")
const KeywordProperties = useComponent("KeywordProperties") const KeywordContains = useComponent("KeywordContains")
const KeywordPatternProperties = useComponent("KeywordPatternProperties") const KeywordProperties = useComponent("KeywordProperties")
const KeywordAdditionalProperties = useComponent( const KeywordPatternProperties = useComponent("KeywordPatternProperties")
"KeywordAdditionalProperties" const KeywordAdditionalProperties = useComponent(
) "KeywordAdditionalProperties"
const KeywordPropertyNames = useComponent("KeywordPropertyNames") )
const KeywordUnevaluatedItems = useComponent("KeywordUnevaluatedItems") const KeywordPropertyNames = useComponent("KeywordPropertyNames")
const KeywordUnevaluatedProperties = useComponent( const KeywordUnevaluatedItems = useComponent("KeywordUnevaluatedItems")
"KeywordUnevaluatedProperties" const KeywordUnevaluatedProperties = useComponent(
) "KeywordUnevaluatedProperties"
const KeywordType = useComponent("KeywordType") )
const KeywordEnum = useComponent("KeywordEnum") const KeywordType = useComponent("KeywordType")
const KeywordConst = useComponent("KeywordConst") const KeywordEnum = useComponent("KeywordEnum")
const KeywordConstraint = useComponent("KeywordConstraint") const KeywordConst = useComponent("KeywordConst")
const KeywordFormat = useComponent("KeywordFormat") const KeywordConstraint = useComponent("KeywordConstraint")
const KeywordTitle = useComponent("KeywordTitle") const KeywordDependentRequired = useComponent("KeywordDependentRequired")
const KeywordDescription = useComponent("KeywordDescription") const KeywordFormat = useComponent("KeywordFormat")
const ExpandDeepButton = useComponent("ExpandDeepButton") const KeywordTitle = useComponent("KeywordTitle")
const KeywordDescription = useComponent("KeywordDescription")
const ExpandDeepButton = useComponent("ExpandDeepButton")
/** /**
* Effects handlers. * Effects handlers.
*/ */
useEffect(() => { useEffect(() => {
setExpandedDeeply(isExpandedDeeply) setExpandedDeeply(isExpandedDeeply)
}, [isExpandedDeeply]) }, [isExpandedDeeply])
useEffect(() => { useEffect(() => {
setExpandedDeeply(expandedDeeply) setExpandedDeeply(expandedDeeply)
}, [expandedDeeply]) }, [expandedDeeply])
/** /**
* Event handlers. * Event handlers.
*/ */
const handleExpansion = useCallback( const handleExpansion = useCallback(
(e, expandedNew) => { (e, expandedNew) => {
setExpanded(expandedNew) setExpanded(expandedNew)
!expandedNew && setExpandedDeeply(false) !expandedNew && setExpandedDeeply(false)
onExpand(e, expandedNew, false) onExpand(e, expandedNew, false)
}, },
[onExpand] [onExpand]
) )
const handleExpansionDeep = useCallback( const handleExpansionDeep = useCallback(
(e, expandedDeepNew) => { (e, expandedDeepNew) => {
setExpanded(expandedDeepNew) setExpanded(expandedDeepNew)
setExpandedDeeply(expandedDeepNew) setExpandedDeeply(expandedDeepNew)
onExpand(e, expandedDeepNew, true) onExpand(e, expandedDeepNew, true)
}, },
[onExpand] [onExpand]
) )
return ( return (
<JSONSchemaLevelContext.Provider value={nextLevel}> <JSONSchemaLevelContext.Provider value={nextLevel}>
<JSONSchemaDeepExpansionContext.Provider value={expandedDeeply}> <JSONSchemaDeepExpansionContext.Provider value={expandedDeeply}>
<JSONSchemaCyclesContext.Provider value={renderedSchemas}> <JSONSchemaCyclesContext.Provider value={renderedSchemas}>
<article <article
ref={ref} ref={ref}
data-json-schema-level={level} data-json-schema-level={level}
className={classNames("json-schema-2020-12", { className={classNames("json-schema-2020-12", {
"json-schema-2020-12--embedded": isEmbedded, "json-schema-2020-12--embedded": isEmbedded,
"json-schema-2020-12--circular": isCircular, "json-schema-2020-12--circular": isCircular,
})}
>
<div className="json-schema-2020-12-head">
{isExpandable && !isCircular ? (
<>
<Accordion expanded={expanded} onChange={handleExpansion}>
<KeywordTitle title={name} schema={schema} />
</Accordion>
<ExpandDeepButton
expanded={expanded}
onClick={handleExpansionDeep}
/>
</>
) : (
<KeywordTitle title={name} schema={schema} />
)}
<KeywordType schema={schema} isCircular={isCircular} />
<KeywordFormat schema={schema} />
{constraints.length > 0 &&
constraints.map((constraint) => (
<KeywordConstraint key={constraint} constraint={constraint} />
))}
</div>
<div
className={classNames("json-schema-2020-12-body", {
"json-schema-2020-12-body--collapsed": !expanded,
})} })}
> >
{expanded && ( <div className="json-schema-2020-12-head">
<> {isExpandable && !isCircular ? (
<KeywordDescription schema={schema} /> <>
{!isCircular && isExpandable && ( <Accordion expanded={expanded} onChange={handleExpansion}>
<> <KeywordTitle title={name} schema={schema} />
<KeywordProperties schema={schema} /> </Accordion>
<KeywordPatternProperties schema={schema} /> <ExpandDeepButton
<KeywordAdditionalProperties schema={schema} /> expanded={expanded}
<KeywordUnevaluatedProperties schema={schema} /> onClick={handleExpansionDeep}
<KeywordPropertyNames schema={schema} /> />
<KeywordAllOf schema={schema} /> </>
<KeywordAnyOf schema={schema} /> ) : (
<KeywordOneOf schema={schema} /> <KeywordTitle title={name} schema={schema} />
<KeywordNot schema={schema} /> )}
<KeywordIf schema={schema} /> <KeywordType schema={schema} isCircular={isCircular} />
<KeywordThen schema={schema} /> <KeywordFormat schema={schema} />
<KeywordElse schema={schema} /> {constraints.length > 0 &&
<KeywordDependentSchemas schema={schema} /> constraints.map((constraint) => (
<KeywordPrefixItems schema={schema} /> <KeywordConstraint
<KeywordItems schema={schema} /> key={constraint}
<KeywordUnevaluatedItems schema={schema} /> constraint={constraint}
<KeywordContains schema={schema} /> />
</> ))}
)} </div>
<KeywordEnum schema={schema} /> <div
<KeywordConst schema={schema} /> className={classNames("json-schema-2020-12-body", {
<Keyword$schema schema={schema} /> "json-schema-2020-12-body--collapsed": !expanded,
<Keyword$vocabulary schema={schema} /> })}
<Keyword$id schema={schema} /> >
<Keyword$anchor schema={schema} /> {expanded && (
<Keyword$dynamicAnchor schema={schema} /> <>
<Keyword$ref schema={schema} /> <KeywordDescription schema={schema} />
{!isCircular && isExpandable && ( {!isCircular && isExpandable && (
<Keyword$defs schema={schema} /> <>
)} <KeywordProperties schema={schema} />
<Keyword$dynamicRef schema={schema} /> <KeywordPatternProperties schema={schema} />
<Keyword$comment schema={schema} /> <KeywordAdditionalProperties schema={schema} />
</> <KeywordUnevaluatedProperties schema={schema} />
)} <KeywordPropertyNames schema={schema} />
</div> <KeywordAllOf schema={schema} />
</article> <KeywordAnyOf schema={schema} />
</JSONSchemaCyclesContext.Provider> <KeywordOneOf schema={schema} />
</JSONSchemaDeepExpansionContext.Provider> <KeywordNot schema={schema} />
</JSONSchemaLevelContext.Provider> <KeywordIf schema={schema} />
) <KeywordThen schema={schema} />
}) <KeywordElse schema={schema} />
<KeywordDependentSchemas schema={schema} />
<KeywordPrefixItems schema={schema} />
<KeywordItems schema={schema} />
<KeywordUnevaluatedItems schema={schema} />
<KeywordContains schema={schema} />
</>
)}
<KeywordEnum schema={schema} />
<KeywordConst schema={schema} />
<KeywordDependentRequired
schema={schema}
dependentRequired={dependentRequired}
/>
<Keyword$schema schema={schema} />
<Keyword$vocabulary schema={schema} />
<Keyword$id schema={schema} />
<Keyword$anchor schema={schema} />
<Keyword$dynamicAnchor schema={schema} />
<Keyword$ref schema={schema} />
{!isCircular && isExpandable && (
<Keyword$defs schema={schema} />
)}
<Keyword$dynamicRef schema={schema} />
<Keyword$comment schema={schema} />
</>
)}
</div>
</article>
</JSONSchemaCyclesContext.Provider>
</JSONSchemaDeepExpansionContext.Provider>
</JSONSchemaLevelContext.Provider>
)
}
)
JSONSchema.propTypes = { JSONSchema.propTypes = {
name: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), name: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
schema: propTypes.schema.isRequired, schema: propTypes.schema.isRequired,
dependentRequired: PropTypes.arrayOf(PropTypes.string),
onExpand: PropTypes.func, onExpand: PropTypes.func,
} }
JSONSchema.defaultProps = { JSONSchema.defaultProps = {
name: "", name: "",
dependentRequired: [],
onExpand: () => {}, onExpand: () => {},
} }

View File

@@ -0,0 +1,35 @@
/**
* @prettier
*/
import React from "react"
import PropTypes from "prop-types"
import * as propTypes from "../../../prop-types"
const DependentRequired = ({ dependentRequired }) => {
if (dependentRequired.length === 0) return null
return (
<div className="json-schema-2020-12-keyword json-schema-2020-12-keyword--dependentRequired">
<span className="json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary">
Required when defined
</span>
<ul>
{dependentRequired.map((propertyName) => (
<li key={propertyName}>
<span className="json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--warning">
{propertyName}
</span>
</li>
))}
</ul>
</div>
)
}
DependentRequired.propTypes = {
schema: propTypes.schema.isRequired,
dependentRequired: PropTypes.arrayOf(PropTypes.string).isRequired,
}
export default DependentRequired

View File

@@ -0,0 +1,12 @@
.json-schema-2020-12-keyword--dependentRequired {
& > ul {
display: inline-block;
padding: 0;
margin: 0;
li {
display: inline;
list-style-type: none;
}
}
}

View File

@@ -5,9 +5,10 @@ import React from "react"
import classNames from "classnames" import classNames from "classnames"
import { schema } from "../../../prop-types" import { schema } from "../../../prop-types"
import { useComponent } from "../../../hooks" import { useFn, useComponent } from "../../../hooks"
const Properties = ({ schema }) => { const Properties = ({ schema }) => {
const fn = useFn()
const properties = schema?.properties || {} const properties = schema?.properties || {}
const required = Array.isArray(schema?.required) ? schema.required : [] const required = Array.isArray(schema?.required) ? schema.required : []
const JSONSchema = useComponent("JSONSchema") const JSONSchema = useComponent("JSONSchema")
@@ -22,17 +23,28 @@ const Properties = ({ schema }) => {
return ( return (
<div className="json-schema-2020-12-keyword json-schema-2020-12-keyword--properties"> <div className="json-schema-2020-12-keyword json-schema-2020-12-keyword--properties">
<ul> <ul>
{Object.entries(properties).map(([propertyName, schema]) => ( {Object.entries(properties).map(([propertyName, propertySchema]) => {
<li const isRequired = required.includes(propertyName)
key={propertyName} const dependentRequired = fn.getDependentRequired(
className={classNames("json-schema-2020-12-property", { propertyName,
"json-schema-2020-12-property--required": schema
required.includes(propertyName), )
})}
> return (
<JSONSchema name={propertyName} schema={schema} /> <li
</li> key={propertyName}
))} className={classNames("json-schema-2020-12-property", {
"json-schema-2020-12-property--required": isRequired,
})}
>
<JSONSchema
name={propertyName}
schema={propertySchema}
dependentRequired={dependentRequired}
/>
</li>
)
})}
</ul> </ul>
</div> </div>
) )

View File

@@ -11,7 +11,7 @@
list-style-type: none; list-style-type: none;
&--required { &--required {
.json-schema-2020-12__title:after { & > .json-schema-2020-12:first-of-type > .json-schema-2020-12-head .json-schema-2020-12__title:after {
content: '*'; content: '*';
color: red; color: red;
font-weight: bold; font-weight: bold;

View File

@@ -53,6 +53,12 @@
border: 1px dashed #6b6b6b; border: 1px dashed #6b6b6b;
border-radius: 4px; border-radius: 4px;
} }
&--warning {
@extend .json-schema-2020-12-keyword__value--const;
color: red;
border: 1px dashed red;
}
} }
} }
.json-schema-2020-12-keyword__name--secondary + .json-schema-2020-12-keyword__value--secondary::before { .json-schema-2020-12-keyword__name--secondary + .json-schema-2020-12-keyword__value--secondary::before {
@@ -68,3 +74,4 @@
@import './PatternProperties/pattern-properties'; @import './PatternProperties/pattern-properties';
@import './Enum/enum'; @import './Enum/enum';
@import './Constraint/constraint'; @import './Constraint/constraint';
@import './DependentRequired/dependent-required';

View File

@@ -294,3 +294,18 @@ export const stringifyConstraints = (schema) => {
return constraints return constraints
} }
export const getDependentRequired = (propertyName, schema) => {
if (!schema?.dependentRequired) return []
return Array.from(
Object.entries(schema.dependentRequired).reduce((acc, [prop, list]) => {
if (!Array.isArray(list)) return acc
if (!list.includes(propertyName)) return acc
acc.add(prop)
return acc
}, new Set())
)
}

View File

@@ -34,6 +34,7 @@ import KeywordType from "./components/keywords/Type/Type"
import KeywordEnum from "./components/keywords/Enum/Enum" import KeywordEnum from "./components/keywords/Enum/Enum"
import KeywordConst from "./components/keywords/Const" import KeywordConst from "./components/keywords/Const"
import KeywordConstraint from "./components/keywords/Constraint/Constraint" import KeywordConstraint from "./components/keywords/Constraint/Constraint"
import KeywordDependentRequired from "./components/keywords/DependentRequired/DependentRequired"
import KeywordFormat from "./components/keywords/Format/Format" import KeywordFormat from "./components/keywords/Format/Format"
import KeywordTitle from "./components/keywords/Title/Title" import KeywordTitle from "./components/keywords/Title/Title"
import KeywordDescription from "./components/keywords/Description/Description" import KeywordDescription from "./components/keywords/Description/Description"
@@ -50,6 +51,7 @@ import {
isExpandable, isExpandable,
stringify, stringify,
stringifyConstraints, stringifyConstraints,
getDependentRequired,
} from "./fn" } from "./fn"
export const withJSONSchemaContext = (Component, overrides = {}) => { export const withJSONSchemaContext = (Component, overrides = {}) => {
@@ -86,6 +88,7 @@ export const withJSONSchemaContext = (Component, overrides = {}) => {
KeywordEnum, KeywordEnum,
KeywordConst, KeywordConst,
KeywordConstraint, KeywordConstraint,
KeywordDependentRequired,
KeywordFormat, KeywordFormat,
KeywordTitle, KeywordTitle,
KeywordDescription, KeywordDescription,
@@ -116,6 +119,7 @@ export const withJSONSchemaContext = (Component, overrides = {}) => {
isExpandable, isExpandable,
stringify, stringify,
stringifyConstraints, stringifyConstraints,
getDependentRequired,
...overrides.fn, ...overrides.fn,
}, },
} }

View File

@@ -32,6 +32,7 @@ import KeywordType from "./components/keywords/Type/Type"
import KeywordEnum from "./components/keywords/Enum/Enum" import KeywordEnum from "./components/keywords/Enum/Enum"
import KeywordConst from "./components/keywords/Const" import KeywordConst from "./components/keywords/Const"
import KeywordConstraint from "./components/keywords/Constraint/Constraint" import KeywordConstraint from "./components/keywords/Constraint/Constraint"
import KeywordDependentRequired from "./components/keywords/DependentRequired/DependentRequired"
import KeywordFormat from "./components/keywords/Format/Format" import KeywordFormat from "./components/keywords/Format/Format"
import KeywordTitle from "./components/keywords/Title/Title" import KeywordTitle from "./components/keywords/Title/Title"
import KeywordDescription from "./components/keywords/Description/Description" import KeywordDescription from "./components/keywords/Description/Description"
@@ -74,6 +75,7 @@ const JSONSchema202012Plugin = () => ({
JSONSchema202012KeywordEnum: KeywordEnum, JSONSchema202012KeywordEnum: KeywordEnum,
JSONSchema202012KeywordConst: KeywordConst, JSONSchema202012KeywordConst: KeywordConst,
JSONSchema202012KeywordConstraint: KeywordConstraint, JSONSchema202012KeywordConstraint: KeywordConstraint,
JSONSchema202012KeywordDependentRequired: KeywordDependentRequired,
JSONSchema202012KeywordFormat: KeywordFormat, JSONSchema202012KeywordFormat: KeywordFormat,
JSONSchema202012KeywordTitle: KeywordTitle, JSONSchema202012KeywordTitle: KeywordTitle,
JSONSchema202012KeywordDescription: KeywordDescription, JSONSchema202012KeywordDescription: KeywordDescription,

View File

@@ -59,6 +59,9 @@ const ModelsWrapper = createOnlyOAS31ComponentWrapper(({ getSystem }) => {
const KeywordEnum = getComponent("JSONSchema202012KeywordEnum") const KeywordEnum = getComponent("JSONSchema202012KeywordEnum")
const KeywordConst = getComponent("JSONSchema202012KeywordConst") const KeywordConst = getComponent("JSONSchema202012KeywordConst")
const KeywordConstraint = getComponent("JSONSchema202012KeywordConstraint") const KeywordConstraint = getComponent("JSONSchema202012KeywordConstraint")
const KeywordDependentRequired = getComponent(
"JSONSchema202012KeywordDependentRequired"
)
const KeywordFormat = getComponent("JSONSchema202012KeywordFormat") const KeywordFormat = getComponent("JSONSchema202012KeywordFormat")
const KeywordTitle = getComponent("JSONSchema202012KeywordTitle") const KeywordTitle = getComponent("JSONSchema202012KeywordTitle")
const KeywordDescription = getComponent( const KeywordDescription = getComponent(
@@ -107,6 +110,7 @@ const ModelsWrapper = createOnlyOAS31ComponentWrapper(({ getSystem }) => {
KeywordEnum, KeywordEnum,
KeywordConst, KeywordConst,
KeywordConstraint, KeywordConstraint,
KeywordDependentRequired,
KeywordFormat, KeywordFormat,
KeywordTitle, KeywordTitle,
KeywordDescription, KeywordDescription,