Skip to content

Commit

Permalink
fix: treat multipart form as form (#3553)
Browse files Browse the repository at this point in the history
* update convertPackage in ir-to-fdr-converter

* maybe fix multipart form

* stash

* remove duplicate code

* refactor parseFileUploadRequest
  • Loading branch information
abvthecity authored May 9, 2024
1 parent f956db9 commit 93b8bd7
Show file tree
Hide file tree
Showing 14 changed files with 353 additions and 76 deletions.
22 changes: 21 additions & 1 deletion .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file not shown.
9 changes: 6 additions & 3 deletions packages/cli/openapi-ir-to-fern/src/buildEndpoint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FERN_PACKAGE_MARKER_FILENAME } from "@fern-api/configuration";
import { assertNever, MediaType } from "@fern-api/core-utils";
import { RelativeFilePath } from "@fern-api/fs-utils";
import { Endpoint, EndpointAvailability, EndpointExample, Request, Schema, SchemaId } from "@fern-api/openapi-ir-sdk";
import { RawSchemas } from "@fern-api/yaml-schema";
Expand Down Expand Up @@ -425,10 +426,10 @@ function getRequest({
schemaIdsToExclude: [],
value: {
body: "bytes",
"content-type": "application/octet-stream"
"content-type": MediaType.APPLICATION_OCTET_STREAM
}
};
} else {
} else if (request.type === "multipart") {
// multipart
const properties = Object.fromEntries(
request.properties.map((property) => {
Expand All @@ -454,8 +455,10 @@ function getRequest({
body: {
properties
},
"content-type": "multipart/form-data"
"content-type": MediaType.MULTIPART_FORM_DATA
}
};
} else {
assertNever(request);
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import { MediaType } from "@fern-api/core-utils";
import { MultipartRequestProperty, MultipartSchema, RequestWithExample } from "@fern-api/openapi-ir-sdk";
import { OpenAPIV3 } from "openapi-types";
import { convertSchema, getSchemaIdFromReference, SCHEMA_REFERENCE_PREFIX } from "../../../../schema/convertSchemas";
import { isReferenceObject } from "../../../../schema/utils/isReferenceObject";
import { AbstractOpenAPIV3ParserContext } from "../../AbstractOpenAPIV3ParserContext";
import { getApplicationJsonSchemaMediaObject } from "./getApplicationJsonSchema";

export const APPLICATION_JSON_CONTENT = "application/json";
export const APPLICATION_JSON_REGEX = /^application.*json$/;

export const MULTIPART_CONTENT = "multipart/form-data";

export const OCTET_STREAM = "application/octet-stream";

function getMultipartFormDataRequest(
requestBody: OpenAPIV3.RequestBodyObject
): OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject | undefined {
return requestBody.content[MULTIPART_CONTENT]?.schema;
for (const [mediaType, mediaTypeObject] of Object.entries(requestBody.content)) {
if (MediaType.parse(mediaType)?.isMultipart()) {
return mediaTypeObject.schema;
}
}
return undefined;
}

function isOctetStreamRequest(requestBody: OpenAPIV3.RequestBodyObject): boolean {
return requestBody.content[OCTET_STREAM] != null;
for (const mediaType in requestBody.content) {
if (MediaType.parse(mediaType)?.isOctetStream()) {
return true;
}
}
return false;
}

function multipartRequestHasFile(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assertNever } from "@fern-api/core-utils";
import { assertNever, MediaType } from "@fern-api/core-utils";
import { FernOpenapiIr, ResponseWithExample } from "@fern-api/openapi-ir-sdk";
import { OpenAPIV3 } from "openapi-types";
import { getExtension } from "../../../../getExtension";
Expand All @@ -11,11 +11,6 @@ import { OperationContext } from "../contexts";
import { ERROR_NAMES_BY_STATUS_CODE } from "../convertToHttpError";
import { getApplicationJsonSchemaMediaObject, getSchemaMediaObject } from "./getApplicationJsonSchema";

const APPLICATION_OCTET_STREAM_CONTENT = "application/octet-stream";
const APPLICATION_PDF = "application/pdf";
const AUDIO_MPEG = "audio/mpeg";
const TEXT_PLAIN_CONTENT = "text/plain";

// The converter will attempt to get response in priority order
// (i.e. try for 200, then 201, then 204)
const SUCCESSFUL_STATUS_CODES = ["200", "201", "204"];
Expand Down Expand Up @@ -90,10 +85,6 @@ export function convertResponse({
};
}

function isOctetStreamResponse(response: OpenAPIV3.ResponseObject): boolean {
return response?.content?.[APPLICATION_OCTET_STREAM_CONTENT] != null;
}

function convertResolvedResponse({
operationContext,
streamFormat,
Expand Down Expand Up @@ -154,30 +145,35 @@ function convertResolvedResponse({
});
}

if (resolvedResponse.content?.[APPLICATION_OCTET_STREAM_CONTENT] != null) {
return ResponseWithExample.file({ description: resolvedResponse.description });
}

if (resolvedResponse.content?.[APPLICATION_PDF] != null) {
return ResponseWithExample.file({ description: resolvedResponse.description });
}

if (resolvedResponse.content?.[TEXT_PLAIN_CONTENT] != null) {
const textPlainSchema = resolvedResponse.content[TEXT_PLAIN_CONTENT]?.schema;
if (textPlainSchema == null) {
return ResponseWithExample.text({ description: resolvedResponse.description });
for (const [mediaType, mediaObject] of Object.entries(resolvedResponse.content ?? {})) {
const mimeType = MediaType.parse(mediaType);
if (mimeType == null) {
continue;
}
const resolvedTextPlainSchema = isReferenceObject(textPlainSchema)
? context.resolveSchemaReference(textPlainSchema)
: textPlainSchema;
if (resolvedTextPlainSchema.type === "string" && resolvedTextPlainSchema.format === "byte") {

if (
mimeType.isOctetStream() ||
mimeType.isPDF() ||
mimeType.isAudio() ||
mimeType.isImage() ||
mimeType.isVideo()
) {
return ResponseWithExample.file({ description: resolvedResponse.description });
}
return ResponseWithExample.text({ description: resolvedResponse.description });
}

if (resolvedResponse.content?.[AUDIO_MPEG] != null) {
return ResponseWithExample.file({ description: resolvedResponse.description });
if (mimeType.isPlainText()) {
const textPlainSchema = mediaObject.schema;
if (textPlainSchema == null) {
return ResponseWithExample.text({ description: resolvedResponse.description });
}
const resolvedTextPlainSchema = isReferenceObject(textPlainSchema)
? context.resolveSchemaReference(textPlainSchema)
: textPlainSchema;
if (resolvedTextPlainSchema.type === "string" && resolvedTextPlainSchema.format === "byte") {
return ResponseWithExample.file({ description: resolvedResponse.description });
}
return ResponseWithExample.text({ description: resolvedResponse.description });
}
}

return undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assertNever } from "@fern-api/core-utils";
import { assertNever, MediaType } from "@fern-api/core-utils";
import { EndpointWithExample } from "@fern-api/openapi-ir-sdk";
import { OpenAPIV3 } from "openapi-types";
import { getSchemaIdFromReference } from "../../../../schema/convertSchemas";
Expand Down Expand Up @@ -172,7 +172,7 @@ function getRequestBody({
return {
requestBody: {
content: {
"application/json": {
[MediaType.APPLICATION_JSON]: {
schema: requestBodySchemaWithLiteralProperty
}
}
Expand All @@ -193,7 +193,7 @@ function getResponses({
"200": {
description: "",
content: {
"application/json": {
[MediaType.APPLICATION_JSON]: {
schema: response
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MediaType } from "@fern-api/core-utils";
import { OpenAPIV3 } from "openapi-types";
import { APPLICATION_JSON_CONTENT, MULTIPART_CONTENT } from "../../openapi/v3/converters/endpoint/convertRequest";
import { isReferenceObject } from "./isReferenceObject";

export function getReferenceOccurrences(document: OpenAPIV3.Document): Record<string, number> {
Expand Down Expand Up @@ -109,13 +109,24 @@ function removeApplicationJsonAndMultipartConflictsFromOperationObject(
function removeApplicationJsonAndMultipartConflictsFromRequestBody(
requestBody: OpenAPIV3.RequestBodyObject
): OpenAPIV3.RequestBodyObject {
const jsonContent = requestBody.content[APPLICATION_JSON_CONTENT];
const multipartContent = requestBody.content[MULTIPART_CONTENT];
let jsonContent: OpenAPIV3.MediaTypeObject | undefined;
let multipartContent: OpenAPIV3.MediaTypeObject | undefined;
for (const mediatype in requestBody.content) {
const mimetype = MediaType.parse(mediatype);
if (mimetype == null) {
continue;
}
if (mimetype.isJSON()) {
jsonContent = requestBody.content[mediatype];
} else if (mimetype.isMultipart()) {
multipartContent = requestBody.content[mediatype];
}
}
if (multipartContent != null && jsonContent != null) {
return {
...requestBody,
content: {
MULTIPART_CONTENT: multipartContent
[MediaType.MULTIPART_FORM_DATA]: multipartContent
}
};
}
Expand Down
18 changes: 11 additions & 7 deletions packages/cli/register/src/ir-to-fdr-converter/convertPackage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assertNever, isNonNullish, isPlainObject, WithoutQuestionMarks } from "@fern-api/core-utils";
import { assertNever, isNonNullish, isPlainObject, MediaType, WithoutQuestionMarks } from "@fern-api/core-utils";
import { APIV1Write } from "@fern-api/fdr-sdk";
import { ExampleCodeSample, FernIr as Ir } from "@fern-api/ir-sdk";
import { noop, startCase } from "lodash-es";
Expand Down Expand Up @@ -308,7 +308,7 @@ function convertRequestBody(irRequest: Ir.http.HttpRequestBody): APIV1Write.Http
inlinedRequestBody: (inlinedRequestBody) => {
return {
type: "json",
contentType: inlinedRequestBody.contentType ?? "application/json",
contentType: inlinedRequestBody.contentType ?? MediaType.APPLICATION_JSON,
shape: {
type: "object",
extends: inlinedRequestBody.extends.map((extension) => extension.typeId),
Expand All @@ -325,7 +325,7 @@ function convertRequestBody(irRequest: Ir.http.HttpRequestBody): APIV1Write.Http
reference: (reference) => {
return {
type: "json",
contentType: reference.contentType ?? "application/json",
contentType: reference.contentType ?? MediaType.APPLICATION_JSON,
shape: {
type: "reference",
value: convertTypeReference(reference.requestBodyType)
Expand Down Expand Up @@ -553,16 +553,20 @@ function convertHttpEndpointExample(
for (const property of fileUploadSchema.properties) {
property._visit({
file: (file) => {
// TODO: support provided file examples, file arrays
const maybeFile = fullExample[file.key.wireValue];
// TODO: support filename with data in examples
if (file.type === "file") {
value[file.key.wireValue] = {
type: "filename",
value: "<file1>"
value: typeof maybeFile === "string" ? maybeFile : "<file1>"
};
} else if (file.type === "fileArray") {
const filenames = (Array.isArray(maybeFile) ? maybeFile : [maybeFile]).filter(
(filename) => typeof filename === "string"
) as string[];
value[file.key.wireValue] = {
type: "filename",
value: "<file1>"
type: "filenames",
value: filenames
};
}
},
Expand Down
Loading

0 comments on commit 93b8bd7

Please sign in to comment.