Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ const Items = ({ schema }) => {
*/
if (!fn.hasKeyword(schema, "items")) return null

const itemsTitle = fn.getTitle(schema.items)
const name = (
<span className="json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary">
Items
{itemsTitle || "Items"}
</span>
)

Expand Down
24 changes: 3 additions & 21 deletions src/core/plugins/json-schema-5/components/model.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,7 @@ import PropTypes from "prop-types"
import { Map } from "immutable"

import RollingLoadSVG from "core/assets/rolling-load.svg"

const decodeRefName = uri => {
const unescaped = uri.replace(/~1/g, "/").replace(/~0/g, "~")

try {
return decodeURIComponent(unescaped)
} catch {
return unescaped
}
}
import { getModelName } from "core/utils/get-model-name"

export default class Model extends ImmutablePureComponent {
static propTypes = {
Expand All @@ -33,15 +24,6 @@ export default class Model extends ImmutablePureComponent {
includeWriteOnly: PropTypes.bool,
}

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

getRefSchema =( model )=> {
let { specSelectors } = this.props

Expand All @@ -60,7 +42,7 @@ export default class Model extends ImmutablePureComponent {

// If we weren't passed a `name` but have a resolved ref, grab the name from the ref
if (!name && $$ref) {
name = this.getModelName($$ref)
name = getModelName($$ref)
}

/*
Expand All @@ -71,7 +53,7 @@ export default class Model extends ImmutablePureComponent {
* - we had a circular ref inside the allOf keyword
*/
if ($ref) {
const refName = this.getModelName($ref)
const refName = getModelName($ref)
const refSchema = this.getRefSchema(refName)
if (Map.isMap(refSchema)) {
schema = refSchema.mergeDeep(schema)
Expand Down
8 changes: 7 additions & 1 deletion src/core/plugins/oas31/after-load.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import {
makeIsExpandable,
makeGetTitle,
getProperties,
} from "./json-schema-2020-12-extensions/fn"
import { wrapOAS31Fn } from "./fn"
Expand All @@ -16,8 +17,13 @@ function afterLoad({ fn, getSystem }) {
fn.jsonSchema202012.isExpandable,
getSystem
)
const getTitle = makeGetTitle(fn.jsonSchema202012.getTitle)

Object.assign(this.fn.jsonSchema202012, { isExpandable, getProperties })
Object.assign(this.fn.jsonSchema202012, {
isExpandable,
getProperties,
...(getTitle && { getTitle }),
})
}

// wraps schema generators from samples plugin and make them specific to OpenAPI 3.1 version
Expand Down
15 changes: 1 addition & 14 deletions src/core/plugins/oas31/components/model/model.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,7 @@ import React, { forwardRef, useCallback } from "react"
import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes"

const decodeRefName = (uri) => {
const unescaped = uri.replace(/~1/g, "/").replace(/~0/g, "~")
try {
return decodeURIComponent(unescaped)
} catch {
return unescaped
}
}
const getModelName = (uri) => {
if (typeof uri === "string" && uri.includes("#/components/schemas/")) {
return decodeRefName(uri.replace(/^.*#\/components\/schemas\//, ""))
}
return null
}
import { getModelName } from "../../../../utils/get-model-name"

const Model = forwardRef(
({ schema, getComponent, onToggle = () => {}, specPath }, ref) => {
Expand Down
14 changes: 14 additions & 0 deletions src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
/**
* @prettier
*/
import { getModelName } from "../../../utils/get-model-name"

export const makeGetTitle = (original) => {
Comment thread
adrianodpdiaz marked this conversation as resolved.
if (typeof original !== "function") {
return null
}

return (schema, options) => {
const title = original(schema, options)
if (title) return title
return getModelName(schema?.$$ref) || ""
}
}

export const makeIsExpandable = (original, getSystem) => {
const { fn } = getSystem()

Expand Down
1 change: 1 addition & 0 deletions src/core/plugins/oas31/wrap-components/model.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const ModelWrapper = createOnlyOAS31ComponentWrapper(
fn: {
getProperties: fn.jsonSchema202012.getProperties,
isExpandable: fn.jsonSchema202012.isExpandable,
getTitle: fn.jsonSchema202012.getTitle,
getSchemaKeywords: makeGetSchemaKeywords(
fn.jsonSchema202012.getSchemaKeywords
),
Expand Down
1 change: 1 addition & 0 deletions src/core/plugins/oas31/wrap-components/models.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const ModelsWrapper = createOnlyOAS31ComponentWrapper(({ getSystem }) => {
fn: {
getProperties: fn.jsonSchema202012.getProperties,
isExpandable: fn.jsonSchema202012.isExpandable,
getTitle: fn.jsonSchema202012.getTitle,
getSchemaKeywords: makeGetSchemaKeywords(
fn.jsonSchema202012.getSchemaKeywords
),
Expand Down
24 changes: 24 additions & 0 deletions src/core/utils/get-model-name.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @prettier
*/
export const decodeRefName = (uri) => {
const unescaped = uri.replace(/~1/g, "/").replace(/~0/g, "~")

try {
return decodeURIComponent(unescaped)
} catch {
return unescaped
}
}

export const getModelName = (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
}
23 changes: 23 additions & 0 deletions test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @prettier
*/
describe("OpenAPI 3.1 array items label", () => {
beforeEach(() => {
cy.visit("/?url=/documents/features/oas31-array-items-label.yaml")
cy.get(".models").click()
cy.get(".json-schema-2020-12").contains("UserList").click()
cy.get(".json-schema-2020-12-accordion").contains("users").click()
})

it("should display the referenced schema name instead of 'Items' for array items", () => {
cy.get(".json-schema-2020-12-keyword--items")
.find(".json-schema-2020-12-keyword__name--primary")
.should("have.text", "User")
})

it("should not display the generic 'Items' label when items is a $ref", () => {
cy.get(".json-schema-2020-12-keyword--items")
.find(".json-schema-2020-12-keyword__name--primary")
.should("not.have.text", "Items")
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
openapi: 3.1.0
info:
title: Test
version: "1.0"
paths:
/users:
get:
summary: Get users
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/UserList"
components:
schemas:
User:
type: object
description: A user of the system.
properties:
id:
type: string
name:
type: string
UserList:
type: object
description: A list of users.
properties:
results:
type: integer
users:
type: array
items:
$ref: "#/components/schemas/User"
12 changes: 5 additions & 7 deletions test/unit/core/helpers/get-model-name.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,18 @@
* @prettier
*/

import Model from "../../../../src/core/plugins/json-schema-5/components/model"
import { getModelName } from "../../../../src/core/utils/get-model-name"

describe("getModelName", () => {
const model = new Model()

it("should decode JSON Pointer and URI encoding for OpenAPI v3 refs", () => {
const actual = model.getModelName("#/components/schemas/a~1b%2Bc")
const actual = getModelName("#/components/schemas/a~1b%2Bc")
const expected = "a/b+c"

expect(actual).toStrictEqual(expected)
})

it("should decode JSON Pointer and URI encoding for Swagger v2 refs", () => {
const actual = model.getModelName(
const actual = getModelName(
"#/definitions/custom%3A%3Anamespace%3A%3APerson"
)
const expected = "custom::namespace::Person"
Expand All @@ -24,14 +22,14 @@ describe("getModelName", () => {
})

it("should decode multiple json-pointer values", () => {
const actual = model.getModelName("#/components/schemas/~1~1~0~0")
const actual = getModelName("#/components/schemas/~1~1~0~0")
const expected = "//~~"

expect(actual).toStrictEqual(expected)
})

it("should support invalid URI encoding", () => {
const actual = model.getModelName("#/components/schemas/%25%d")
const actual = getModelName("#/components/schemas/%25%d")
const expected = "%25%d"

expect(actual).toStrictEqual(expected)
Expand Down
67 changes: 67 additions & 0 deletions test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @prettier
*/
import { makeGetTitle } from "core/plugins/oas31/json-schema-2020-12-extensions/fn"

const baseGetTitle = (schema) => schema?.title ?? ""

describe("OAS31 - json-schema-2020-12-extensions - makeGetTitle", () => {
const getTitle = makeGetTitle(baseGetTitle)

it("should return null when original is not a function", () => {
expect(makeGetTitle(null)).toBeNull()
expect(makeGetTitle(undefined)).toBeNull()
expect(makeGetTitle("string")).toBeNull()
})

describe("when schema has a title", () => {
it("should return the title from the original getTitle", () => {
expect(getTitle({ title: "MyTitle" })).toBe("MyTitle")
})

it("should prefer title over $$ref", () => {
expect(
getTitle({
title: "MyTitle",
$$ref: "#/components/schemas/Foo",
})
).toBe("MyTitle")
})
})

describe("when schema has no title but has $$ref", () => {
it("should extract the schema name from a full URL $$ref", () => {
expect(
getTitle({
$$ref: "http://localhost:3200/swagger.json#/components/schemas/Foo",
})
).toBe("Foo")
})

it("should extract the schema name from a relative $$ref", () => {
expect(getTitle({ $$ref: "swagger.json#/components/schemas/Bar" })).toBe(
"Bar"
)
})

it("should extract the schema name from a fragment-only $$ref", () => {
expect(getTitle({ $$ref: "#/components/schemas/Foo" })).toBe("Foo")
})
})

describe("when schema has no title but has $$ref pointing to #/definitions/", () => {
it("should extract the schema name from a definitions $$ref", () => {
expect(getTitle({ $$ref: "#/definitions/Bar" })).toBe("Bar")
})
})

describe("when schema has no title and $$ref does not match known paths", () => {
it("should return empty string for an unrecognized $$ref", () => {
expect(getTitle({ $$ref: "#/other/path/Baz" })).toBe("")
})

it("should return empty string for null schema", () => {
expect(getTitle(null)).toBe("")
})
})
})