Skip to content

Commit

Permalink
feat: support discriminator dropdown for allof
Browse files Browse the repository at this point in the history
  • Loading branch information
YousefHaggy committed Nov 26, 2024
1 parent 0f5c0b3 commit faf4ac4
Show file tree
Hide file tree
Showing 13 changed files with 184 additions and 17 deletions.
3 changes: 2 additions & 1 deletion src/core/components/parameter-row.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default class ParameterRow extends Component {
}

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

let isOAS3 = specSelectors.isOAS3()

Expand Down Expand Up @@ -357,6 +357,7 @@ export default class ParameterRow extends Component {
getConfigs={ getConfigs }
isExecute={ isExecute }
specSelectors={ specSelectors }
specActions={ specActions }
schema={ schema }
example={ bodyParam }
includeWriteOnly={ true }/>
Expand Down
1 change: 1 addition & 0 deletions src/core/components/parameters/parameters.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export default class Parameters extends Component {
setRetainRequestBodyValueFlag={retainRequestBodyValueFlagForOperation}
userHasEditedBody={oas3Selectors.hasUserEditedBody(...pathMethod)}
specPath={specPath.slice(0, -1).push("requestBody")}
specActions={specActions}
requestBody={requestBody}
requestBodyValue={oas3Selectors.requestBodyValue(...pathMethod)}
requestBodyInclusionSetting={oas3Selectors.requestBodyInclusionSetting(...pathMethod)}
Expand Down
3 changes: 3 additions & 0 deletions src/core/components/response.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default class Response extends React.Component {
getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired,
oas3Actions: PropTypes.object.isRequired,
specPath: ImPropTypes.list.isRequired,
fn: PropTypes.object.isRequired,
Expand Down Expand Up @@ -84,6 +85,7 @@ export default class Response extends React.Component {
getComponent,
getConfigs,
specSelectors,
specActions,
contentType,
controlsAcceptHeader,
oas3Actions,
Expand Down Expand Up @@ -237,6 +239,7 @@ export default class Response extends React.Component {
getComponent={ getComponent }
getConfigs={ getConfigs }
specSelectors={ specSelectors }
specActions={ specActions }
schema={ fromJSOrdered(schema) }
example={ example }
includeReadOnly={ true }/>
Expand Down
2 changes: 2 additions & 0 deletions src/core/components/responses.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default class Responses extends React.Component {
getComponent,
getConfigs,
specSelectors,
specActions,
fn,
producesValue,
displayRequestDuration,
Expand Down Expand Up @@ -145,6 +146,7 @@ export default class Responses extends React.Component {
code={ code }
response={ response }
specSelectors={ specSelectors }
specActions={ specActions }
controlsAcceptHeader={response === acceptControllingResponse}
onContentTypeChange={this.onResponseContentTypeChange}
contentType={ producesValue }
Expand Down
3 changes: 3 additions & 0 deletions src/core/plugins/json-schema-5/components/model-example.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const ModelExample = ({
getComponent,
getConfigs,
specSelectors,
specActions,
}) => {
const { defaultModelRendering, defaultModelExpandDepth } = getConfigs()
const ModelWrapper = getComponent("ModelWrapper")
Expand Down Expand Up @@ -132,6 +133,7 @@ const ModelExample = ({
getComponent={getComponent}
getConfigs={getConfigs}
specSelectors={specSelectors}
specActions={specActions}
expandDepth={defaultModelExpandDepth}
specPath={specPath}
includeReadOnly={includeReadOnly}
Expand All @@ -147,6 +149,7 @@ ModelExample.propTypes = {
getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.shape({ isOAS3: PropTypes.func.isRequired })
.isRequired,
specActions: PropTypes.object.isRequired,
schema: PropTypes.object.isRequired,
example: PropTypes.any.isRequired,
isExecute: PropTypes.bool,
Expand Down
132 changes: 123 additions & 9 deletions src/core/plugins/json-schema-5/components/model-wrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React, { Component, } from "react"
import React, { Component } from "react"
import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes"

export default class ModelWrapper extends Component {

static propTypes = {
schema: PropTypes.object.isRequired,
name: PropTypes.string,
Expand All @@ -13,32 +12,147 @@ export default class ModelWrapper extends Component {
getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired,
expandDepth: PropTypes.number,
layoutActions: PropTypes.object,
layoutSelectors: PropTypes.object.isRequired,
includeReadOnly: PropTypes.bool,
includeWriteOnly: PropTypes.bool,
}

onToggle = (name,isShown) => {
constructor(props) {
super(props)
this.state = {
selectedSchema: null
}
}

onToggle = (name, isShown) => {
// If this prop is present, we'll have deepLinking for it
if(this.props.layoutActions) {
this.props.layoutActions.show(this.props.fullPath, isShown)
}
}

render(){
let { getComponent, getConfigs } = this.props
onSchemaSelect = (e) => {
const selectedSchema = e.target.value
const schemaPath = ["components", "schemas", selectedSchema]

const isResolved = this.props.specSelectors.specResolvedSubtree(schemaPath) != null
if (!isResolved) {
this.props.specActions.requestResolvedSubtree(schemaPath)
}

this.setState({ selectedSchema })
}

decodeRefName = (uri) => {
const unescaped = uri.replace(/~1/g, "/").replace(/~0/g, "~")
try {
return decodeURIComponent(unescaped)
} catch {
return unescaped
}
}

getModelName = (uri) => {
if (typeof uri === "string" && uri.includes("#/components/schemas/")) {
return this.decodeRefName(uri.replace(/^.*#\/components\/schemas\//, ""))
}
return null
}

/**
* Builds a Map of schema options combining explicit discriminator mappings and implicit mappings.
*
* @returns {Map<string, string[]>} A Map where:
* - key: the schema name (e.g., "Cat", "Dog")
* - value: array of discriminator values that map to this schema
*
* Examples:
* 1. Explicit mapping only:
* { "Cat": ["kitty", "kitten"], "Dog": ["puppy"] }
*
* 2. Implicit mapping only:
* { "Cat": ["Cat"], "Dog": ["Dog"] }
*
* 3. Mixed mapping:
* { "Cat": ["kitty", "kitten"], "Dog": ["Dog"] }
* where "Cat" has explicit mappings but "Dog" uses implicit
*/
buildSchemaOptions = (name, discriminator, schemaMap) => {
const options = new Map()
const mapping = discriminator && discriminator.get("mapping")

// First add any explicit mappings
if (mapping && mapping.size > 0) {
mapping.forEach((schemaRef, key) => {
const schemaName = this.getModelName(schemaRef)
if (schemaName) {
const existing = options.get(schemaName) || []
options.set(schemaName, [...existing, key])
}
})
}

// Then add implicit mappings for any schemas not already mapped
const childSchemas = schemaMap[name] || []
childSchemas.forEach(childName => {
if (!options.has(childName)) {
// No explicit mapping for this schema, use implicit
options.set(childName, [childName])
}
})

return options
}

render() {
let { getComponent, getConfigs, schema, specSelectors } = this.props
const Model = getComponent("Model")

let expanded
if(this.props.layoutSelectors) {
// If this is prop is present, we'll have deepLinking for it
expanded = this.props.layoutSelectors.isShown(this.props.fullPath)
}

return <div className="model-box">
<Model { ...this.props } getConfigs={ getConfigs } expanded={expanded} depth={ 1 } onToggle={ this.onToggle } expandDepth={ this.props.expandDepth || 0 }/>
</div>
const name = this.getModelName(schema.get("$$ref"))
const schemaMap = specSelectors.getParentToChildMap()
const discriminator = schema.get("discriminator")

const options = this.buildSchemaOptions(name, discriminator, schemaMap)
const showDropdown = !!discriminator && options.size > 0

// Use selected schema or original base schema
const effectiveSchema = this.state.selectedSchema
? specSelectors.findDefinition(this.state.selectedSchema)
: schema

return (
<div className="model-box">
{showDropdown && (
<div className="model-box-control">
<select onChange={this.onSchemaSelect} value={this.state.selectedSchema || ""}>
<option value="">Base: {name}</option>
{Array.from(options.entries()).map(([schemaName, keys]) => (
<option key={schemaName} value={schemaName}>
{keys.length > 1 ? `${keys.join(" | ")} (${schemaName})` : schemaName}
</option>
))}
</select>
</div>
)}
<Model
{ ...this.props }
name={this.state.selectedSchema}
schema={effectiveSchema}
getConfigs={getConfigs}
expanded={expanded}
depth={1}
onToggle={this.onToggle}
expandDepth={this.props.expandDepth || 0}
/>
</div>
)
}
}
3 changes: 2 additions & 1 deletion src/core/plugins/json-schema-5/components/models.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class Models extends Component {
}

render(){
let { specSelectors, getComponent, layoutSelectors, layoutActions, getConfigs } = this.props
let { specSelectors, specActions, getComponent, layoutSelectors, layoutActions, getConfigs } = this.props
let definitions = specSelectors.definitions()
let { docExpansion, defaultModelsExpandDepth } = getConfigs()
if (!definitions.size || defaultModelsExpandDepth < 0) return null
Expand Down Expand Up @@ -100,6 +100,7 @@ export default class Models extends Component {
specPath={specPath}
getComponent={ getComponent }
specSelectors={ specSelectors }
specActions={ specActions }
getConfigs = {getConfigs}
layoutSelectors = {layoutSelectors}
layoutActions = {layoutActions}
Expand Down
3 changes: 3 additions & 0 deletions src/core/plugins/oas3/components/request-body.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const RequestBody = ({
getComponent,
getConfigs,
specSelectors,
specActions,
fn,
contentType,
isExecute,
Expand Down Expand Up @@ -284,6 +285,7 @@ const RequestBody = ({
getComponent={ getComponent }
getConfigs={ getConfigs }
specSelectors={ specSelectors }
specActions={ specActions }
expandDepth={1}
isExecute={isExecute}
schema={mediaTypeValue.get("schema")}
Expand Down Expand Up @@ -319,6 +321,7 @@ RequestBody.propTypes = {
getConfigs: PropTypes.func.isRequired,
fn: PropTypes.object.isRequired,
specSelectors: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired,
contentType: PropTypes.string,
isExecute: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
Expand Down
8 changes: 5 additions & 3 deletions src/core/plugins/oas31/components/model/model.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ const getModelName = (uri) => {
}
return null
}

const Model = forwardRef(
({ schema, getComponent, onToggle = () => {} }, ref) => {
({ schema, name: nameFromProp, getComponent, specSelectors, onToggle = () => {} }, ref) => {
const JSONSchema202012 = getComponent("JSONSchema202012")
const name = getModelName(schema.get("$$ref"))
const name = nameFromProp || getModelName(schema.get("$$ref"))

const handleExpand = useCallback(
(e, expanded) => {
Expand All @@ -36,6 +35,7 @@ const Model = forwardRef(
<JSONSchema202012
name={name}
schema={schema.toJS()}
specSelectors={specSelectors}
ref={ref}
onExpand={handleExpand}
/>
Expand All @@ -45,6 +45,8 @@ const Model = forwardRef(

Model.propTypes = {
schema: ImPropTypes.map.isRequired,
name: PropTypes.string,
specSelectors: PropTypes.func.isRequired,
getComponent: PropTypes.func.isRequired,
onToggle: PropTypes.func,
}
Expand Down
7 changes: 6 additions & 1 deletion src/core/plugins/spec/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ export function updateUrl(url) {
}

export function updateJsonSpec(json) {
return {type: UPDATE_JSON, payload: json}
return {
type: UPDATE_JSON,
payload: {
json
}
}
}

export const parseToJson = (str) => ({specActions, specSelectors, errActions}) => {
Expand Down
3 changes: 2 additions & 1 deletion src/core/plugins/spec/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export default {
},

[UPDATE_JSON]: (state, action) => {
return state.set("json", fromJSOrdered(action.payload))
return state
.set("json", fromJSOrdered(action.payload.json))
},

[UPDATE_RESOLVED]: (state, action) => {
Expand Down
30 changes: 30 additions & 0 deletions src/core/plugins/spec/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,36 @@ export const specResolved = createSelector(
spec => spec.get("resolved", Map())
)



// Get parent-child schema map
export const getParentToChildMap = createSelector(
specJS,
spec => {
const schemaMap = {}
const schemas = spec?.components?.schemas;
if (!!schemas) {
Object.entries(schemas).forEach(([schemaName, schema]) => {
if (schema.allOf) {
schema.allOf.forEach(item => {
if (item.$ref) {
// Extract parent schema name from $ref
const parentName = item.$ref.split("/").pop()
// Add current schema as child of parent
if (!schemaMap[parentName]) {
schemaMap[parentName] = []
}
schemaMap[parentName].push(schemaName)
}
})
}
})
}
return schemaMap
}
)


export const specResolvedSubtree = (state, path) => {
return state.getIn(["resolvedSubtrees", ...path], undefined)
}
Expand Down
3 changes: 2 additions & 1 deletion src/style/_models.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
{
font-size: 12px;
font-weight: 300;

display: block;

@include text_code();

.deprecated
Expand Down

0 comments on commit faf4ac4

Please sign in to comment.