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

Refs #9278
This commit is contained in:
Oliwia Rogala
2025-04-11 13:43:44 +02:00
committed by GitHub
parent 22adad3a2e
commit c29e7126c9
19 changed files with 812 additions and 200 deletions

View File

@@ -87,6 +87,13 @@ const defaultOptions = Object.freeze({
onComplete: null, onComplete: null,
modelPropertyMacro: null, modelPropertyMacro: null,
parameterMacro: null, parameterMacro: null,
fileUploadMediaTypes: [
"application/octet-stream",
"image/",
"audio/",
"video/",
],
}) })
export default defaultOptions export default defaultOptions

View File

@@ -45,6 +45,10 @@ const mappings = {
docExpansion: { typeCaster: stringTypeCaster }, docExpansion: { typeCaster: stringTypeCaster },
dom_id: { typeCaster: nullableStringTypeCaster }, dom_id: { typeCaster: nullableStringTypeCaster },
domNode: { typeCaster: domNodeTypeCaster }, domNode: { typeCaster: domNodeTypeCaster },
fileUploadMediaTypes: {
typeCaster: arrayTypeCaster,
defaultValue: defaultOptions.fileUploadMediaTypes,
},
filter: { typeCaster: filterTypeCaster }, filter: { typeCaster: filterTypeCaster },
fn: { typeCaster: objectTypeCaster }, fn: { typeCaster: objectTypeCaster },
initialState: { typeCaster: objectTypeCaster }, initialState: { typeCaster: objectTypeCaster },

View File

@@ -1,6 +1,8 @@
/** /**
* @prettier * @prettier
*/ */
import { List, Map } from "immutable"
export const upperFirst = (value) => { export const upperFirst = (value) => {
if (typeof value === "string") { if (typeof value === "string") {
return `${value.charAt(0).toUpperCase()}${value.slice(1)}` return `${value.charAt(0).toUpperCase()}${value.slice(1)}`
@@ -504,3 +506,22 @@ export const makeGetExtensionKeywords = (fnAccessor) => {
return getExtensionKeywords return getExtensionKeywords
} }
export const hasSchemaType = (schema, type) => {
const isSchemaImmutable = Map.isMap(schema)
if (!isSchemaImmutable && !isPlainObject(schema)) {
return false
}
const hasType = (schemaType) =>
type === schemaType || (Array.isArray(type) && type.includes(schemaType))
const schemaType = isSchemaImmutable ? schema.get("type") : schema.type
if (List.isList(schemaType) || Array.isArray(schemaType)) {
return schemaType.some((t) => hasType(t))
}
return hasType(schemaType)
}

View File

@@ -55,6 +55,7 @@ import {
isBooleanJSONSchema, isBooleanJSONSchema,
getSchemaKeywords, getSchemaKeywords,
makeGetExtensionKeywords, makeGetExtensionKeywords,
hasSchemaType,
} from "./fn" } from "./fn"
import { JSONSchemaPathContext, JSONSchemaLevelContext } from "./context" import { JSONSchemaPathContext, JSONSchemaLevelContext } from "./context"
import { import {
@@ -143,6 +144,7 @@ const JSONSchema202012Plugin = ({ getSystem, fn }) => {
useLevel, useLevel,
getSchemaKeywords, getSchemaKeywords,
getExtensionKeywords: makeGetExtensionKeywords(fnAccessor), getExtensionKeywords: makeGetExtensionKeywords(fnAccessor),
hasSchemaType,
}, },
}, },
} }

View File

@@ -51,6 +51,7 @@ export class JsonSchemaForm extends Component {
const format = schema && schema.get ? schema.get("format") : null const format = schema && schema.get ? schema.get("format") : null
const type = schema && schema.get ? schema.get("type") : null const type = schema && schema.get ? schema.get("type") : null
const foldedType = fn.jsonSchema202012.foldType(immutableToJS(type)) const foldedType = fn.jsonSchema202012.foldType(immutableToJS(type))
const isFileUploadIntended = fn.isFileUploadIntended(schema)
let getComponentSilently = (name) => getComponent(name, false, { failSilently: true }) let getComponentSilently = (name) => getComponent(name, false, { failSilently: true })
let Comp = type ? format ? let Comp = type ? format ?
@@ -58,7 +59,7 @@ export class JsonSchemaForm extends Component {
getComponentSilently(`JsonSchema_${type}`) : getComponentSilently(`JsonSchema_${type}`) :
getComponent("JsonSchema_string") getComponent("JsonSchema_string")
if (List.isList(type) && (foldedType === "array" || foldedType === "object")) { if (!isFileUploadIntended && List.isList(type) && (foldedType === "array" || foldedType === "object")) {
Comp = getComponent("JsonSchema_object") Comp = getComponent("JsonSchema_object")
} }

View File

@@ -0,0 +1,19 @@
/**
* @prettier
*/
import { Map } from "immutable"
import isPlainObject from "lodash/isPlainObject"
export const hasSchemaType = (schema, type) => {
const isSchemaImmutable = Map.isMap(schema)
if (!isSchemaImmutable && !isPlainObject(schema)) {
return false
}
const schemaType = isSchemaImmutable ? schema.get("type") : schema.type
return (
type === schemaType || (Array.isArray(type) && type.includes(schemaType))
)
}

View File

@@ -14,6 +14,7 @@ import Schemes from "./components/schemes"
import SchemesContainer from "./containers/schemes" import SchemesContainer from "./containers/schemes"
import * as JSONSchemaComponents from "./components/json-schema-components" import * as JSONSchemaComponents from "./components/json-schema-components"
import { ModelExtensions } from "./components/model-extensions" import { ModelExtensions } from "./components/model-extensions"
import { hasSchemaType } from "./fn"
const JSONSchema5Plugin = () => ({ const JSONSchema5Plugin = () => ({
components: { components: {
@@ -31,6 +32,9 @@ const JSONSchema5Plugin = () => ({
SchemesContainer, SchemesContainer,
...JSONSchemaComponents, ...JSONSchemaComponents,
}, },
fn: {
hasSchemaType,
},
}) })
export default JSONSchema5Plugin export default JSONSchema5Plugin

View File

@@ -105,21 +105,13 @@ const RequestBody = ({
} }
requestBodyErrors = List.isList(requestBodyErrors) ? requestBodyErrors : List() requestBodyErrors = List.isList(requestBodyErrors) ? requestBodyErrors : List()
if(!mediaTypeValue.size) { const isFileUploadIntended = fn.isFileUploadIntended(
return null mediaTypeValue?.get("schema"),
} contentType
)
const isObjectContent = mediaTypeValue.getIn(["schema", "type"]) === "object"
const isBinaryFormat = mediaTypeValue.getIn(["schema", "format"]) === "binary"
const isBase64Format = mediaTypeValue.getIn(["schema", "format"]) === "base64"
if( if(
contentType === "application/octet-stream" isFileUploadIntended
|| contentType.indexOf("image/") === 0
|| contentType.indexOf("audio/") === 0
|| contentType.indexOf("video/") === 0
|| isBinaryFormat
|| isBase64Format
) { ) {
const Input = getComponent("Input") const Input = getComponent("Input")
@@ -132,6 +124,13 @@ const RequestBody = ({
return <Input type={"file"} onChange={handleFile} /> return <Input type={"file"} onChange={handleFile} />
} }
if (!mediaTypeValue.size) {
return null
}
const isObjectContent = fn.hasSchemaType(mediaTypeValue.get("schema"), "object")
if ( if (
isObjectContent && isObjectContent &&
( (
@@ -190,11 +189,11 @@ const RequestBody = ({
initialValue = JSON.parse(initialValue) initialValue = JSON.parse(initialValue)
} }
const isFile = type === "string" && (format === "binary" || format === "base64") const isFileUploadIntended = fn.isFileUploadIntended(schema)
const jsonSchemaForm = <JsonSchemaForm const jsonSchemaForm = <JsonSchemaForm
fn={fn} fn={fn}
dispatchInitialValue={!isFile} dispatchInitialValue={!isFileUploadIntended}
schema={schema} schema={schema}
description={key} description={key}
getComponent={getComponent} getComponent={getComponent}

View File

@@ -0,0 +1,35 @@
/**
* @prettier
*/
import { Map } from "immutable"
import isPlainObject from "lodash/isPlainObject"
export const makeIsFileUploadIntended = (getSystem) => {
const isFileUploadIntended = (schema, mediaType = null) => {
const { getConfigs, fn } = getSystem()
const { fileUploadMediaTypes } = getConfigs()
const isFileUploadMediaType =
typeof mediaType === "string" &&
fileUploadMediaTypes.some((fileUploadMediaType) =>
mediaType.startsWith(fileUploadMediaType)
)
if (isFileUploadMediaType) {
return true
}
const isSchemaImmutable = Map.isMap(schema)
if (!isSchemaImmutable && !isPlainObject(schema)) {
return false
}
const format = isSchemaImmutable ? schema.get("format") : schema.format
return (
fn.hasSchemaType(schema, "string") && ["binary", "byte"].includes(format)
)
}
return isFileUploadIntended
}

View File

@@ -9,8 +9,11 @@ import wrapComponents from "./wrap-components"
import * as actions from "./actions" import * as actions from "./actions"
import * as selectors from "./selectors" import * as selectors from "./selectors"
import reducers from "./reducers" import reducers from "./reducers"
import { makeIsFileUploadIntended } from "./fn"
export default function ({ getSystem }) {
const isFileUploadIntended = makeIsFileUploadIntended(getSystem)
export default function () {
return { return {
components, components,
wrapComponents, wrapComponents,
@@ -28,5 +31,9 @@ export default function () {
selectors: { ...selectors }, selectors: { ...selectors },
}, },
}, },
fn: {
isFileUploadIntended,
isFileUploadIntendedOAS30: isFileUploadIntended,
},
} }
} }

View File

@@ -6,14 +6,14 @@ export default OAS3ComponentWrapFactory(({ Ori, ...props }) => {
schema, schema,
getComponent, getComponent,
errors, errors,
onChange onChange,
fn
} = props } = props
const format = schema && schema.get ? schema.get("format") : null const isFileUploadIntended = fn.isFileUploadIntended(schema)
const type = schema && schema.get ? schema.get("type") : null
const Input = getComponent("Input") const Input = getComponent("Input")
if(type && type === "string" && (format && (format === "binary" || format === "base64"))) { if (isFileUploadIntended) {
return <Input type="file" return <Input type="file"
className={ errors.length ? "invalid" : ""} className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""} title={ errors.length ? errors : ""}

View File

@@ -6,6 +6,7 @@ import {
getProperties, getProperties,
} from "./json-schema-2020-12-extensions/fn" } from "./json-schema-2020-12-extensions/fn"
import { wrapOAS31Fn } from "./fn" import { wrapOAS31Fn } from "./fn"
import { makeIsFileUploadIntended } from "./oas3-extensions/fn"
function afterLoad({ fn, getSystem }) { function afterLoad({ fn, getSystem }) {
// overrides for fn.jsonSchema202012 // overrides for fn.jsonSchema202012
@@ -38,6 +39,29 @@ function afterLoad({ fn, getSystem }) {
Object.assign(this.fn, wrappedFns) Object.assign(this.fn, wrappedFns)
} }
// overrides behavior in OpenAPI 3.1.x, recognizes more intentions
const isFileUploadIntended = makeIsFileUploadIntended(getSystem)
const { isFileUploadIntended: isFileUploadIntendedWrap } = wrapOAS31Fn(
{
isFileUploadIntended,
},
getSystem()
)
this.fn.isFileUploadIntended = isFileUploadIntendedWrap
this.fn.isFileUploadIntendedOAS31 = isFileUploadIntended
if (fn.jsonSchema202012) {
const { hasSchemaType } = wrapOAS31Fn(
{
hasSchemaType: fn.jsonSchema202012.hasSchemaType,
},
getSystem()
)
this.fn.hasSchemaType = hasSchemaType
}
} }
export default afterLoad export default afterLoad

View File

@@ -0,0 +1,13 @@
/**
* @prettier
*/
export const makeIsFileUploadIntended = (getSystem) => {
const isFileUploadIntended = (schema, mediaType = null) => {
const { fn } = getSystem()
return fn.isFileUploadIntendedOAS30(schema, mediaType)
}
return isFileUploadIntended
}

View File

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

View File

@@ -0,0 +1,257 @@
/**
* @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("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()
})
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")
})
})
})

View File

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

View File

@@ -7,8 +7,11 @@ info:
* `audio/*` content type (no matter what schema format) * `audio/*` content type (no matter what schema format)
* `image/*` content type (no matter what schema format) * `image/*` content type (no matter what schema format)
* `video/*` content type (no matter what schema format) * `video/*` content type (no matter what schema format)
* schema format is `base64` (no matter what content type) * `application/octect-stream` content type with empty Media Type Object
* 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" version: "1.0.0"
paths: paths:
/upload-application-octet-stream: /upload-application-octet-stream:
@@ -19,13 +22,6 @@ paths:
application/octet-stream: application/octet-stream:
schema: schema:
type: string type: string
responses:
'200':
description: successful operation
content:
text/plain:
schema:
type: string
/upload-image-png: /upload-image-png:
post: post:
operationId: uploadImagePng operationId: uploadImagePng
@@ -34,13 +30,6 @@ paths:
image/png: image/png:
schema: schema:
type: string type: string
responses:
'200':
description: successful operation
content:
text/plain:
schema:
type: string
/upload-audio-wav: /upload-audio-wav:
post: post:
operationId: uploadAudioWav operationId: uploadAudioWav
@@ -49,13 +38,6 @@ paths:
audio/wav: audio/wav:
schema: schema:
type: string type: string
responses:
'200':
description: successful operation
content:
text/plain:
schema:
type: string
/upload-video-mpeg: /upload-video-mpeg:
post: post:
operationId: uploadVideoMpeg operationId: uploadVideoMpeg
@@ -64,13 +46,12 @@ paths:
video/mpeg: video/mpeg:
schema: schema:
type: string type: string
responses: /upload-application-octet-stream-empty:
'200': post:
description: successful operation operationId: uploadApplicationOctetStreamEmpty
requestBody:
content: content:
text/plain: application/octet-stream: {}
schema:
type: string
/upload-schema-format-binary: /upload-schema-format-binary:
post: post:
operationId: uploadSchemaFormatBinary operationId: uploadSchemaFormatBinary
@@ -80,26 +61,36 @@ paths:
schema: schema:
type: string type: string
format: binary format: binary
responses: /upload-schema-format-byte:
'200':
description: successful operation
content:
text/plain:
schema:
type: string
/upload-schema-format-base64:
post: post:
operationId: uploadSchemaFormatBase64 operationId: uploadSchemaFormatByte
requestBody: requestBody:
content: content:
application/x-custom: application/x-custom:
schema: schema:
type: string type: string
format: base64 format: byte
responses: /upload-property-schema-format-binary:
'200': post:
description: successful operation operationId: uploadPropertySchemaFormatBinary
requestBody:
content: content:
text/plain: multipart/form-data:
schema: schema:
type: object
properties:
file:
type: string 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

View File

@@ -0,0 +1,150 @@
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)
* `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)
* 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-application-octet-stream-empty:
post:
operationId: uploadApplicationOctetStreamEmpty
requestBody:
content:
application/octet-stream: {}
/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

View File

@@ -4,6 +4,7 @@ import { Select, Input, TextArea } from "core/components/layout-utils"
import { mount, render } from "enzyme" import { mount, render } from "enzyme"
import * as JsonSchemaComponents from "core/plugins/json-schema-5/components/json-schema-components" 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 { foldType } from "core/plugins/json-schema-2020-12-samples/fn/index"
import { makeIsFileUploadIntended } from "core/plugins/oas3/fn"
const components = {...JsonSchemaComponents, Select, Input, TextArea} const components = {...JsonSchemaComponents, Select, Input, TextArea}
@@ -13,6 +14,16 @@ const getComponentStub = (name) => {
return null return null
} }
const getSystemStub = () => ({
getConfigs: () => ({
fileUploadMediaTypes: [],
}),
fn: {
hasSchemaType: () => {},
isFileUploadIntendedOAS30: () => {},
},
})
describe("<JsonSchemaComponents.JsonSchemaForm/>", function(){ describe("<JsonSchemaComponents.JsonSchemaForm/>", function(){
describe("strings", function() { describe("strings", function() {
it("should render the correct options for a string enum parameter", function(){ it("should render the correct options for a string enum parameter", function(){
@@ -26,6 +37,7 @@ describe("<JsonSchemaComponents.JsonSchemaForm/>", function(){
jsonSchema202012: { jsonSchema202012: {
foldType, foldType,
}, },
isFileUploadIntended: makeIsFileUploadIntended(getSystemStub)
}, },
schema: Immutable.fromJS({ schema: Immutable.fromJS({
type: "string", type: "string",
@@ -53,6 +65,7 @@ describe("<JsonSchemaComponents.JsonSchemaForm/>", function(){
jsonSchema202012: { jsonSchema202012: {
foldType, foldType,
}, },
isFileUploadIntended: makeIsFileUploadIntended(getSystemStub)
}, },
schema: Immutable.fromJS({ schema: Immutable.fromJS({
type: "string", type: "string",
@@ -78,6 +91,7 @@ describe("<JsonSchemaComponents.JsonSchemaForm/>", function(){
jsonSchema202012: { jsonSchema202012: {
foldType, foldType,
}, },
isFileUploadIntended: makeIsFileUploadIntended(getSystemStub)
}, },
required: true, required: true,
schema: Immutable.fromJS({ schema: Immutable.fromJS({
@@ -106,6 +120,7 @@ describe("<JsonSchemaComponents.JsonSchemaForm/>", function(){
jsonSchema202012: { jsonSchema202012: {
foldType, foldType,
}, },
isFileUploadIntended: makeIsFileUploadIntended(getSystemStub)
}, },
schema: Immutable.fromJS({ schema: Immutable.fromJS({
type: "boolean" type: "boolean"
@@ -133,6 +148,7 @@ describe("<JsonSchemaComponents.JsonSchemaForm/>", function(){
jsonSchema202012: { jsonSchema202012: {
foldType, foldType,
}, },
isFileUploadIntended: makeIsFileUploadIntended(getSystemStub)
}, },
schema: Immutable.fromJS({ schema: Immutable.fromJS({
type: "boolean", type: "boolean",
@@ -160,6 +176,7 @@ describe("<JsonSchemaComponents.JsonSchemaForm/>", function(){
jsonSchema202012: { jsonSchema202012: {
foldType, foldType,
}, },
isFileUploadIntended: makeIsFileUploadIntended(getSystemStub)
}, },
schema: Immutable.fromJS({ schema: Immutable.fromJS({
type: "boolean", type: "boolean",
@@ -188,6 +205,7 @@ describe("<JsonSchemaComponents.JsonSchemaForm/>", function(){
jsonSchema202012: { jsonSchema202012: {
foldType, foldType,
}, },
isFileUploadIntended: makeIsFileUploadIntended(getSystemStub)
}, },
required: true, required: true,
schema: Immutable.fromJS({ schema: Immutable.fromJS({
@@ -219,6 +237,7 @@ describe("<JsonSchemaComponents.JsonSchemaForm/>", function(){
jsonSchema202012: { jsonSchema202012: {
foldType, foldType,
}, },
isFileUploadIntended: makeIsFileUploadIntended(getSystemStub)
}, },
errors: List(), errors: List(),
schema: Immutable.fromJS({ schema: Immutable.fromJS({
@@ -252,6 +271,7 @@ describe("<JsonSchemaComponents.JsonSchemaForm/>", function(){
jsonSchema202012: { jsonSchema202012: {
foldType, foldType,
}, },
isFileUploadIntended: makeIsFileUploadIntended(getSystemStub)
}, },
schema: Immutable.fromJS({ schema: Immutable.fromJS({
type: "NotARealType" type: "NotARealType"
@@ -278,6 +298,7 @@ describe("<JsonSchemaComponents.JsonSchemaForm/>", function(){
jsonSchema202012: { jsonSchema202012: {
foldType, foldType,
}, },
isFileUploadIntended: makeIsFileUploadIntended(getSystemStub)
}, },
schema: Immutable.fromJS({ schema: Immutable.fromJS({
type: "NotARealType", type: "NotARealType",