From 23379f6cd1b7fe20d37535d4c645ba4a202737e9 Mon Sep 17 00:00:00 2001 From: Oliwia Rogala Date: Thu, 10 Apr 2025 12:36:14 +0200 Subject: [PATCH 1/8] fix: align OpenAPI 3.x.y file uploads with specification Refs #9278 --- .../components/json-schema-components.jsx | 3 +- .../plugins/oas3/components/request-body.jsx | 26 +- src/core/plugins/oas3/fn.js | 22 ++ src/core/plugins/oas3/index.js | 2 + .../wrap-components/json-schema-string.jsx | 8 +- src/core/plugins/oas31/after-load.js | 8 + src/core/plugins/oas31/oas3-extensions/fn.js | 25 ++ .../oas3/request-body-upload-file.cy.js | 167 +++++++++++++ .../oas31-request-body-upload-file.cy.js | 235 ++++++++++++++++++ .../features/request-body-upload-file.cy.js | 132 ---------- ...aml => oas3-request-body-upload-file.yaml} | 78 +++--- .../oas31-request-body-upload-file.yaml | 143 +++++++++++ .../components/json-schema-form.jsx | 11 + 13 files changed, 661 insertions(+), 199 deletions(-) create mode 100644 src/core/plugins/oas3/fn.js create mode 100644 src/core/plugins/oas31/oas3-extensions/fn.js create mode 100644 test/e2e-cypress/e2e/features/plugins/oas3/request-body-upload-file.cy.js create mode 100644 test/e2e-cypress/e2e/features/plugins/oas31/oas31-request-body-upload-file.cy.js delete mode 100644 test/e2e-cypress/e2e/features/request-body-upload-file.cy.js rename test/e2e-cypress/static/documents/features/{request-body-upload-file.yaml => oas3-request-body-upload-file.yaml} (55%) create mode 100644 test/e2e-cypress/static/documents/features/oas31-request-body-upload-file.yaml diff --git a/src/core/plugins/json-schema-5/components/json-schema-components.jsx b/src/core/plugins/json-schema-5/components/json-schema-components.jsx index 1899ffcdf68..6575a067dcd 100644 --- a/src/core/plugins/json-schema-5/components/json-schema-components.jsx +++ b/src/core/plugins/json-schema-5/components/json-schema-components.jsx @@ -51,6 +51,7 @@ export class JsonSchemaForm extends Component { const format = schema && schema.get ? schema.get("format") : null const type = schema && schema.get ? schema.get("type") : null const foldedType = fn.jsonSchema202012.foldType(immutableToJS(type)) + const isFileUploadIntended = fn.isFileUploadIntended(schema) let getComponentSilently = (name) => getComponent(name, false, { failSilently: true }) let Comp = type ? format ? @@ -58,7 +59,7 @@ export class JsonSchemaForm extends Component { getComponentSilently(`JsonSchema_${type}`) : getComponent("JsonSchema_string") - if (List.isList(type) && (foldedType === "array" || foldedType === "object")) { + if (!isFileUploadIntended && List.isList(type) && (foldedType === "array" || foldedType === "object")) { Comp = getComponent("JsonSchema_object") } diff --git a/src/core/plugins/oas3/components/request-body.jsx b/src/core/plugins/oas3/components/request-body.jsx index 7c80040be50..2ab7ea55918 100644 --- a/src/core/plugins/oas3/components/request-body.jsx +++ b/src/core/plugins/oas3/components/request-body.jsx @@ -105,21 +105,10 @@ const RequestBody = ({ } requestBodyErrors = List.isList(requestBodyErrors) ? requestBodyErrors : List() - if(!mediaTypeValue.size) { - return null - } - - const isObjectContent = mediaTypeValue.getIn(["schema", "type"]) === "object" - const isBinaryFormat = mediaTypeValue.getIn(["schema", "format"]) === "binary" - const isBase64Format = mediaTypeValue.getIn(["schema", "format"]) === "base64" + const isFileUploadIntended = fn.isFileUploadIntended(mediaTypeValue?.get("schema"), contentType) if( - contentType === "application/octet-stream" - || contentType.indexOf("image/") === 0 - || contentType.indexOf("audio/") === 0 - || contentType.indexOf("video/") === 0 - || isBinaryFormat - || isBase64Format + isFileUploadIntended ) { const Input = getComponent("Input") @@ -132,6 +121,13 @@ const RequestBody = ({ return } + + if (!mediaTypeValue.size) { + return null + } + + const isObjectContent = mediaTypeValue.getIn(["schema", "type"]) === "object" + if ( isObjectContent && ( @@ -190,11 +186,11 @@ const RequestBody = ({ initialValue = JSON.parse(initialValue) } - const isFile = type === "string" && (format === "binary" || format === "base64") + const isFileUploadIntended = fn.isFileUploadIntended(schema) const jsonSchemaForm = { + if ( + typeof mediaType === "string" && + (mediaType.startsWith("application/octet-stream") || + mediaType.startsWith("image/") || + mediaType.startsWith("audio/") || + mediaType.startsWith("video/")) + ) { + return true + } + + if (!schema) return false + + const type = schema.get("type") + const format = schema.get("format") + + return type === "string" && (format === "binary" || format === "byte") +} diff --git a/src/core/plugins/oas3/index.js b/src/core/plugins/oas3/index.js index 7d63ce0e6a0..6423a27e1ef 100644 --- a/src/core/plugins/oas3/index.js +++ b/src/core/plugins/oas3/index.js @@ -9,6 +9,7 @@ import wrapComponents from "./wrap-components" import * as actions from "./actions" import * as selectors from "./selectors" import reducers from "./reducers" +import * as fn from "./fn" export default function () { return { @@ -28,5 +29,6 @@ export default function () { selectors: { ...selectors }, }, }, + fn, } } diff --git a/src/core/plugins/oas3/wrap-components/json-schema-string.jsx b/src/core/plugins/oas3/wrap-components/json-schema-string.jsx index 96534b0f20b..86469396e83 100644 --- a/src/core/plugins/oas3/wrap-components/json-schema-string.jsx +++ b/src/core/plugins/oas3/wrap-components/json-schema-string.jsx @@ -6,14 +6,14 @@ export default OAS3ComponentWrapFactory(({ Ori, ...props }) => { schema, getComponent, errors, - onChange + onChange, + fn } = props - const format = schema && schema.get ? schema.get("format") : null - const type = schema && schema.get ? schema.get("type") : null + const isFileUploadIntended = fn.isFileUploadIntended(schema) const Input = getComponent("Input") - if(type && type === "string" && (format && (format === "binary" || format === "base64"))) { + if (isFileUploadIntended) { return { + if ( + typeof mediaType === "string" && + (mediaType.startsWith("application/octet-stream") || + mediaType.startsWith("image/") || + mediaType.startsWith("audio/") || + mediaType.startsWith("video/")) + ) { + return true + } + + if (!schema) return null + + const type = immutableToJS(schema.get("type")) + const isTypeString = + type === "string" || (Array.isArray(type) && type.includes("string")) + const format = schema.get("format") + + return isTypeString && (format === "binary" || format === "byte") +} diff --git a/test/e2e-cypress/e2e/features/plugins/oas3/request-body-upload-file.cy.js b/test/e2e-cypress/e2e/features/plugins/oas3/request-body-upload-file.cy.js new file mode 100644 index 00000000000..45f7ff3880f --- /dev/null +++ b/test/e2e-cypress/e2e/features/plugins/oas3/request-body-upload-file.cy.js @@ -0,0 +1,167 @@ +/** + * @prettier + */ + +describe("OpenAPI 3.0 Request Body upload file button", () => { + beforeEach(() => { + cy.visit("/?url=/documents/features/oas3-request-body-upload-file.yaml") + }) + + describe("application/octet-stream", () => { + beforeEach(() => { + cy.get("#operations-default-uploadApplicationOctetStream").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for application/octet-stream media types." + ) + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("image/png", () => { + beforeEach(() => { + cy.get("#operations-default-uploadImagePng").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for image/png media types." + ) + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("audio/wav", () => { + beforeEach(() => { + cy.get("#operations-default-uploadAudioWav").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for audio/wav media types." + ) + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("video/mpeg", () => { + beforeEach(() => { + cy.get("#operations-default-uploadVideoMpeg").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for video/mpeg media types." + ) + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("schema type string and format binary", () => { + beforeEach(() => { + cy.get("#operations-default-uploadSchemaFormatBinary").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for application/x-custom media types." + ) + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("schema type string and format byte", () => { + beforeEach(() => { + cy.get("#operations-default-uploadSchemaFormatByte").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for application/x-custom media types." + ) + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("multipart/form-data object property with schema type string and format binary", () => { + beforeEach(() => { + cy.get("#operations-default-uploadPropertySchemaFormatBinary").click() + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("multipart/form-data object property with schema type string and format byte", () => { + beforeEach(() => { + cy.get("#operations-default-uploadPropertySchemaFormatByte").click() + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) +}) diff --git a/test/e2e-cypress/e2e/features/plugins/oas31/oas31-request-body-upload-file.cy.js b/test/e2e-cypress/e2e/features/plugins/oas31/oas31-request-body-upload-file.cy.js new file mode 100644 index 00000000000..50742ec26c4 --- /dev/null +++ b/test/e2e-cypress/e2e/features/plugins/oas31/oas31-request-body-upload-file.cy.js @@ -0,0 +1,235 @@ +/** + * @prettier + */ + +describe("OpenAPI 3.1 Request Body upload file button", () => { + beforeEach(() => { + cy.visit("/?url=/documents/features/oas31-request-body-upload-file.yaml") + }) + + describe("application/octet-stream", () => { + beforeEach(() => { + cy.get("#operations-default-uploadApplicationOctetStream").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for application/octet-stream media types." + ) + }) + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("image/png", () => { + beforeEach(() => { + cy.get("#operations-default-uploadImagePng").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for image/png media types." + ) + }) + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("audio/wav", () => { + beforeEach(() => { + cy.get("#operations-default-uploadAudioWav").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for audio/wav media types." + ) + }) + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("video/mpeg", () => { + beforeEach(() => { + cy.get("#operations-default-uploadVideoMpeg").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for video/mpeg media types." + ) + }) + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("schema type string and format binary", () => { + beforeEach(() => { + cy.get("#operations-default-uploadSchemaTypeFormatBinary").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for application/x-custom media types." + ) + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("schema type string and format byte", () => { + beforeEach(() => { + cy.get("#operations-default-uploadSchemaTypeFormatByte").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for application/x-custom media types." + ) + }) + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("schema union type includes string and format binary", () => { + beforeEach(() => { + cy.get("#operations-default-uploadSchemaUnionTypeFormatBinary").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for application/x-custom media types." + ) + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("schema union type includes string and format byte", () => { + beforeEach(() => { + cy.get("#operations-default-uploadSchemaUnionTypeFormatByte").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for application/x-custom media types." + ) + }) + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("multipart/form-data object property with schema type string and format binary", () => { + beforeEach(() => { + cy.get("#operations-default-uploadPropertySchemaFormatBinary").click() + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("multipart/form-data object property with schema type string and format byte", () => { + beforeEach(() => { + cy.get("#operations-default-uploadPropertySchemaFormatByte").click() + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("multipart/form-data object property with schema union type including string and format binary", () => { + beforeEach(() => { + cy.get( + "#operations-default-uploadPropertySchemaUnionTypeFormatBinary" + ).click() + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + + describe("multipart/form-data object property with schema union type including string and format byte", () => { + beforeEach(() => { + cy.get( + "#operations-default-uploadPropertySchemaUnionTypeFormatByte" + ).click() + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) +}) diff --git a/test/e2e-cypress/e2e/features/request-body-upload-file.cy.js b/test/e2e-cypress/e2e/features/request-body-upload-file.cy.js deleted file mode 100644 index bb0819f92dc..00000000000 --- a/test/e2e-cypress/e2e/features/request-body-upload-file.cy.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * @prettier - */ - -describe("OpenAPI 3.0 Request Body upload file button", () => { - describe("application/octet-stream", () => { - it("should display description with the correct content type", () => { - cy.visit("/?url=/documents/features/request-body-upload-file.yaml") - .get("#operations-default-uploadApplicationOctetStream") - .click() - .get(".opblock-section-request-body .opblock-description-wrapper i") - .should( - "have.text", - "Example values are not available for application/octet-stream media types." - ) - }) - it("should display a file upload button", () => { - cy.visit("/?url=/documents/features/request-body-upload-file.yaml") - .get("#operations-default-uploadApplicationOctetStream") - .click() - .get(".try-out__btn") - .click() - .get(".opblock-section-request-body .opblock-description-wrapper input") - .should("have.prop", "type", "file") - }) - }) - describe("image/png", () => { - it("should display description with the correct content type", () => { - cy.visit("/?url=/documents/features/request-body-upload-file.yaml") - .get("#operations-default-uploadImagePng") - .click() - .get(".opblock-section-request-body .opblock-description-wrapper i") - .should( - "have.text", - "Example values are not available for image/png media types." - ) - }) - it("should display a file upload button", () => { - cy.visit("/?url=/documents/features/request-body-upload-file.yaml") - .get("#operations-default-uploadApplicationOctetStream") - .click() - .get(".try-out__btn") - .click() - .get(".opblock-section-request-body .opblock-description-wrapper input") - .should("have.prop", "type", "file") - }) - }) - describe("audio/wav", () => { - it("should display description with the correct content type", () => { - cy.visit("/?url=/documents/features/request-body-upload-file.yaml") - .get("#operations-default-uploadAudioWav") - .click() - .get(".opblock-section-request-body .opblock-description-wrapper i") - .should( - "have.text", - "Example values are not available for audio/wav media types." - ) - }) - it("should display a file upload button", () => { - cy.visit("/?url=/documents/features/request-body-upload-file.yaml") - .get("#operations-default-uploadApplicationOctetStream") - .click() - .get(".try-out__btn") - .click() - .get(".opblock-section-request-body .opblock-description-wrapper input") - .should("have.prop", "type", "file") - }) - }) - describe("video/mpeg", () => { - it("should display description with the correct content type", () => { - cy.visit("/?url=/documents/features/request-body-upload-file.yaml") - .get("#operations-default-uploadVideoMpeg") - .click() - .get(".opblock-section-request-body .opblock-description-wrapper i") - .should( - "have.text", - "Example values are not available for video/mpeg media types." - ) - }) - it("should display a file upload button", () => { - cy.visit("/?url=/documents/features/request-body-upload-file.yaml") - .get("#operations-default-uploadApplicationOctetStream") - .click() - .get(".try-out__btn") - .click() - .get(".opblock-section-request-body .opblock-description-wrapper input") - .should("have.prop", "type", "file") - }) - }) - describe("schema format binary", () => { - it("should display description with the correct content type", () => { - cy.visit("/?url=/documents/features/request-body-upload-file.yaml") - .get("#operations-default-uploadSchemaFormatBinary") - .click() - .get(".opblock-section-request-body .opblock-description-wrapper i") - .should( - "have.text", - "Example values are not available for application/x-custom media types." - ) - }) - it("should display a file upload button", () => { - cy.visit("/?url=/documents/features/request-body-upload-file.yaml") - .get("#operations-default-uploadSchemaFormatBinary") - .click() - .get(".try-out__btn") - .click() - .get(".opblock-section-request-body .opblock-description-wrapper input") - .should("have.prop", "type", "file") - }) - }) - describe("schema format base64", () => { - it("should display description with the correct content type", () => { - cy.visit("/?url=/documents/features/request-body-upload-file.yaml") - .get("#operations-default-uploadSchemaFormatBase64") - .click() - .get(".opblock-section-request-body .opblock-description-wrapper i") - .should( - "have.text", - "Example values are not available for application/x-custom media types." - ) - }) - it("should display a file upload button", () => { - cy.visit("/?url=/documents/features/request-body-upload-file.yaml") - .get("#operations-default-uploadSchemaFormatBinary") - .click() - .get(".try-out__btn") - .click() - .get(".opblock-section-request-body .opblock-description-wrapper input") - .should("have.prop", "type", "file") - }) - }) -}) diff --git a/test/e2e-cypress/static/documents/features/request-body-upload-file.yaml b/test/e2e-cypress/static/documents/features/oas3-request-body-upload-file.yaml similarity index 55% rename from test/e2e-cypress/static/documents/features/request-body-upload-file.yaml rename to test/e2e-cypress/static/documents/features/oas3-request-body-upload-file.yaml index 2c487da298c..154703db103 100644 --- a/test/e2e-cypress/static/documents/features/request-body-upload-file.yaml +++ b/test/e2e-cypress/static/documents/features/oas3-request-body-upload-file.yaml @@ -7,8 +7,10 @@ info: * `audio/*` content type (no matter what schema format) * `image/*` content type (no matter what schema format) * `video/*` content type (no matter what schema format) - * schema format is `base64` (no matter what content type) - * schema format is `binary` (no matter what content type) + * schema type is `string` and format is `byte` (no matter what content type) + * schema type is `string` and format is `binary` (no matter what content type) + * multipart/form-data object property schema type is `string` and format is `byte` + * multipart/form-data object property schema type is `string` and format is `binary` version: "1.0.0" paths: /upload-application-octet-stream: @@ -19,13 +21,6 @@ paths: application/octet-stream: schema: type: string - responses: - '200': - description: successful operation - content: - text/plain: - schema: - type: string /upload-image-png: post: operationId: uploadImagePng @@ -34,13 +29,6 @@ paths: image/png: schema: type: string - responses: - '200': - description: successful operation - content: - text/plain: - schema: - type: string /upload-audio-wav: post: operationId: uploadAudioWav @@ -49,13 +37,6 @@ paths: audio/wav: schema: type: string - responses: - '200': - description: successful operation - content: - text/plain: - schema: - type: string /upload-video-mpeg: post: operationId: uploadVideoMpeg @@ -64,13 +45,6 @@ paths: video/mpeg: schema: type: string - responses: - '200': - description: successful operation - content: - text/plain: - schema: - type: string /upload-schema-format-binary: post: operationId: uploadSchemaFormatBinary @@ -80,26 +54,36 @@ paths: schema: type: string format: binary - responses: - '200': - description: successful operation - content: - text/plain: - schema: - type: string - /upload-schema-format-base64: + /upload-schema-format-byte: post: - operationId: uploadSchemaFormatBase64 + operationId: uploadSchemaFormatByte requestBody: content: application/x-custom: schema: type: string - format: base64 - responses: - '200': - description: successful operation - content: - text/plain: - schema: - type: string + format: byte + /upload-property-schema-format-binary: + post: + operationId: uploadPropertySchemaFormatBinary + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + /upload-property-schema-format-byte: + post: + operationId: uploadPropertySchemaFormatByte + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: byte diff --git a/test/e2e-cypress/static/documents/features/oas31-request-body-upload-file.yaml b/test/e2e-cypress/static/documents/features/oas31-request-body-upload-file.yaml new file mode 100644 index 00000000000..be6c17630e0 --- /dev/null +++ b/test/e2e-cypress/static/documents/features/oas31-request-body-upload-file.yaml @@ -0,0 +1,143 @@ +openapi: 3.1.0 +info: + title: "Request body file upload" + description: |- + This document has examples for examining the `schema` or content type for request bodies requiring a file upload + * `application/octect-stream` content type (no matter what schema format) + * `audio/*` content type (no matter what schema format) + * `image/*` content type (no matter what schema format) + * `video/*` content type (no matter what schema format) + * schema type is `string` and format is `byte` (no matter what content type) + * schema type is `string` and format is `binary` (no matter what content type) + * schema union type includes `string` and format is `byte` (no matter what content type) + * schema union type includes `string` and format is `binary` (no matter what content type) + * multipart/form-data object property schema type is `string` and format is `byte` + * multipart/form-data object property schema type is `string` and format is `binary` + * multipart/form-data object property schema union type includes `string` and format is `byte` + * multipart/form-data object property schema union type includes `string` and format is `binary` + version: "1.0.0" +paths: + /upload-application-octet-stream: + post: + operationId: uploadApplicationOctetStream + requestBody: + content: + application/octet-stream: + schema: + type: string + /upload-image-png: + post: + operationId: uploadImagePng + requestBody: + content: + image/png: + schema: + type: string + /upload-audio-wav: + post: + operationId: uploadAudioWav + requestBody: + content: + audio/wav: + schema: + type: string + /upload-video-mpeg: + post: + operationId: uploadVideoMpeg + requestBody: + content: + video/mpeg: + schema: + type: string + /upload-schema-type-format-binary: + post: + operationId: uploadSchemaTypeFormatBinary + requestBody: + content: + application/x-custom: + schema: + type: string + format: binary + /upload-schema-type-format-byte: + post: + operationId: uploadSchemaTypeFormatByte + requestBody: + content: + application/x-custom: + schema: + type: string + format: byte + /upload-schema-union-type-format-binary: + post: + operationId: uploadSchemaUnionTypeFormatBinary + requestBody: + content: + application/x-custom: + schema: + type: + - object + - string + format: binary + /upload-schema-union-type-format-byte: + post: + operationId: uploadSchemaUnionTypeFormatByte + requestBody: + content: + application/x-custom: + schema: + type: + - object + - string + format: byte + /upload-property-schema-format-binary: + post: + operationId: uploadPropertySchemaFormatBinary + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + /upload-property-schema-format-byte: + post: + operationId: uploadPropertySchemaFormatByte + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + /upload-property-schema-union-type-format-binary: + post: + operationId: uploadPropertySchemaUnionTypeFormatBinary + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: + - object + - string + format: binary + /upload-property-schema-union-type-format-byte: + post: + operationId: uploadPropertySchemaUnionTypeFormatByte + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: + - object + - string + format: byte diff --git a/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx b/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx index 73d0d2292a3..5626554193c 100644 --- a/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx +++ b/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx @@ -4,6 +4,7 @@ import { Select, Input, TextArea } from "core/components/layout-utils" import { mount, render } from "enzyme" import * as JsonSchemaComponents from "core/plugins/json-schema-5/components/json-schema-components" import { foldType } from "core/plugins/json-schema-2020-12-samples/fn/index" +import { isFileUploadIntended } from "core/plugins/oas3/fn" const components = {...JsonSchemaComponents, Select, Input, TextArea} @@ -26,6 +27,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, + isFileUploadIntended }, schema: Immutable.fromJS({ type: "string", @@ -53,6 +55,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, + isFileUploadIntended }, schema: Immutable.fromJS({ type: "string", @@ -78,6 +81,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, + isFileUploadIntended }, required: true, schema: Immutable.fromJS({ @@ -106,6 +110,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, + isFileUploadIntended }, schema: Immutable.fromJS({ type: "boolean" @@ -133,6 +138,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, + isFileUploadIntended }, schema: Immutable.fromJS({ type: "boolean", @@ -160,6 +166,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, + isFileUploadIntended }, schema: Immutable.fromJS({ type: "boolean", @@ -188,6 +195,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, + isFileUploadIntended }, required: true, schema: Immutable.fromJS({ @@ -219,6 +227,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, + isFileUploadIntended }, errors: List(), schema: Immutable.fromJS({ @@ -252,6 +261,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, + isFileUploadIntended }, schema: Immutable.fromJS({ type: "NotARealType" @@ -278,6 +288,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, + isFileUploadIntended }, schema: Immutable.fromJS({ type: "NotARealType", From 64d09d486458fdd8cdd59d856680556571c0c5ad Mon Sep 17 00:00:00 2001 From: Oliwia Rogala Date: Thu, 10 Apr 2025 12:48:16 +0200 Subject: [PATCH 2/8] test: add tests for empty Media Type Object --- .../oas3/request-body-upload-file.cy.js | 22 +++++++++++++++++++ .../oas31-request-body-upload-file.cy.js | 22 +++++++++++++++++++ .../oas3-request-body-upload-file.yaml | 7 ++++++ .../oas31-request-body-upload-file.yaml | 7 ++++++ 4 files changed, 58 insertions(+) diff --git a/test/e2e-cypress/e2e/features/plugins/oas3/request-body-upload-file.cy.js b/test/e2e-cypress/e2e/features/plugins/oas3/request-body-upload-file.cy.js index 45f7ff3880f..c5827b59432 100644 --- a/test/e2e-cypress/e2e/features/plugins/oas3/request-body-upload-file.cy.js +++ b/test/e2e-cypress/e2e/features/plugins/oas3/request-body-upload-file.cy.js @@ -95,6 +95,28 @@ describe("OpenAPI 3.0 Request Body upload file button", () => { }) }) + describe("application/octet-stream with empty Media Type Object", () => { + beforeEach(() => { + cy.get("#operations-default-uploadApplicationOctetStreamEmpty").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for application/octet-stream media types." + ) + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + describe("schema type string and format binary", () => { beforeEach(() => { cy.get("#operations-default-uploadSchemaFormatBinary").click() diff --git a/test/e2e-cypress/e2e/features/plugins/oas31/oas31-request-body-upload-file.cy.js b/test/e2e-cypress/e2e/features/plugins/oas31/oas31-request-body-upload-file.cy.js index 50742ec26c4..4d1f9a254aa 100644 --- a/test/e2e-cypress/e2e/features/plugins/oas31/oas31-request-body-upload-file.cy.js +++ b/test/e2e-cypress/e2e/features/plugins/oas31/oas31-request-body-upload-file.cy.js @@ -91,6 +91,28 @@ describe("OpenAPI 3.1 Request Body upload file button", () => { }) }) + describe("application/octet-stream with empty Media Type Object", () => { + beforeEach(() => { + cy.get("#operations-default-uploadApplicationOctetStreamEmpty").click() + }) + + it("should display description with the correct content type", () => { + cy.get( + ".opblock-section-request-body .opblock-description-wrapper i" + ).should( + "have.text", + "Example values are not available for application/octet-stream media types." + ) + }) + + it("should display a file upload button", () => { + cy.get(".try-out__btn").click() + cy.get( + ".opblock-section-request-body .opblock-description-wrapper input" + ).should("have.prop", "type", "file") + }) + }) + describe("schema type string and format binary", () => { beforeEach(() => { cy.get("#operations-default-uploadSchemaTypeFormatBinary").click() diff --git a/test/e2e-cypress/static/documents/features/oas3-request-body-upload-file.yaml b/test/e2e-cypress/static/documents/features/oas3-request-body-upload-file.yaml index 154703db103..619a5095a14 100644 --- a/test/e2e-cypress/static/documents/features/oas3-request-body-upload-file.yaml +++ b/test/e2e-cypress/static/documents/features/oas3-request-body-upload-file.yaml @@ -7,6 +7,7 @@ info: * `audio/*` content type (no matter what schema format) * `image/*` content type (no matter what schema format) * `video/*` content type (no matter what schema format) + * `application/octect-stream` content type with empty Media Type Object * schema type is `string` and format is `byte` (no matter what content type) * schema type is `string` and format is `binary` (no matter what content type) * multipart/form-data object property schema type is `string` and format is `byte` @@ -45,6 +46,12 @@ paths: video/mpeg: schema: type: string + /upload-application-octet-stream-empty: + post: + operationId: uploadApplicationOctetStreamEmpty + requestBody: + content: + application/octet-stream: {} /upload-schema-format-binary: post: operationId: uploadSchemaFormatBinary diff --git a/test/e2e-cypress/static/documents/features/oas31-request-body-upload-file.yaml b/test/e2e-cypress/static/documents/features/oas31-request-body-upload-file.yaml index be6c17630e0..85c04608eef 100644 --- a/test/e2e-cypress/static/documents/features/oas31-request-body-upload-file.yaml +++ b/test/e2e-cypress/static/documents/features/oas31-request-body-upload-file.yaml @@ -7,6 +7,7 @@ info: * `audio/*` content type (no matter what schema format) * `image/*` content type (no matter what schema format) * `video/*` content type (no matter what schema format) + * `application/octect-stream` content type with empty Media Type Object * schema type is `string` and format is `byte` (no matter what content type) * schema type is `string` and format is `binary` (no matter what content type) * schema union type includes `string` and format is `byte` (no matter what content type) @@ -49,6 +50,12 @@ paths: video/mpeg: schema: type: string + /upload-application-octet-stream-empty: + post: + operationId: uploadApplicationOctetStreamEmpty + requestBody: + content: + application/octet-stream: {} /upload-schema-type-format-binary: post: operationId: uploadSchemaTypeFormatBinary From 0579584afbbd28beca2cd4eeb71806e7353dd363 Mon Sep 17 00:00:00 2001 From: Oliwia Rogala Date: Thu, 10 Apr 2025 15:22:52 +0200 Subject: [PATCH 3/8] feat: add config option for file upload media types --- src/core/config/defaults.js | 7 ++++++ src/core/config/type-cast/mappings.js | 4 +++ .../plugins/oas3/components/request-body.jsx | 14 ++++++++--- src/core/plugins/oas3/fn.js | 22 +++++++++------- src/core/plugins/oas3/index.js | 7 ++++-- src/core/plugins/oas31/after-load.js | 2 ++ src/core/plugins/oas31/oas3-extensions/fn.js | 25 +++++++++++-------- 7 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/core/config/defaults.js b/src/core/config/defaults.js index b7daa8fd624..12ce768b11e 100644 --- a/src/core/config/defaults.js +++ b/src/core/config/defaults.js @@ -87,6 +87,13 @@ const defaultOptions = Object.freeze({ onComplete: null, modelPropertyMacro: null, parameterMacro: null, + + fileUploadMediaTypes: [ + "application/octet-stream", + "image/", + "audio/", + "video/", + ], }) export default defaultOptions diff --git a/src/core/config/type-cast/mappings.js b/src/core/config/type-cast/mappings.js index b025fb1295e..559f35025c2 100644 --- a/src/core/config/type-cast/mappings.js +++ b/src/core/config/type-cast/mappings.js @@ -45,6 +45,10 @@ const mappings = { docExpansion: { typeCaster: stringTypeCaster }, dom_id: { typeCaster: nullableStringTypeCaster }, domNode: { typeCaster: domNodeTypeCaster }, + fileUploadMediaTypes: { + typeCaster: arrayTypeCaster, + defaultValue: defaultOptions.fileUploadMediaTypes, + }, filter: { typeCaster: filterTypeCaster }, fn: { typeCaster: objectTypeCaster }, initialState: { typeCaster: objectTypeCaster }, diff --git a/src/core/plugins/oas3/components/request-body.jsx b/src/core/plugins/oas3/components/request-body.jsx index 2ab7ea55918..e8400e74083 100644 --- a/src/core/plugins/oas3/components/request-body.jsx +++ b/src/core/plugins/oas3/components/request-body.jsx @@ -78,7 +78,7 @@ const RequestBody = ({ const Example = getComponent("Example") const ParameterIncludeEmpty = getComponent("ParameterIncludeEmpty") - const { showCommonExtensions } = getConfigs() + const { showCommonExtensions, fileUploadMediaTypes } = getConfigs() const requestBodyDescription = requestBody?.get("description") ?? null const requestBodyContent = requestBody?.get("content") ?? new OrderedMap() @@ -105,7 +105,10 @@ const RequestBody = ({ } requestBodyErrors = List.isList(requestBodyErrors) ? requestBodyErrors : List() - const isFileUploadIntended = fn.isFileUploadIntended(mediaTypeValue?.get("schema"), contentType) + const isFileUploadIntended = fn.isFileUploadIntended( + mediaTypeValue?.get("schema"), + { mediaType: contentType, fileUploadMediaTypes } + ) if( isFileUploadIntended @@ -126,7 +129,12 @@ const RequestBody = ({ return null } - const isObjectContent = mediaTypeValue.getIn(["schema", "type"]) === "object" + const schemaType = mediaTypeValue.getIn(["schema", "type"]) + const isObjectContent = + schemaType === "object" || + (specSelectors.isOAS31() && + List.isList(schemaType) && + schemaType.includes("object")) if ( isObjectContent && diff --git a/src/core/plugins/oas3/fn.js b/src/core/plugins/oas3/fn.js index 8c9201a1b14..67d8c26db45 100644 --- a/src/core/plugins/oas3/fn.js +++ b/src/core/plugins/oas3/fn.js @@ -1,22 +1,26 @@ /** * @prettier */ +import { Map } from "immutable" -export const isFileUploadIntended = (schema, mediaType = null) => { - if ( +export const isFileUploadIntended = ( + schema, + { mediaType = null, fileUploadMediaTypes = [] } = {} +) => { + const isFileUploadMediaType = typeof mediaType === "string" && - (mediaType.startsWith("application/octet-stream") || - mediaType.startsWith("image/") || - mediaType.startsWith("audio/") || - mediaType.startsWith("video/")) - ) { + fileUploadMediaTypes.some((fileUploadMediaType) => + mediaType.startsWith(fileUploadMediaType) + ) + + if (isFileUploadMediaType) { return true } - if (!schema) return false + if (!Map.isMap(schema)) return false const type = schema.get("type") const format = schema.get("format") - return type === "string" && (format === "binary" || format === "byte") + return type === "string" && ["binary", "byte"].includes(format) } diff --git a/src/core/plugins/oas3/index.js b/src/core/plugins/oas3/index.js index 6423a27e1ef..8da48dfcc28 100644 --- a/src/core/plugins/oas3/index.js +++ b/src/core/plugins/oas3/index.js @@ -9,7 +9,7 @@ import wrapComponents from "./wrap-components" import * as actions from "./actions" import * as selectors from "./selectors" import reducers from "./reducers" -import * as fn from "./fn" +import { isFileUploadIntended } from "./fn" export default function () { return { @@ -29,6 +29,9 @@ export default function () { selectors: { ...selectors }, }, }, - fn, + fn: { + isFileUploadIntended, + isFileUploadIntendedOAS30: isFileUploadIntended, + }, } } diff --git a/src/core/plugins/oas31/after-load.js b/src/core/plugins/oas31/after-load.js index 0228398794f..a0df8b09b19 100644 --- a/src/core/plugins/oas31/after-load.js +++ b/src/core/plugins/oas31/after-load.js @@ -40,12 +40,14 @@ function afterLoad({ fn, getSystem }) { Object.assign(this.fn, wrappedFns) } + // overrides behavior in OpenAPI 3.1.x, recognizes more intentions const { isFileUploadIntended: isFileUploadIntendedWrap } = wrapOAS31Fn( { isFileUploadIntended }, getSystem() ) this.fn.isFileUploadIntended = isFileUploadIntendedWrap + this.fn.isFileUploadIntendedOAS31 = isFileUploadIntended } export default afterLoad diff --git a/src/core/plugins/oas31/oas3-extensions/fn.js b/src/core/plugins/oas31/oas3-extensions/fn.js index a06d8bbd930..e499efb678d 100644 --- a/src/core/plugins/oas31/oas3-extensions/fn.js +++ b/src/core/plugins/oas31/oas3-extensions/fn.js @@ -1,25 +1,30 @@ /** * @prettier */ +import { Map } from "immutable" + import { immutableToJS } from "core/utils" -export const isFileUploadIntended = (schema, mediaType = null) => { - if ( +export const isFileUploadIntended = ( + schema, + { mediaType = null, fileUploadMediaTypes = [] } = {} +) => { + const isFileUploadMediaType = typeof mediaType === "string" && - (mediaType.startsWith("application/octet-stream") || - mediaType.startsWith("image/") || - mediaType.startsWith("audio/") || - mediaType.startsWith("video/")) - ) { + fileUploadMediaTypes.some((fileUploadMediaType) => + mediaType.startsWith(fileUploadMediaType) + ) + + if (isFileUploadMediaType) { return true } - if (!schema) return null + if (!Map.isMap(schema)) return null const type = immutableToJS(schema.get("type")) - const isTypeString = + const includesStringType = type === "string" || (Array.isArray(type) && type.includes("string")) const format = schema.get("format") - return isTypeString && (format === "binary" || format === "byte") + return includesStringType && ["binary", "byte"].includes(format) } From 790d34bf8a8b1cf59d55acc90ec8a293f64a131b Mon Sep 17 00:00:00 2001 From: Oliwia Rogala Date: Fri, 11 Apr 2025 08:28:54 +0200 Subject: [PATCH 4/8] refactor: get configs internally and transform schema to object --- .../plugins/oas3/components/request-body.jsx | 4 +- src/core/plugins/oas3/fn.js | 33 ++++++++-------- src/core/plugins/oas3/index.js | 6 ++- src/core/plugins/oas31/after-load.js | 3 +- src/core/plugins/oas31/oas3-extensions/fn.js | 39 +++++++++---------- 5 files changed, 42 insertions(+), 43 deletions(-) diff --git a/src/core/plugins/oas3/components/request-body.jsx b/src/core/plugins/oas3/components/request-body.jsx index e8400e74083..0dda4701e95 100644 --- a/src/core/plugins/oas3/components/request-body.jsx +++ b/src/core/plugins/oas3/components/request-body.jsx @@ -78,7 +78,7 @@ const RequestBody = ({ const Example = getComponent("Example") const ParameterIncludeEmpty = getComponent("ParameterIncludeEmpty") - const { showCommonExtensions, fileUploadMediaTypes } = getConfigs() + const { showCommonExtensions } = getConfigs() const requestBodyDescription = requestBody?.get("description") ?? null const requestBodyContent = requestBody?.get("content") ?? new OrderedMap() @@ -107,7 +107,7 @@ const RequestBody = ({ const isFileUploadIntended = fn.isFileUploadIntended( mediaTypeValue?.get("schema"), - { mediaType: contentType, fileUploadMediaTypes } + contentType ) if( diff --git a/src/core/plugins/oas3/fn.js b/src/core/plugins/oas3/fn.js index 67d8c26db45..3e5ca98832d 100644 --- a/src/core/plugins/oas3/fn.js +++ b/src/core/plugins/oas3/fn.js @@ -1,26 +1,25 @@ /** * @prettier */ -import { Map } from "immutable" +import { objectify } from "core/utils" -export const isFileUploadIntended = ( - schema, - { mediaType = null, fileUploadMediaTypes = [] } = {} -) => { - const isFileUploadMediaType = - typeof mediaType === "string" && - fileUploadMediaTypes.some((fileUploadMediaType) => - mediaType.startsWith(fileUploadMediaType) - ) +export const makeIsFileUploadIntended = (getSystem) => { + const isFileUploadIntended = (schema, mediaType = null) => { + const { fileUploadMediaTypes } = getSystem().getConfigs() + const isFileUploadMediaType = + typeof mediaType === "string" && + fileUploadMediaTypes.some((fileUploadMediaType) => + mediaType.startsWith(fileUploadMediaType) + ) - if (isFileUploadMediaType) { - return true - } + if (isFileUploadMediaType) { + return true + } - if (!Map.isMap(schema)) return false + const { type, format } = objectify(schema) - const type = schema.get("type") - const format = schema.get("format") + return type === "string" && ["binary", "byte"].includes(format) + } - return type === "string" && ["binary", "byte"].includes(format) + return isFileUploadIntended } diff --git a/src/core/plugins/oas3/index.js b/src/core/plugins/oas3/index.js index 8da48dfcc28..9a42d9e2ae1 100644 --- a/src/core/plugins/oas3/index.js +++ b/src/core/plugins/oas3/index.js @@ -9,9 +9,11 @@ import wrapComponents from "./wrap-components" import * as actions from "./actions" import * as selectors from "./selectors" import reducers from "./reducers" -import { isFileUploadIntended } from "./fn" +import { makeIsFileUploadIntended } from "./fn" + +export default function ({ getSystem }) { + const isFileUploadIntended = makeIsFileUploadIntended(getSystem) -export default function () { return { components, wrapComponents, diff --git a/src/core/plugins/oas31/after-load.js b/src/core/plugins/oas31/after-load.js index a0df8b09b19..6232dad36bd 100644 --- a/src/core/plugins/oas31/after-load.js +++ b/src/core/plugins/oas31/after-load.js @@ -6,7 +6,7 @@ import { getProperties, } from "./json-schema-2020-12-extensions/fn" import { wrapOAS31Fn } from "./fn" -import { isFileUploadIntended } from "./oas3-extensions/fn" +import { makeIsFileUploadIntended } from "./oas3-extensions/fn" function afterLoad({ fn, getSystem }) { // overrides for fn.jsonSchema202012 @@ -41,6 +41,7 @@ function afterLoad({ fn, getSystem }) { } // overrides behavior in OpenAPI 3.1.x, recognizes more intentions + const isFileUploadIntended = makeIsFileUploadIntended(getSystem) const { isFileUploadIntended: isFileUploadIntendedWrap } = wrapOAS31Fn( { isFileUploadIntended }, getSystem() diff --git a/src/core/plugins/oas31/oas3-extensions/fn.js b/src/core/plugins/oas31/oas3-extensions/fn.js index e499efb678d..3ae8a0c1b6a 100644 --- a/src/core/plugins/oas31/oas3-extensions/fn.js +++ b/src/core/plugins/oas31/oas3-extensions/fn.js @@ -1,30 +1,27 @@ /** * @prettier */ -import { Map } from "immutable" +import { objectify } from "core/utils" -import { immutableToJS } from "core/utils" +export const makeIsFileUploadIntended = (getSystem) => { + const isFileUploadIntended = (schema, mediaType = null) => { + const { fileUploadMediaTypes } = getSystem().getConfigs() + const isFileUploadMediaType = + typeof mediaType === "string" && + fileUploadMediaTypes.some((fileUploadMediaType) => + mediaType.startsWith(fileUploadMediaType) + ) -export const isFileUploadIntended = ( - schema, - { mediaType = null, fileUploadMediaTypes = [] } = {} -) => { - const isFileUploadMediaType = - typeof mediaType === "string" && - fileUploadMediaTypes.some((fileUploadMediaType) => - mediaType.startsWith(fileUploadMediaType) - ) + if (isFileUploadMediaType) { + return true + } - if (isFileUploadMediaType) { - return true - } - - if (!Map.isMap(schema)) return null + const { type, format } = objectify(schema) + const includesStringType = + type === "string" || (Array.isArray(type) && type.includes("string")) - const type = immutableToJS(schema.get("type")) - const includesStringType = - type === "string" || (Array.isArray(type) && type.includes("string")) - const format = schema.get("format") + return includesStringType && ["binary", "byte"].includes(format) + } - return includesStringType && ["binary", "byte"].includes(format) + return isFileUploadIntended } From 0f0c5cdbefb69422ae3ca41c6d55f1f5cb06efce Mon Sep 17 00:00:00 2001 From: Oliwia Rogala Date: Fri, 11 Apr 2025 08:38:21 +0200 Subject: [PATCH 5/8] test: fix function usage in unit tests --- .../components/json-schema-form.jsx | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx b/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx index 5626554193c..6249a3ca373 100644 --- a/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx +++ b/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx @@ -4,7 +4,7 @@ import { Select, Input, TextArea } from "core/components/layout-utils" import { mount, render } from "enzyme" import * as JsonSchemaComponents from "core/plugins/json-schema-5/components/json-schema-components" import { foldType } from "core/plugins/json-schema-2020-12-samples/fn/index" -import { isFileUploadIntended } from "core/plugins/oas3/fn" +import { makeIsFileUploadIntended } from "core/plugins/oas3/fn" const components = {...JsonSchemaComponents, Select, Input, TextArea} @@ -14,6 +14,12 @@ const getComponentStub = (name) => { return null } +const getSystemStub = () => ({ + getConfigs: () => ({ + fileUploadMediaTypes: [] + }) +}) + describe("", function(){ describe("strings", function() { it("should render the correct options for a string enum parameter", function(){ @@ -27,7 +33,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, - isFileUploadIntended + isFileUploadIntended: makeIsFileUploadIntended(getSystemStub) }, schema: Immutable.fromJS({ type: "string", @@ -55,7 +61,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, - isFileUploadIntended + isFileUploadIntended: makeIsFileUploadIntended(getSystemStub) }, schema: Immutable.fromJS({ type: "string", @@ -81,7 +87,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, - isFileUploadIntended + isFileUploadIntended: makeIsFileUploadIntended(getSystemStub) }, required: true, schema: Immutable.fromJS({ @@ -110,7 +116,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, - isFileUploadIntended + isFileUploadIntended: makeIsFileUploadIntended(getSystemStub) }, schema: Immutable.fromJS({ type: "boolean" @@ -138,7 +144,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, - isFileUploadIntended + isFileUploadIntended: makeIsFileUploadIntended(getSystemStub) }, schema: Immutable.fromJS({ type: "boolean", @@ -166,7 +172,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, - isFileUploadIntended + isFileUploadIntended: makeIsFileUploadIntended(getSystemStub) }, schema: Immutable.fromJS({ type: "boolean", @@ -195,7 +201,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, - isFileUploadIntended + isFileUploadIntended: makeIsFileUploadIntended(getSystemStub) }, required: true, schema: Immutable.fromJS({ @@ -227,7 +233,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, - isFileUploadIntended + isFileUploadIntended: makeIsFileUploadIntended(getSystemStub) }, errors: List(), schema: Immutable.fromJS({ @@ -261,7 +267,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, - isFileUploadIntended + isFileUploadIntended: makeIsFileUploadIntended(getSystemStub) }, schema: Immutable.fromJS({ type: "NotARealType" @@ -288,7 +294,7 @@ describe("", function(){ jsonSchema202012: { foldType, }, - isFileUploadIntended + isFileUploadIntended: makeIsFileUploadIntended(getSystemStub) }, schema: Immutable.fromJS({ type: "NotARealType", From 9782dd01b9e9bd054f1b7d35475fce45dd1a7a8f Mon Sep 17 00:00:00 2001 From: Oliwia Rogala Date: Fri, 11 Apr 2025 11:51:49 +0200 Subject: [PATCH 6/8] feat: add function for checking if schema has certain types --- src/core/plugins/json-schema-2020-12/fn.js | 18 ++++++++++++++++++ src/core/plugins/json-schema-2020-12/index.js | 2 ++ src/core/plugins/json-schema-5/fn.js | 17 +++++++++++++++++ src/core/plugins/json-schema-5/index.js | 4 ++++ .../plugins/oas3/components/request-body.jsx | 7 +------ src/core/plugins/oas3/fn.js | 19 +++++++++++++++---- src/core/plugins/oas31/after-load.js | 15 +++++++++++---- src/core/plugins/oas31/oas3-extensions/fn.js | 18 ++---------------- 8 files changed, 70 insertions(+), 30 deletions(-) create mode 100644 src/core/plugins/json-schema-5/fn.js diff --git a/src/core/plugins/json-schema-2020-12/fn.js b/src/core/plugins/json-schema-2020-12/fn.js index bdf158b0ec2..69459506a92 100644 --- a/src/core/plugins/json-schema-2020-12/fn.js +++ b/src/core/plugins/json-schema-2020-12/fn.js @@ -1,6 +1,8 @@ /** * @prettier */ +import { List, Map } from "immutable" + export const upperFirst = (value) => { if (typeof value === "string") { return `${value.charAt(0).toUpperCase()}${value.slice(1)}` @@ -502,3 +504,19 @@ export const makeGetExtensionKeywords = (fnAccessor) => { return getExtensionKeywords } + +export const schemaHasType = (schema, types) => { + const isSchemaImmutable = Map.isMap(schema) + + if (!isSchemaImmutable && !isPlainObject(schema)) { + return false + } + + const type = isSchemaImmutable ? schema.get("type") : schema.type + + if (List.isList(type) || Array.isArray(type)) { + return type.some((t) => types.includes(t)) + } + + return types.includes(type) +} diff --git a/src/core/plugins/json-schema-2020-12/index.js b/src/core/plugins/json-schema-2020-12/index.js index e6c929d9fd9..9593c0f8638 100644 --- a/src/core/plugins/json-schema-2020-12/index.js +++ b/src/core/plugins/json-schema-2020-12/index.js @@ -55,6 +55,7 @@ import { isBooleanJSONSchema, getSchemaKeywords, makeGetExtensionKeywords, + schemaHasType, } from "./fn" import { JSONSchemaPathContext, JSONSchemaLevelContext } from "./context" import { @@ -143,6 +144,7 @@ const JSONSchema202012Plugin = ({ getSystem, fn }) => { useLevel, getSchemaKeywords, getExtensionKeywords: makeGetExtensionKeywords(fnAccessor), + schemaHasType, }, }, } diff --git a/src/core/plugins/json-schema-5/fn.js b/src/core/plugins/json-schema-5/fn.js new file mode 100644 index 00000000000..a763e950e77 --- /dev/null +++ b/src/core/plugins/json-schema-5/fn.js @@ -0,0 +1,17 @@ +/** + * @prettier + */ +import { Map } from "immutable" +import isPlainObject from "lodash/isPlainObject" + +export const schemaHasType = (schema, types) => { + const isSchemaImmutable = Map.isMap(schema) + + if (!isSchemaImmutable && !isPlainObject(schema)) { + return false + } + + const type = isSchemaImmutable ? schema.get("type") : schema.type + + return types.includes(type) +} diff --git a/src/core/plugins/json-schema-5/index.js b/src/core/plugins/json-schema-5/index.js index 3aa3e375d78..9edc406540d 100644 --- a/src/core/plugins/json-schema-5/index.js +++ b/src/core/plugins/json-schema-5/index.js @@ -14,6 +14,7 @@ import Schemes from "./components/schemes" import SchemesContainer from "./containers/schemes" import * as JSONSchemaComponents from "./components/json-schema-components" import { ModelExtensions } from "./components/model-extensions" +import { schemaHasType } from "./fn" const JSONSchema5Plugin = () => ({ components: { @@ -31,6 +32,9 @@ const JSONSchema5Plugin = () => ({ SchemesContainer, ...JSONSchemaComponents, }, + fn: { + schemaHasType, + }, }) export default JSONSchema5Plugin diff --git a/src/core/plugins/oas3/components/request-body.jsx b/src/core/plugins/oas3/components/request-body.jsx index 0dda4701e95..2393ac829c1 100644 --- a/src/core/plugins/oas3/components/request-body.jsx +++ b/src/core/plugins/oas3/components/request-body.jsx @@ -129,12 +129,7 @@ const RequestBody = ({ return null } - const schemaType = mediaTypeValue.getIn(["schema", "type"]) - const isObjectContent = - schemaType === "object" || - (specSelectors.isOAS31() && - List.isList(schemaType) && - schemaType.includes("object")) + const isObjectContent = fn.schemaHasType(mediaTypeValue.get("schema"), ["object"]) if ( isObjectContent && diff --git a/src/core/plugins/oas3/fn.js b/src/core/plugins/oas3/fn.js index 3e5ca98832d..2f7d3a75d3a 100644 --- a/src/core/plugins/oas3/fn.js +++ b/src/core/plugins/oas3/fn.js @@ -1,11 +1,13 @@ /** * @prettier */ -import { objectify } from "core/utils" +import { Map } from "immutable" +import isPlainObject from "lodash/isPlainObject" export const makeIsFileUploadIntended = (getSystem) => { const isFileUploadIntended = (schema, mediaType = null) => { - const { fileUploadMediaTypes } = getSystem().getConfigs() + const { getConfigs, fn } = getSystem() + const { fileUploadMediaTypes } = getConfigs() const isFileUploadMediaType = typeof mediaType === "string" && fileUploadMediaTypes.some((fileUploadMediaType) => @@ -16,9 +18,18 @@ export const makeIsFileUploadIntended = (getSystem) => { return true } - const { type, format } = objectify(schema) + const isSchemaImmutable = Map.isMap(schema) - return type === "string" && ["binary", "byte"].includes(format) + if (!isSchemaImmutable && !isPlainObject(schema)) { + return false + } + + const format = isSchemaImmutable ? schema.get("format") : schema.format + + return ( + fn.schemaHasType(schema, ["string"]) && + ["binary", "byte"].includes(format) + ) } return isFileUploadIntended diff --git a/src/core/plugins/oas31/after-load.js b/src/core/plugins/oas31/after-load.js index 6232dad36bd..746443c3afc 100644 --- a/src/core/plugins/oas31/after-load.js +++ b/src/core/plugins/oas31/after-load.js @@ -42,13 +42,20 @@ function afterLoad({ fn, getSystem }) { // overrides behavior in OpenAPI 3.1.x, recognizes more intentions const isFileUploadIntended = makeIsFileUploadIntended(getSystem) - const { isFileUploadIntended: isFileUploadIntendedWrap } = wrapOAS31Fn( - { isFileUploadIntended }, - getSystem() - ) + const { isFileUploadIntended: isFileUploadIntendedWrap, schemaHasType } = + wrapOAS31Fn( + { + isFileUploadIntended, + ...(fn.jsonSchema202012 && { + schemaHasType: fn.jsonSchema202012.schemaHasType, + }), + }, + getSystem() + ) this.fn.isFileUploadIntended = isFileUploadIntendedWrap this.fn.isFileUploadIntendedOAS31 = isFileUploadIntended + this.fn.schemaHasType = schemaHasType } export default afterLoad diff --git a/src/core/plugins/oas31/oas3-extensions/fn.js b/src/core/plugins/oas31/oas3-extensions/fn.js index 3ae8a0c1b6a..f032e228247 100644 --- a/src/core/plugins/oas31/oas3-extensions/fn.js +++ b/src/core/plugins/oas31/oas3-extensions/fn.js @@ -1,26 +1,12 @@ /** * @prettier */ -import { objectify } from "core/utils" export const makeIsFileUploadIntended = (getSystem) => { const isFileUploadIntended = (schema, mediaType = null) => { - const { fileUploadMediaTypes } = getSystem().getConfigs() - const isFileUploadMediaType = - typeof mediaType === "string" && - fileUploadMediaTypes.some((fileUploadMediaType) => - mediaType.startsWith(fileUploadMediaType) - ) + const { fn } = getSystem() - if (isFileUploadMediaType) { - return true - } - - const { type, format } = objectify(schema) - const includesStringType = - type === "string" || (Array.isArray(type) && type.includes("string")) - - return includesStringType && ["binary", "byte"].includes(format) + return fn.isFileUploadIntendedOAS30(schema, mediaType) } return isFileUploadIntended From 0c106715c770c83e0316e1236630388b78388599 Mon Sep 17 00:00:00 2001 From: Oliwia Rogala Date: Fri, 11 Apr 2025 11:55:14 +0200 Subject: [PATCH 7/8] test: add missing functions for unit tests --- .../json-schema-5/components/json-schema-form.jsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx b/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx index 6249a3ca373..6b58cd239a9 100644 --- a/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx +++ b/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx @@ -15,9 +15,13 @@ const getComponentStub = (name) => { } const getSystemStub = () => ({ - getConfigs: () => ({ - fileUploadMediaTypes: [] - }) + getConfigs: () => ({ + fileUploadMediaTypes: [], + }), + fn: { + schemaHasType: () => {}, + isFileUploadIntendedOAS30: () => {}, + }, }) describe("", function(){ From aa3341506805d2ed641549716510469edad7af02 Mon Sep 17 00:00:00 2001 From: Oliwia Rogala Date: Fri, 11 Apr 2025 13:14:02 +0200 Subject: [PATCH 8/8] feat: support single type comparison --- src/core/plugins/json-schema-2020-12/fn.js | 13 ++++++---- src/core/plugins/json-schema-2020-12/index.js | 4 ++-- src/core/plugins/json-schema-5/fn.js | 8 ++++--- src/core/plugins/json-schema-5/index.js | 4 ++-- .../plugins/oas3/components/request-body.jsx | 2 +- src/core/plugins/oas3/fn.js | 3 +-- src/core/plugins/oas31/after-load.js | 24 ++++++++++++------- .../components/json-schema-form.jsx | 2 +- 8 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/core/plugins/json-schema-2020-12/fn.js b/src/core/plugins/json-schema-2020-12/fn.js index 69459506a92..4d9e0b8ef5e 100644 --- a/src/core/plugins/json-schema-2020-12/fn.js +++ b/src/core/plugins/json-schema-2020-12/fn.js @@ -505,18 +505,21 @@ export const makeGetExtensionKeywords = (fnAccessor) => { return getExtensionKeywords } -export const schemaHasType = (schema, types) => { +export const hasSchemaType = (schema, type) => { const isSchemaImmutable = Map.isMap(schema) if (!isSchemaImmutable && !isPlainObject(schema)) { return false } - const type = isSchemaImmutable ? schema.get("type") : schema.type + const hasType = (schemaType) => + type === schemaType || (Array.isArray(type) && type.includes(schemaType)) - if (List.isList(type) || Array.isArray(type)) { - return type.some((t) => types.includes(t)) + const schemaType = isSchemaImmutable ? schema.get("type") : schema.type + + if (List.isList(schemaType) || Array.isArray(schemaType)) { + return schemaType.some((t) => hasType(t)) } - return types.includes(type) + return hasType(schemaType) } diff --git a/src/core/plugins/json-schema-2020-12/index.js b/src/core/plugins/json-schema-2020-12/index.js index 9593c0f8638..c78d89dc7d2 100644 --- a/src/core/plugins/json-schema-2020-12/index.js +++ b/src/core/plugins/json-schema-2020-12/index.js @@ -55,7 +55,7 @@ import { isBooleanJSONSchema, getSchemaKeywords, makeGetExtensionKeywords, - schemaHasType, + hasSchemaType, } from "./fn" import { JSONSchemaPathContext, JSONSchemaLevelContext } from "./context" import { @@ -144,7 +144,7 @@ const JSONSchema202012Plugin = ({ getSystem, fn }) => { useLevel, getSchemaKeywords, getExtensionKeywords: makeGetExtensionKeywords(fnAccessor), - schemaHasType, + hasSchemaType, }, }, } diff --git a/src/core/plugins/json-schema-5/fn.js b/src/core/plugins/json-schema-5/fn.js index a763e950e77..c4844b4bcba 100644 --- a/src/core/plugins/json-schema-5/fn.js +++ b/src/core/plugins/json-schema-5/fn.js @@ -4,14 +4,16 @@ import { Map } from "immutable" import isPlainObject from "lodash/isPlainObject" -export const schemaHasType = (schema, types) => { +export const hasSchemaType = (schema, type) => { const isSchemaImmutable = Map.isMap(schema) if (!isSchemaImmutable && !isPlainObject(schema)) { return false } - const type = isSchemaImmutable ? schema.get("type") : schema.type + const schemaType = isSchemaImmutable ? schema.get("type") : schema.type - return types.includes(type) + return ( + type === schemaType || (Array.isArray(type) && type.includes(schemaType)) + ) } diff --git a/src/core/plugins/json-schema-5/index.js b/src/core/plugins/json-schema-5/index.js index 9edc406540d..f3e394a1699 100644 --- a/src/core/plugins/json-schema-5/index.js +++ b/src/core/plugins/json-schema-5/index.js @@ -14,7 +14,7 @@ import Schemes from "./components/schemes" import SchemesContainer from "./containers/schemes" import * as JSONSchemaComponents from "./components/json-schema-components" import { ModelExtensions } from "./components/model-extensions" -import { schemaHasType } from "./fn" +import { hasSchemaType } from "./fn" const JSONSchema5Plugin = () => ({ components: { @@ -33,7 +33,7 @@ const JSONSchema5Plugin = () => ({ ...JSONSchemaComponents, }, fn: { - schemaHasType, + hasSchemaType, }, }) diff --git a/src/core/plugins/oas3/components/request-body.jsx b/src/core/plugins/oas3/components/request-body.jsx index 2393ac829c1..d7065ee04d0 100644 --- a/src/core/plugins/oas3/components/request-body.jsx +++ b/src/core/plugins/oas3/components/request-body.jsx @@ -129,7 +129,7 @@ const RequestBody = ({ return null } - const isObjectContent = fn.schemaHasType(mediaTypeValue.get("schema"), ["object"]) + const isObjectContent = fn.hasSchemaType(mediaTypeValue.get("schema"), "object") if ( isObjectContent && diff --git a/src/core/plugins/oas3/fn.js b/src/core/plugins/oas3/fn.js index 2f7d3a75d3a..a063e4864c7 100644 --- a/src/core/plugins/oas3/fn.js +++ b/src/core/plugins/oas3/fn.js @@ -27,8 +27,7 @@ export const makeIsFileUploadIntended = (getSystem) => { const format = isSchemaImmutable ? schema.get("format") : schema.format return ( - fn.schemaHasType(schema, ["string"]) && - ["binary", "byte"].includes(format) + fn.hasSchemaType(schema, "string") && ["binary", "byte"].includes(format) ) } diff --git a/src/core/plugins/oas31/after-load.js b/src/core/plugins/oas31/after-load.js index 746443c3afc..b7c80032f0f 100644 --- a/src/core/plugins/oas31/after-load.js +++ b/src/core/plugins/oas31/after-load.js @@ -42,20 +42,26 @@ function afterLoad({ fn, getSystem }) { // overrides behavior in OpenAPI 3.1.x, recognizes more intentions const isFileUploadIntended = makeIsFileUploadIntended(getSystem) - const { isFileUploadIntended: isFileUploadIntendedWrap, schemaHasType } = - wrapOAS31Fn( + const { isFileUploadIntended: isFileUploadIntendedWrap } = wrapOAS31Fn( + { + isFileUploadIntended, + }, + getSystem() + ) + + this.fn.isFileUploadIntended = isFileUploadIntendedWrap + this.fn.isFileUploadIntendedOAS31 = isFileUploadIntended + + if (fn.jsonSchema202012) { + const { hasSchemaType } = wrapOAS31Fn( { - isFileUploadIntended, - ...(fn.jsonSchema202012 && { - schemaHasType: fn.jsonSchema202012.schemaHasType, - }), + hasSchemaType: fn.jsonSchema202012.hasSchemaType, }, getSystem() ) - this.fn.isFileUploadIntended = isFileUploadIntendedWrap - this.fn.isFileUploadIntendedOAS31 = isFileUploadIntended - this.fn.schemaHasType = schemaHasType + this.fn.hasSchemaType = hasSchemaType + } } export default afterLoad diff --git a/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx b/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx index 6b58cd239a9..4600beb0784 100644 --- a/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx +++ b/test/unit/core/plugins/json-schema-5/components/json-schema-form.jsx @@ -19,7 +19,7 @@ const getSystemStub = () => ({ fileUploadMediaTypes: [], }), fn: { - schemaHasType: () => {}, + hasSchemaType: () => {}, isFileUploadIntendedOAS30: () => {}, }, })