diff --git a/src/core/plugins/json-schema-2020-12/components/keywords/Items.jsx b/src/core/plugins/json-schema-2020-12/components/keywords/Items.jsx index 95f6523459a..a5b575528b5 100644 --- a/src/core/plugins/json-schema-2020-12/components/keywords/Items.jsx +++ b/src/core/plugins/json-schema-2020-12/components/keywords/Items.jsx @@ -15,9 +15,10 @@ const Items = ({ schema }) => { */ if (!fn.hasKeyword(schema, "items")) return null + const itemsTitle = fn.getTitle(schema.items) const name = ( - Items + {itemsTitle || "Items"} ) diff --git a/src/core/plugins/json-schema-5/components/model.jsx b/src/core/plugins/json-schema-5/components/model.jsx index 1fb70d386bf..690d1b47d68 100644 --- a/src/core/plugins/json-schema-5/components/model.jsx +++ b/src/core/plugins/json-schema-5/components/model.jsx @@ -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 = { @@ -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 @@ -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) } /* @@ -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) diff --git a/src/core/plugins/oas31/after-load.js b/src/core/plugins/oas31/after-load.js index 7e6be340760..77fc44062ae 100644 --- a/src/core/plugins/oas31/after-load.js +++ b/src/core/plugins/oas31/after-load.js @@ -3,6 +3,7 @@ */ import { makeIsExpandable, + makeGetTitle, getProperties, } from "./json-schema-2020-12-extensions/fn" import { wrapOAS31Fn } from "./fn" @@ -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 diff --git a/src/core/plugins/oas31/components/model/model.jsx b/src/core/plugins/oas31/components/model/model.jsx index 33ae7dbb4ce..d3a75488fc3 100644 --- a/src/core/plugins/oas31/components/model/model.jsx +++ b/src/core/plugins/oas31/components/model/model.jsx @@ -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) => { diff --git a/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js b/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js index bb158801ecd..0d62df1db25 100644 --- a/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js +++ b/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js @@ -1,6 +1,20 @@ /** * @prettier */ +import { getModelName } from "../../../utils/get-model-name" + +export const makeGetTitle = (original) => { + 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() diff --git a/src/core/plugins/oas31/wrap-components/model.jsx b/src/core/plugins/oas31/wrap-components/model.jsx index 3a7e30ae77d..c5336ed0e87 100644 --- a/src/core/plugins/oas31/wrap-components/model.jsx +++ b/src/core/plugins/oas31/wrap-components/model.jsx @@ -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 ), diff --git a/src/core/plugins/oas31/wrap-components/models.jsx b/src/core/plugins/oas31/wrap-components/models.jsx index 0c1fd763885..3a00921477b 100644 --- a/src/core/plugins/oas31/wrap-components/models.jsx +++ b/src/core/plugins/oas31/wrap-components/models.jsx @@ -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 ), diff --git a/src/core/utils/get-model-name.js b/src/core/utils/get-model-name.js new file mode 100644 index 00000000000..450d5fd5dae --- /dev/null +++ b/src/core/utils/get-model-name.js @@ -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 +} diff --git a/test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js b/test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js new file mode 100644 index 00000000000..97dad2c0bc4 --- /dev/null +++ b/test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js @@ -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") + }) +}) diff --git a/test/e2e-cypress/static/documents/features/oas31-array-items-label.yaml b/test/e2e-cypress/static/documents/features/oas31-array-items-label.yaml new file mode 100644 index 00000000000..5608fdb17d2 --- /dev/null +++ b/test/e2e-cypress/static/documents/features/oas31-array-items-label.yaml @@ -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" diff --git a/test/unit/core/helpers/get-model-name.js b/test/unit/core/helpers/get-model-name.js index 63407055702..555ac300ad6 100644 --- a/test/unit/core/helpers/get-model-name.js +++ b/test/unit/core/helpers/get-model-name.js @@ -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" @@ -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) diff --git a/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js b/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js new file mode 100644 index 00000000000..b5433264d09 --- /dev/null +++ b/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js @@ -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("") + }) + }) +})