Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/usage/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ Parameter name | Docker variable | Description
'none' (expands nothing).
</td>
</tr>
<tr>
<td><a name="user-content-resolvesubtreeonexpand"></a><code>resolveSubtreeOnExpand</code>
</td>
<td><em>Unavailable</em></td>
<td><code>Boolean=true</code>. Controls whether an operation's or model's
subtree is resolved (dereferencing <code>$ref</code>s and generating
samples) when it is expanded. The default is <code>true</code>. Set to
<code>false</code> to skip this work and render directly from the
unresolved specification, which can keep expansion responsive on very
large or deeply nested schemas (for example documents with many nested
<code>oneOf</code> compositions). When disabled, referenced schemas are
shown by name rather than fully inlined.
</td>
</tr>
<tr>
<td><a name="user-content-filter"></a><code>filter</code></td>
<td><code>FILTER</code></td>
Expand Down
1 change: 1 addition & 0 deletions src/core/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const defaultOptions = Object.freeze({
defaultModelRendering: "example",
defaultModelExpandDepth: 1,
defaultModelsExpandDepth: 1,
resolveSubtreeOnExpand: true,
showExtensions: false,
showCommonExtensions: false,
withCredentials: false,
Expand Down
4 changes: 4 additions & 0 deletions src/core/config/type-cast/mappings.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ const mappings = {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.requestSnippetsEnabled,
},
resolveSubtreeOnExpand: {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.resolveSubtreeOnExpand,
},
responseInterceptor: {
typeCaster: functionTypeCaster,
defaultValue: defaultOptions.responseInterceptor,
Expand Down
44 changes: 37 additions & 7 deletions src/core/containers/OperationContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default class OperationContainer extends PureComponent {
const { isShown } = this.props
const resolvedSubtree = this.getResolvedSubtree()

if(isShown && resolvedSubtree === undefined) {
if (this.shouldResolveOperationSubtree() && isShown && resolvedSubtree === undefined) {
this.requestResolvedSubtree()
}
}
Expand All @@ -98,21 +98,34 @@ export default class OperationContainer extends PureComponent {
this.setState({ executeInProgress: false })
}

if (isShown && resolvedSubtree === undefined) {
if (
this.shouldResolveOperationSubtree() &&
isShown &&
resolvedSubtree === undefined
) {
this.requestResolvedSubtree()
}
}

toggleShown =() => {
let { layoutActions, tag, operationId, isShown } = this.props
const resolvedSubtree = this.getResolvedSubtree()
if(!isShown && resolvedSubtree === undefined) {
if (
this.shouldResolveOperationSubtree() &&
!isShown &&
resolvedSubtree === undefined
) {
// transitioning from collapsed to expanded
this.requestResolvedSubtree()
}
layoutActions.show(["operations", tag, operationId], !isShown)
}

shouldResolveOperationSubtree = () => {
const { resolveSubtreeOnExpand = true } = this.props.getConfigs()
return resolveSubtreeOnExpand
}

onCancelClick=() => {
this.setState({tryItOutEnabled: !this.state.tryItOutEnabled})
}
Expand Down Expand Up @@ -214,19 +227,36 @@ export default class OperationContainer extends PureComponent {

const Operation = getComponent( "operation" )

const resolvedSubtree = this.getResolvedSubtree() || Map()
const resolvedSubtree = this.getResolvedSubtree()
const unresolvedOperation = unresolvedOp.get("operation") || Map()
const hasResolvedSubtree = Map.isMap(resolvedSubtree) && resolvedSubtree.size
// When subtree resolution is enabled we wait for the resolved subtree
// before rendering, otherwise initializing param values from the
// unresolved ($ref) schema would prevent them from refreshing once the
// resolved schema arrives. When resolution is disabled, fall back to the
// unresolved operation so the operation still renders.
const operation = hasResolvedSubtree
? resolvedSubtree
: this.shouldResolveOperationSubtree()
? Map()
: unresolvedOperation

const operationProps = fromJS({
op: resolvedSubtree,
op: operation,
tag,
path,
summary: unresolvedOp.getIn(["operation", "summary"]) || "",
deprecated: resolvedSubtree.get("deprecated") || unresolvedOp.getIn(["operation", "deprecated"]) || false,
deprecated:
operation.get("deprecated") ||
unresolvedOp.getIn(["operation", "deprecated"]) ||
false,
method,
security,
isAuthorized,
operationId,
originalOperationId: resolvedSubtree.getIn(["operation", "__originalOperationId"]),
originalOperationId:
operation.get("__originalOperationId") ||
unresolvedOp.getIn(["operation", "__originalOperationId"]),
showSummary,
isShown,
jumpToKey,
Expand Down
3 changes: 3 additions & 0 deletions src/core/plugins/json-schema-5/components/model.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export default class Model extends ImmutablePureComponent {
if (!name && $$ref) {
name = this.getModelName($$ref)
}
if (!name && $ref) {
name = this.getModelName($ref)
}

/*
* If we have an unresolved ref, get the schema and name from the ref.
Expand Down
14 changes: 5 additions & 9 deletions src/core/plugins/json-schema-5/components/models.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ export default class Models extends Component {
}

handleToggle = (name, isExpanded) => {
const { layoutActions } = this.props
const { layoutActions, getConfigs } = this.props
const { resolveSubtreeOnExpand = true } = getConfigs()
layoutActions.show([...this.getSchemaBasePath(), name], isExpanded)
if(isExpanded) {
if (isExpanded && resolveSubtreeOnExpand) {
this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), name])
}
}
Expand Down Expand Up @@ -82,18 +83,13 @@ export default class Models extends Component {
const schemaValue = specSelectors.specResolvedSubtree(fullPath)
const rawSchemaValue = specSelectors.specJson().getIn(fullPath)

const schema = Map.isMap(schemaValue) ? schemaValue : Im.Map()
const rawSchema = Map.isMap(rawSchemaValue) ? rawSchemaValue : Im.Map()
const schema =
Map.isMap(schemaValue) && schemaValue.size ? schemaValue : rawSchema

const displayName = schema.get("title") || rawSchema.get("title") || name
const isShown = layoutSelectors.isShown(fullPath, false)

if( isShown && (schema.size === 0 && rawSchema.size > 0) ) {
// Firing an action in a container render is not great,
// but it works for now.
this.props.specActions.requestResolvedSubtree(fullPath)
}

const content = <ModelWrapper name={ name }
expandDepth={ defaultModelsExpandDepth }
schema={ schema || Im.Map() }
Expand Down
68 changes: 68 additions & 0 deletions src/core/plugins/json-schema-5/components/object-model.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,65 @@ const braceOpen = "{"
const braceClose = "}"
const propClass = "property"

const decodeRefName = (uri) => {
if (typeof uri !== "string") {
return null
}

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

const getRefDisplayName = (ref) => {
if (typeof ref !== "string") {
return null
}
if (ref.indexOf("#/definitions/") !== -1) {
return decodeRefName(ref.replace(/^.*#\/definitions\//, ""))
}
if (ref.indexOf("#/components/schemas/") !== -1) {
return decodeRefName(ref.replace(/^.*#\/components\/schemas\//, ""))
}
return null
}

const inferSchemaDisplayName = (schema, level = 0) => {
if (!schema || typeof schema.get !== "function" || level > 2) {
return null
}

const title = schema.get("title")
if (typeof title === "string" && title) {
return title
}

const refDisplayName = getRefDisplayName(
schema.get("$$ref") || schema.get("$ref")
)
if (refDisplayName) {
return refDisplayName
}

const compositions = ["allOf", "oneOf", "anyOf"]
for (let i = 0; i < compositions.length; i++) {
const composed = schema.get(compositions[i])
if (List.isList(composed) && composed.size) {
for (let j = 0; j < composed.size; j++) {
const inferred = inferSchemaDisplayName(composed.get(j), level + 1)
if (inferred) {
return inferred
}
}
}
}

return null
}

export default class ObjectModel extends Component {
static propTypes = {
schema: PropTypes.object.isRequired,
Expand Down Expand Up @@ -237,6 +296,8 @@ export default class ObjectModel extends Component {
<td>{"allOf ->"}</td>
<td>
{allOf.map((schema, k) => {
const inferredDisplayName =
inferSchemaDisplayName(schema)
return (
<div key={k}>
<Model
Expand All @@ -246,6 +307,7 @@ export default class ObjectModel extends Component {
specPath={specPath.push("allOf", k)}
getConfigs={getConfigs}
schema={schema}
displayName={inferredDisplayName}
depth={depth + 1}
/>
</div>
Expand All @@ -259,6 +321,8 @@ export default class ObjectModel extends Component {
<td>{"anyOf ->"}</td>
<td>
{anyOf.map((schema, k) => {
const inferredDisplayName =
inferSchemaDisplayName(schema)
return (
<div key={k}>
<Model
Expand All @@ -268,6 +332,7 @@ export default class ObjectModel extends Component {
specPath={specPath.push("anyOf", k)}
getConfigs={getConfigs}
schema={schema}
displayName={inferredDisplayName}
depth={depth + 1}
/>
</div>
Expand All @@ -281,6 +346,8 @@ export default class ObjectModel extends Component {
<td>{"oneOf ->"}</td>
<td>
{oneOf.map((schema, k) => {
const inferredDisplayName =
inferSchemaDisplayName(schema)
return (
<div key={k}>
<Model
Expand All @@ -290,6 +357,7 @@ export default class ObjectModel extends Component {
specPath={specPath.push("oneOf", k)}
getConfigs={getConfigs}
schema={schema}
displayName={inferredDisplayName}
depth={depth + 1}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @prettier
*/
describe("resolveSubtreeOnExpand disabled", () => {
it("renders a schema model and its properties when expanded", () => {
cy.visit(
"/?url=/documents/features/models.swagger.yaml&resolveSubtreeOnExpand=false"
)
.get("#model-Pet .model-box .model-box-control")
.click()
.get("#model-Pet .model-box .model .inner-object")
.should("exist")

cy.get("#model-Pet").contains("tr.property-row td", "name").should("exist")
cy.get("#model-Pet")
.contains("tr.property-row td", "category")
.should("exist")
cy.get("#model-Pet").contains("Category").should("exist")
})
})
4 changes: 4 additions & 0 deletions test/unit/core/config/type-cast/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ describe("typeCast", () => {
tryItOutEnabled: "false",
withCredentials: "true",
filter: "false",
resolveSubtreeOnExpand: "true",
}

const expectedConfig = {
deepLinking: true,
tryItOutEnabled: false,
withCredentials: true,
filter: false,
resolveSubtreeOnExpand: true,
}

expect(typeCast(config)).toStrictEqual(expectedConfig)
Expand Down Expand Up @@ -87,6 +89,7 @@ describe("typeCast", () => {
maxDisplayedTags: "null",
defaultModelExpandDepth: {},
defaultModelsExpandDepth: false,
resolveSubtreeOnExpand: "invalid",
}

const expectedConfig = {
Expand All @@ -97,6 +100,7 @@ describe("typeCast", () => {
maxDisplayedTags: -1,
defaultModelExpandDepth: 1,
defaultModelsExpandDepth: 1,
resolveSubtreeOnExpand: true,
}

expect(typeCast(config)).toStrictEqual(expectedConfig)
Expand Down
38 changes: 38 additions & 0 deletions test/unit/core/plugins/json-schema-5/components/model.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from "react"
import { shallow } from "enzyme"
import { fromJS } from "immutable"
import Model from "core/plugins/json-schema-5/components/model"

describe("<Model />", function() {
const ObjectModel = () => null
const ArrayModel = () => null
const PrimitiveModel = () => null

const getComponent = (componentName) => {
return {
ObjectModel,
ArrayModel,
PrimitiveModel,
}[componentName]
}

const props = {
getComponent,
getConfigs: () => ({}),
specSelectors: {
isOAS3: () => true,
findDefinition: () => fromJS({ type: "object" }),
},
specPath: fromJS([]),
}

it("derives model name from unresolved $ref", function() {
const wrapper = shallow(
<Model {...props} schema={fromJS({ $ref: "#/components/schemas/Links" })} />
)

const renderedObjectModel = wrapper.find(ObjectModel)
expect(renderedObjectModel.length).toEqual(1)
expect(renderedObjectModel.first().prop("name")).toEqual("Links")
})
})
Loading