Skip to content

Commit 7870ef8

Browse files
committed
feat: support discriminator dropdown for allof
1 parent 0f5c0b3 commit 7870ef8

File tree

11 files changed

+176
-15
lines changed

11 files changed

+176
-15
lines changed

src/core/components/parameter-row.jsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export default class ParameterRow extends Component {
191191
}
192192

193193
render() {
194-
let {param, rawParam, getComponent, getConfigs, isExecute, fn, onChangeConsumes, specSelectors, pathMethod, specPath, oas3Selectors} = this.props
194+
let {param, rawParam, getComponent, getConfigs, isExecute, fn, onChangeConsumes, specSelectors, specActions, pathMethod, specPath, oas3Selectors} = this.props
195195

196196
let isOAS3 = specSelectors.isOAS3()
197197

@@ -357,6 +357,7 @@ export default class ParameterRow extends Component {
357357
getConfigs={ getConfigs }
358358
isExecute={ isExecute }
359359
specSelectors={ specSelectors }
360+
specActions={ specActions }
360361
schema={ schema }
361362
example={ bodyParam }
362363
includeWriteOnly={ true }/>

src/core/components/parameters/parameters.jsx

+1
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ export default class Parameters extends Component {
236236
setRetainRequestBodyValueFlag={retainRequestBodyValueFlagForOperation}
237237
userHasEditedBody={oas3Selectors.hasUserEditedBody(...pathMethod)}
238238
specPath={specPath.slice(0, -1).push("requestBody")}
239+
specActions={specActions}
239240
requestBody={requestBody}
240241
requestBodyValue={oas3Selectors.requestBodyValue(...pathMethod)}
241242
requestBodyInclusionSetting={oas3Selectors.requestBodyInclusionSetting(...pathMethod)}

src/core/components/response.jsx

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export default class Response extends React.Component {
3838
getComponent: PropTypes.func.isRequired,
3939
getConfigs: PropTypes.func.isRequired,
4040
specSelectors: PropTypes.object.isRequired,
41+
specActions: PropTypes.object.isRequired,
4142
oas3Actions: PropTypes.object.isRequired,
4243
specPath: ImPropTypes.list.isRequired,
4344
fn: PropTypes.object.isRequired,
@@ -84,6 +85,7 @@ export default class Response extends React.Component {
8485
getComponent,
8586
getConfigs,
8687
specSelectors,
88+
specActions,
8789
contentType,
8890
controlsAcceptHeader,
8991
oas3Actions,
@@ -237,6 +239,7 @@ export default class Response extends React.Component {
237239
getComponent={ getComponent }
238240
getConfigs={ getConfigs }
239241
specSelectors={ specSelectors }
242+
specActions={ specActions }
240243
schema={ fromJSOrdered(schema) }
241244
example={ example }
242245
includeReadOnly={ true }/>

src/core/components/responses.jsx

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export default class Responses extends React.Component {
6565
getComponent,
6666
getConfigs,
6767
specSelectors,
68+
specActions,
6869
fn,
6970
producesValue,
7071
displayRequestDuration,
@@ -145,6 +146,7 @@ export default class Responses extends React.Component {
145146
code={ code }
146147
response={ response }
147148
specSelectors={ specSelectors }
149+
specActions={ specActions }
148150
controlsAcceptHeader={response === acceptControllingResponse}
149151
onContentTypeChange={this.onResponseContentTypeChange}
150152
contentType={ producesValue }

src/core/plugins/json-schema-5/components/model-example.jsx

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const ModelExample = ({
4747
getComponent,
4848
getConfigs,
4949
specSelectors,
50+
specActions,
5051
}) => {
5152
const { defaultModelRendering, defaultModelExpandDepth } = getConfigs()
5253
const ModelWrapper = getComponent("ModelWrapper")
@@ -132,6 +133,7 @@ const ModelExample = ({
132133
getComponent={getComponent}
133134
getConfigs={getConfigs}
134135
specSelectors={specSelectors}
136+
specActions={specActions}
135137
expandDepth={defaultModelExpandDepth}
136138
specPath={specPath}
137139
includeReadOnly={includeReadOnly}
@@ -147,6 +149,7 @@ ModelExample.propTypes = {
147149
getComponent: PropTypes.func.isRequired,
148150
specSelectors: PropTypes.shape({ isOAS3: PropTypes.func.isRequired })
149151
.isRequired,
152+
specActions: PropTypes.object.isRequired,
150153
schema: PropTypes.object.isRequired,
151154
example: PropTypes.any.isRequired,
152155
isExecute: PropTypes.bool,
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import React, { Component, } from "react"
1+
import React, { Component } from "react"
22
import PropTypes from "prop-types"
33
import ImPropTypes from "react-immutable-proptypes"
44

55
export default class ModelWrapper extends Component {
6-
76
static propTypes = {
87
schema: PropTypes.object.isRequired,
98
name: PropTypes.string,
@@ -13,32 +12,147 @@ export default class ModelWrapper extends Component {
1312
getComponent: PropTypes.func.isRequired,
1413
getConfigs: PropTypes.func.isRequired,
1514
specSelectors: PropTypes.object.isRequired,
15+
specActions: PropTypes.object.isRequired,
1616
expandDepth: PropTypes.number,
1717
layoutActions: PropTypes.object,
1818
layoutSelectors: PropTypes.object.isRequired,
1919
includeReadOnly: PropTypes.bool,
2020
includeWriteOnly: PropTypes.bool,
2121
}
2222

23-
onToggle = (name,isShown) => {
23+
constructor(props) {
24+
super(props)
25+
this.state = {
26+
selectedSchema: null
27+
}
28+
}
29+
30+
onToggle = (name, isShown) => {
2431
// If this prop is present, we'll have deepLinking for it
2532
if(this.props.layoutActions) {
2633
this.props.layoutActions.show(this.props.fullPath, isShown)
2734
}
2835
}
2936

30-
render(){
31-
let { getComponent, getConfigs } = this.props
37+
onSchemaSelect = (e) => {
38+
const selectedSchema = e.target.value
39+
const schemaPath = ["components", "schemas", selectedSchema]
40+
41+
const isResolved = this.props.specSelectors.specResolvedSubtree(schemaPath) != null
42+
if (!isResolved) {
43+
this.props.specActions.requestResolvedSubtree(schemaPath)
44+
}
45+
46+
this.setState({ selectedSchema })
47+
}
48+
49+
decodeRefName = (uri) => {
50+
const unescaped = uri.replace(/~1/g, "/").replace(/~0/g, "~")
51+
try {
52+
return decodeURIComponent(unescaped)
53+
} catch {
54+
return unescaped
55+
}
56+
}
57+
58+
getModelName = (uri) => {
59+
if (typeof uri === "string" && uri.includes("#/components/schemas/")) {
60+
return this.decodeRefName(uri.replace(/^.*#\/components\/schemas\//, ""))
61+
}
62+
return null
63+
}
64+
65+
/**
66+
* Builds a Map of schema options combining explicit discriminator mappings and implicit mappings.
67+
*
68+
* @returns {Map<string, string[]>} A Map where:
69+
* - key: the schema name (e.g., "Cat", "Dog")
70+
* - value: array of discriminator values that map to this schema
71+
*
72+
* Examples:
73+
* 1. Explicit mapping only:
74+
* { "Cat": ["kitty", "kitten"], "Dog": ["puppy"] }
75+
*
76+
* 2. Implicit mapping only:
77+
* { "Cat": ["Cat"], "Dog": ["Dog"] }
78+
*
79+
* 3. Mixed mapping:
80+
* { "Cat": ["kitty", "kitten"], "Dog": ["Dog"] }
81+
* where "Cat" has explicit mappings but "Dog" uses implicit
82+
*/
83+
buildSchemaOptions = (name, discriminator, schemaMap) => {
84+
const options = new Map()
85+
const mapping = discriminator && discriminator.get("mapping")
86+
87+
// First add any explicit mappings
88+
if (mapping && mapping.size > 0) {
89+
mapping.forEach((schemaRef, key) => {
90+
const schemaName = this.getModelName(schemaRef)
91+
if (schemaName) {
92+
const existing = options.get(schemaName) || []
93+
options.set(schemaName, [...existing, key])
94+
}
95+
})
96+
}
97+
98+
// Then add implicit mappings for any schemas not already mapped
99+
const childSchemas = schemaMap[name] || []
100+
childSchemas.forEach(childName => {
101+
if (!options.has(childName)) {
102+
// No explicit mapping for this schema, use implicit
103+
options.set(childName, [childName])
104+
}
105+
})
106+
107+
return options
108+
}
109+
110+
render() {
111+
let { getComponent, getConfigs, schema, specSelectors } = this.props
32112
const Model = getComponent("Model")
33113

34114
let expanded
35115
if(this.props.layoutSelectors) {
36-
// If this is prop is present, we'll have deepLinking for it
37116
expanded = this.props.layoutSelectors.isShown(this.props.fullPath)
38117
}
39118

40-
return <div className="model-box">
41-
<Model { ...this.props } getConfigs={ getConfigs } expanded={expanded} depth={ 1 } onToggle={ this.onToggle } expandDepth={ this.props.expandDepth || 0 }/>
42-
</div>
119+
const name = this.getModelName(schema.get("$$ref"))
120+
const schemaMap = specSelectors.getParentToChildMap()
121+
const discriminator = schema.get("discriminator")
122+
123+
const options = this.buildSchemaOptions(name, discriminator, schemaMap)
124+
const showDropdown = !!discriminator && options.size > 0
125+
126+
// Use selected schema or original base schema
127+
const effectiveSchema = this.state.selectedSchema
128+
? specSelectors.findDefinition(this.state.selectedSchema)
129+
: schema
130+
131+
return (
132+
<div className="model-box">
133+
{showDropdown && (
134+
<div className="model-box-control">
135+
<select onChange={this.onSchemaSelect} value={this.state.selectedSchema || ""}>
136+
<option value="">Base: {name}</option>
137+
{Array.from(options.entries()).map(([schemaName, keys]) => (
138+
<option key={schemaName} value={schemaName}>
139+
{keys.length > 1 ? `${keys.join(" | ")} (${schemaName})` : schemaName}
140+
</option>
141+
))}
142+
</select>
143+
</div>
144+
)}
145+
<Model
146+
{ ...this.props }
147+
name={this.state.selectedSchema}
148+
schema={effectiveSchema}
149+
getConfigs={getConfigs}
150+
expanded={expanded}
151+
depth={1}
152+
onToggle={this.onToggle}
153+
expandDepth={this.props.expandDepth || 0}
154+
/>
155+
</div>
156+
)
43157
}
44158
}

src/core/plugins/json-schema-5/components/models.jsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default class Models extends Component {
4343
}
4444

4545
render(){
46-
let { specSelectors, getComponent, layoutSelectors, layoutActions, getConfigs } = this.props
46+
let { specSelectors, specActions, getComponent, layoutSelectors, layoutActions, getConfigs } = this.props
4747
let definitions = specSelectors.definitions()
4848
let { docExpansion, defaultModelsExpandDepth } = getConfigs()
4949
if (!definitions.size || defaultModelsExpandDepth < 0) return null
@@ -100,6 +100,7 @@ export default class Models extends Component {
100100
specPath={specPath}
101101
getComponent={ getComponent }
102102
specSelectors={ specSelectors }
103+
specActions={ specActions }
103104
getConfigs = {getConfigs}
104105
layoutSelectors = {layoutSelectors}
105106
layoutActions = {layoutActions}

src/core/plugins/oas3/components/request-body.jsx

+3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const RequestBody = ({
4141
getComponent,
4242
getConfigs,
4343
specSelectors,
44+
specActions,
4445
fn,
4546
contentType,
4647
isExecute,
@@ -284,6 +285,7 @@ const RequestBody = ({
284285
getComponent={ getComponent }
285286
getConfigs={ getConfigs }
286287
specSelectors={ specSelectors }
288+
specActions={ specActions }
287289
expandDepth={1}
288290
isExecute={isExecute}
289291
schema={mediaTypeValue.get("schema")}
@@ -319,6 +321,7 @@ RequestBody.propTypes = {
319321
getConfigs: PropTypes.func.isRequired,
320322
fn: PropTypes.object.isRequired,
321323
specSelectors: PropTypes.object.isRequired,
324+
specActions: PropTypes.object.isRequired,
322325
contentType: PropTypes.string,
323326
isExecute: PropTypes.bool.isRequired,
324327
onChange: PropTypes.func.isRequired,

src/core/plugins/oas31/components/model/model.jsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ const getModelName = (uri) => {
1919
}
2020
return null
2121
}
22-
2322
const Model = forwardRef(
24-
({ schema, getComponent, onToggle = () => {} }, ref) => {
23+
({ schema, name: nameFromProp, getComponent, specSelectors, onToggle = () => {} }, ref) => {
2524
const JSONSchema202012 = getComponent("JSONSchema202012")
26-
const name = getModelName(schema.get("$$ref"))
25+
const name = nameFromProp || getModelName(schema.get("$$ref"))
2726

2827
const handleExpand = useCallback(
2928
(e, expanded) => {
@@ -36,6 +35,7 @@ const Model = forwardRef(
3635
<JSONSchema202012
3736
name={name}
3837
schema={schema.toJS()}
38+
specSelectors={specSelectors}
3939
ref={ref}
4040
onExpand={handleExpand}
4141
/>
@@ -45,6 +45,8 @@ const Model = forwardRef(
4545

4646
Model.propTypes = {
4747
schema: ImPropTypes.map.isRequired,
48+
name: PropTypes.string,
49+
specSelectors: PropTypes.func.isRequired,
4850
getComponent: PropTypes.func.isRequired,
4951
onToggle: PropTypes.func,
5052
}

src/core/plugins/spec/selectors.js

+30
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,36 @@ export const specResolved = createSelector(
4848
spec => spec.get("resolved", Map())
4949
)
5050

51+
52+
53+
// Get parent-child schema map
54+
export const getParentToChildMap = createSelector(
55+
specJS,
56+
spec => {
57+
const schemaMap = {}
58+
const schemas = spec?.components?.schemas;
59+
if (!!schemas) {
60+
Object.entries(schemas).forEach(([schemaName, schema]) => {
61+
if (schema.allOf) {
62+
schema.allOf.forEach(item => {
63+
if (item.$ref) {
64+
// Extract parent schema name from $ref
65+
const parentName = item.$ref.split("/").pop()
66+
// Add current schema as child of parent
67+
if (!schemaMap[parentName]) {
68+
schemaMap[parentName] = []
69+
}
70+
schemaMap[parentName].push(schemaName)
71+
}
72+
})
73+
}
74+
})
75+
}
76+
return schemaMap
77+
}
78+
)
79+
80+
5181
export const specResolvedSubtree = (state, path) => {
5282
return state.getIn(["resolvedSubtrees", ...path], undefined)
5383
}

src/style/_models.scss

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
{
33
font-size: 12px;
44
font-weight: 300;
5-
5+
display: block;
6+
67
@include text_code();
78

89
.deprecated

0 commit comments

Comments
 (0)