Skip to content

Commit

Permalink
Merge pull request #1305 from hey-api/fix/experimental-parser-content…
Browse files Browse the repository at this point in the history
…-file-no-schema

fix: handle file-like content media type without explicit schema
  • Loading branch information
mrlubos authored Nov 18, 2024
2 parents c3d6655 + a3698a7 commit 59b84a0
Show file tree
Hide file tree
Showing 17 changed files with 291 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/many-tomatoes-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hey-api/openapi-ts': patch
---

fix: handle file-like content media type without explicit schema
70 changes: 70 additions & 0 deletions packages/openapi-ts/src/ir/__tests__/mediaType.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, it } from 'vitest';

import { isMediaTypeFileLike } from '../mediaType';

describe('isMediaTypeFileLike', () => {
const scenarios: Array<{
fileLike: ReturnType<typeof isMediaTypeFileLike>;
mediaType: Parameters<typeof isMediaTypeFileLike>[0]['mediaType'];
}> = [
{
fileLike: false,
mediaType: 'application/json',
},
{
fileLike: true,
mediaType: 'application/json+download',
},
{
fileLike: false,
mediaType: 'application/json; charset=ascii',
},
{
fileLike: true,
mediaType: 'application/octet-stream',
},
{
fileLike: true,
mediaType: 'application/pdf',
},
{
fileLike: true,
mediaType: 'application/xml; charset=utf-8',
},
{
fileLike: true,
mediaType: 'application/zip',
},
{
fileLike: false,
mediaType: 'image/jpeg',
},
{
fileLike: false,
mediaType: 'image/jpeg; charset=utf-8',
},
{
fileLike: false,
mediaType: 'text/html; charset=utf-8',
},
{
fileLike: true,
mediaType: 'text/javascript; charset=ISO-8859-1',
},
{
fileLike: true,
mediaType: 'text/plain; charset=utf-8',
},
{
fileLike: true,
mediaType: 'video/mp4',
},
];

it.each(scenarios)(
'detects $mediaType as file-like? $fileLike',
async ({ mediaType, fileLike }) => {
expect(isMediaTypeFileLike({ mediaType })).toEqual(fileLike);
},
);
});
1 change: 1 addition & 0 deletions packages/openapi-ts/src/ir/ir.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export interface IRResponsesObject {

export interface IRResponseObject {
// TODO: parser - handle headers, links, and possibly other media types?
mediaType?: string;
schema: IRSchemaObject;
}

Expand Down
11 changes: 11 additions & 0 deletions packages/openapi-ts/src/ir/mediaType.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
const fileLikeRegExp =
/^(application\/(pdf|rtf|msword|vnd\.(ms-|openxmlformats-officedocument\.)|zip|x-(7z|tar|rar|zip|iso)|octet-stream|gzip|x-msdownload|json\+download|xml|x-yaml|x-7z-compressed|x-tar)|text\/(plain|yaml|css|javascript)|audio\/(mpeg|wav)|video\/(mp4|x-matroska)|image\/(vnd\.adobe\.photoshop|svg\+xml))(; ?charset=[^;]+)?$/i;
const jsonMimeRegExp = /^application\/(.*\+)?json(;.*)?$/i;
const multipartFormDataMimeRegExp = /^multipart\/form-data(;.*)?$/i;
const xWwwFormUrlEncodedMimeRegExp =
/^application\/x-www-form-urlencoded(;.*)?$/i;

export type IRMediaType = 'form-data' | 'json' | 'url-search-params';

export const isMediaTypeFileLike = ({
mediaType,
}: {
mediaType: string;
}): boolean => {
fileLikeRegExp.lastIndex = 0;
return fileLikeRegExp.test(mediaType);
};

export const mediaTypeToIrMediaType = ({
mediaType,
}: {
Expand Down
42 changes: 41 additions & 1 deletion packages/openapi-ts/src/openApi/3.0.x/parser/mediaType.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { IRMediaType } from '../../../ir/mediaType';
import { mediaTypeToIrMediaType } from '../../../ir/mediaType';
import {
isMediaTypeFileLike,
mediaTypeToIrMediaType,
} from '../../../ir/mediaType';
import type {
MediaTypeObject,
ReferenceObject,
Expand All @@ -12,6 +15,43 @@ interface Content {
type: IRMediaType | undefined;
}

export const contentToSchema = ({
content,
}: {
content: Content;
}): SchemaObject | undefined => {
const { mediaType, schema } = content;

if (schema && '$ref' in schema) {
return {
allOf: [{ ...schema }],
};
}

if (!schema) {
if (isMediaTypeFileLike({ mediaType })) {
return {
format: 'binary',
type: 'string',
};
}
return;
}

if (
schema.type === 'string' &&
!schema.format &&
isMediaTypeFileLike({ mediaType })
) {
return {
...schema,
format: 'binary',
};
}

return schema;
};

export const mediaTypeObject = ({
content,
}: {
Expand Down
17 changes: 6 additions & 11 deletions packages/openapi-ts/src/openApi/3.0.x/parser/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
ResponseObject,
SchemaObject,
} from '../types/spec';
import { mediaTypeObject } from './mediaType';
import { contentToSchema, mediaTypeObject } from './mediaType';
import { paginationField } from './pagination';
import { schemaToIrSchema } from './schema';

Expand Down Expand Up @@ -134,18 +134,13 @@ const operationToIrOperation = ({

if (content) {
irOperation.responses[name] = {
mediaType: content.mediaType,
schema: schemaToIrSchema({
context,
schema:
content.schema && '$ref' in content.schema
? {
allOf: [{ ...content.schema }],
description: responseObject.description,
}
: {
description: responseObject.description,
...content.schema,
},
schema: {
description: responseObject.description,
...contentToSchema({ content }),
},
}),
};
} else {
Expand Down
36 changes: 35 additions & 1 deletion packages/openapi-ts/src/openApi/3.1.x/parser/mediaType.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { IRMediaType } from '../../../ir/mediaType';
import { mediaTypeToIrMediaType } from '../../../ir/mediaType';
import {
isMediaTypeFileLike,
mediaTypeToIrMediaType,
} from '../../../ir/mediaType';
import type { MediaTypeObject, SchemaObject } from '../types/spec';

interface Content {
Expand All @@ -8,6 +11,37 @@ interface Content {
type: IRMediaType | undefined;
}

export const contentToSchema = ({
content,
}: {
content: Content;
}): SchemaObject | undefined => {
const { mediaType, schema } = content;

if (!schema) {
if (isMediaTypeFileLike({ mediaType })) {
return {
format: 'binary',
type: 'string',
};
}
return;
}

if (
schema.type === 'string' &&
!schema.format &&
isMediaTypeFileLike({ mediaType })
) {
return {
...schema,
format: 'binary',
};
}

return schema;
};

export const mediaTypeObject = ({
content,
}: {
Expand Down
5 changes: 3 additions & 2 deletions packages/openapi-ts/src/openApi/3.1.x/parser/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
ResponseObject,
SchemaObject,
} from '../types/spec';
import { mediaTypeObject } from './mediaType';
import { contentToSchema, mediaTypeObject } from './mediaType';
import { paginationField } from './pagination';
import { schemaToIrSchema } from './schema';

Expand Down Expand Up @@ -128,11 +128,12 @@ const operationToIrOperation = ({

if (content) {
irOperation.responses[name] = {
mediaType: content.mediaType,
schema: schemaToIrSchema({
context,
schema: {
description: responseObject.description,
...content.schema,
...contentToSchema({ content }),
},
}),
};
Expand Down
7 changes: 7 additions & 0 deletions packages/openapi-ts/test/3.0.x.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ describe(`OpenAPI ${VERSION}`, () => {
description:
'generates correct array when items are oneOf array with single item',
},
{
config: createConfig({
input: 'content-binary.json',
output: 'content-binary',
}),
description: 'handles binary content',
},
{
config: createConfig({
input: 'discriminator-all-of.yaml',
Expand Down
7 changes: 7 additions & 0 deletions packages/openapi-ts/test/3.1.x.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ describe(`OpenAPI ${VERSION}`, () => {
description:
'generates correct array when items are oneOf array with single item',
},
{
config: createConfig({
input: 'content-binary.json',
output: 'content-binary',
}),
description: 'handles binary content',
},
{
config: createConfig({
input: 'discriminator-all-of.yaml',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './types.gen';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// This file is auto-generated by @hey-api/openapi-ts

export type GetFooResponses = {
200: string;
};

export type GetFooResponse = GetFooResponses[keyof GetFooResponses];

export type GetBarResponses = {
200: Blob | File;
};

export type GetBarResponse = GetBarResponses[keyof GetBarResponses];
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './types.gen';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// This file is auto-generated by @hey-api/openapi-ts

export type GetFooResponses = {
200: string;
};

export type GetFooResponse = GetFooResponses[keyof GetFooResponses];

export type GetBarResponses = {
200: Blob | File;
};

export type GetBarResponse = GetBarResponses[keyof GetBarResponses];
2 changes: 1 addition & 1 deletion packages/openapi-ts/test/sample.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const main = async () => {
input: {
// include:
// '^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$',
path: './test/spec/3.0.x/full.json',
path: './test/spec/3.0.x/content-binary.json',
// path: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json',
},
// name: 'foo',
Expand Down
37 changes: 37 additions & 0 deletions packages/openapi-ts/test/spec/3.0.x/content-binary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"openapi": "3.0.0",
"info": {
"title": "OpenAPI 3.0.0 content binary example",
"version": "1"
},
"paths": {
"/foo": {
"get": {
"responses": {
"200": {
"content": {
"image/png": {
"schema": {
"type": "string",
"contentMediaType": "image/png",
"contentEncoding": "base64"
}
}
}
}
}
}
},
"/bar": {
"get": {
"responses": {
"200": {
"content": {
"application/zip": {}
}
}
}
}
}
}
}
Loading

0 comments on commit 59b84a0

Please sign in to comment.