Skip to content

fix: align OpenAPI 3.x.y file uploads with specification #10409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 11, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,15 @@ 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 ?
getComponentSilently(`JsonSchema_${type}_${format}`) :
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")
}

Expand Down
26 changes: 11 additions & 15 deletions src/core/plugins/oas3/components/request-body.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -132,6 +121,13 @@ const RequestBody = ({
return <Input type={"file"} onChange={handleFile} />
}


if (!mediaTypeValue.size) {
return null
}

const isObjectContent = mediaTypeValue.getIn(["schema", "type"]) === "object"

if (
isObjectContent &&
(
Expand Down Expand Up @@ -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 = <JsonSchemaForm
fn={fn}
dispatchInitialValue={!isFile}
dispatchInitialValue={!isFileUploadIntended}
schema={schema}
description={key}
getComponent={getComponent}
Expand Down
22 changes: 22 additions & 0 deletions src/core/plugins/oas3/fn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @prettier
*/

export const isFileUploadIntended = (schema, mediaType = null) => {
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")
}
2 changes: 2 additions & 0 deletions src/core/plugins/oas3/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -28,5 +29,6 @@ export default function () {
selectors: { ...selectors },
},
},
fn,
}
}
8 changes: 4 additions & 4 deletions src/core/plugins/oas3/wrap-components/json-schema-string.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Input type="file"
className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
Expand Down
8 changes: 8 additions & 0 deletions src/core/plugins/oas31/after-load.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getProperties,
} from "./json-schema-2020-12-extensions/fn"
import { wrapOAS31Fn } from "./fn"
import { isFileUploadIntended } from "./oas3-extensions/fn"

function afterLoad({ fn, getSystem }) {
// overrides for fn.jsonSchema202012
Expand Down Expand Up @@ -38,6 +39,13 @@ function afterLoad({ fn, getSystem }) {

Object.assign(this.fn, wrappedFns)
}

const { isFileUploadIntended: isFileUploadIntendedWrap } = wrapOAS31Fn(
{ isFileUploadIntended },
getSystem()
)

this.fn.isFileUploadIntended = isFileUploadIntendedWrap
}

export default afterLoad
25 changes: 25 additions & 0 deletions src/core/plugins/oas31/oas3-extensions/fn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @prettier
*/
import { immutableToJS } from "core/utils"

export const isFileUploadIntended = (schema, mediaType = null) => {
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")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/**
* @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("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()
})

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")
})
})
})
Loading