Skip to content

Commit 41bdc18

Browse files
committed
fix: format json + extractResponseBody flag fixes
1 parent a625cc9 commit 41bdc18

7 files changed

Lines changed: 1554 additions & 6 deletions

File tree

.changeset/easy-hats-try.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"swagger-typescript-api": patch
3+
---
4+
5+
fixed convertation format: json\blob for responses with extractResponseBody flag

.changeset/fresh-files-try.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"swagger-typescript-api": patch
3+
---
4+
5+
fix `contentTypes` internal field for route

src/schema-routes/schema-routes.ts

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { consola } from "consola";
2-
import { typeGuard } from "yummies/type-guard";
3-
import type { AnyObject } from "yummies/types";
42
import { compact, flattenDeep, isEqual, mapValues, uniq } from "es-toolkit";
53
import { camelCase, get, reduce } from "es-toolkit/compat";
4+
import { typeGuard } from "yummies/type-guard";
5+
import type { AnyObject } from "yummies/types";
66
import type {
77
GenerateApiConfiguration,
88
ParsedRoute,
@@ -431,7 +431,8 @@ export class SchemaRoutes {
431431
const result: any[] = [];
432432

433433
for (const [status, requestInfo] of Object.entries(requestInfos || {})) {
434-
const contentTypes = this.getContentTypes([requestInfo], operationId);
434+
// content types are derived from response `content` keys; never mix in operationId
435+
const contentTypes = this.getContentTypes([requestInfo]);
435436
const links = this.getRouteLinksFromResponse(
436437
resolvedSwaggerSchema,
437438
requestInfo,
@@ -848,10 +849,80 @@ export class SchemaRoutes {
848849
);
849850

850851
const successResponse = responseBodyInfo.success;
852+
const contentKind = successResponse.schema?.contentKind;
853+
const actualSchema = this.getSchemaFromRequestType(
854+
successResponse.schema,
855+
);
851856

852-
if (successResponse.schema && !successResponse.schema.$ref) {
853-
const contentKind = successResponse.schema.contentKind;
854-
const schema = this.getSchemaFromRequestType(successResponse.schema);
857+
if (actualSchema && !actualSchema.$ref) {
858+
successResponse.schema = this.schemaParserFabric.createParsedComponent({
859+
schema: actualSchema,
860+
typeName,
861+
schemaPath: [routeInfo.operationId],
862+
});
863+
successResponse.schema.contentKind = contentKind;
864+
if (successResponse.schema.typeData) {
865+
successResponse.schema.typeData.isExtractedResponseBody = true;
866+
}
867+
successResponse.type = this.schemaParserFabric.getInlineParseContent({
868+
$ref: successResponse.schema.$ref,
869+
});
870+
871+
if (idx > -1) {
872+
Object.assign(responseBodyInfo.responses[idx], {
873+
...successResponse.schema,
874+
type: successResponse.type,
875+
});
876+
}
877+
} else if (responseBodyInfo.success.isBinary) {
878+
/* Binary response with $ref or OAS3 content: emit type alias GetXxxData = Blob and use Blob in route (same as isBinarySuccessType in getResponseBodyInfo). */
879+
const blobSchema = { type: "string", format: "byte" };
880+
successResponse.schema = this.schemaParserFabric.createParsedComponent({
881+
schema: blobSchema,
882+
typeName,
883+
schemaPath: [routeInfo.operationId],
884+
});
885+
successResponse.schema.contentKind = contentKind;
886+
if (successResponse.schema.typeData) {
887+
successResponse.schema.typeData.isExtractedResponseBody = true;
888+
}
889+
successResponse.type = this.config.Ts.Keyword.Blob;
890+
891+
if (idx > -1) {
892+
Object.assign(responseBodyInfo.responses[idx], {
893+
...successResponse.schema,
894+
type: successResponse.type,
895+
});
896+
}
897+
} else if (actualSchema?.$ref) {
898+
/* Non-binary response with $ref: emit type alias GetXxxData = RefType (e.g. GetPetByIdData = Pet). */
899+
successResponse.schema = this.schemaParserFabric.createParsedComponent({
900+
schema: actualSchema,
901+
typeName,
902+
schemaPath: [routeInfo.operationId],
903+
});
904+
successResponse.schema.contentKind = contentKind;
905+
if (successResponse.schema.typeData) {
906+
successResponse.schema.typeData.isExtractedResponseBody = true;
907+
}
908+
successResponse.type = this.schemaParserFabric.getInlineParseContent({
909+
$ref: successResponse.schema.$ref,
910+
});
911+
912+
if (idx > -1) {
913+
Object.assign(responseBodyInfo.responses[idx], {
914+
...successResponse.schema,
915+
type: successResponse.type,
916+
});
917+
}
918+
} else if (
919+
successResponse.schema &&
920+
actualSchema === null &&
921+
(responseBodyInfo.success.type === this.config.Ts.Keyword.Any ||
922+
responseBodyInfo.success.type === this.config.defaultResponseType)
923+
) {
924+
/* Response with no content schema and type Any/void (e.g. form-url-encoded): preserve legacy extracted type alias (= any). When actualSchema is null but type is a real type (e.g. $ref to components/responses), skip so route keeps the correct type from getResponseBodyInfo. */
925+
const schema = {};
855926
successResponse.schema = this.schemaParserFabric.createParsedComponent({
856927
schema,
857928
typeName,
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { describe, expect, test } from "vitest";
2+
import { CodeGenConfig } from "../src/configuration.js";
3+
import { SchemaRoutes } from "../src/schema-routes/schema-routes.js";
4+
5+
function createSchemaRoutesWithExtractMocks(overrides: {
6+
getSchemaFromRequestType?: (schema: unknown) => { $ref?: string } | null;
7+
createParsedComponentMock?: (opts: { schema: unknown; typeName: string }) => {
8+
$ref: string;
9+
contentKind?: string;
10+
typeData?: { isExtractedResponseBody?: boolean };
11+
};
12+
getInlineParseContent?: (ref: { $ref: string }) => string;
13+
}) {
14+
const config = new CodeGenConfig({ extractResponseBody: true });
15+
const createParsedComponentMock =
16+
overrides.createParsedComponentMock ?? (() => ({}));
17+
const getInlineParseContent =
18+
overrides.getInlineParseContent ?? ((ref: { $ref: string }) => ref.$ref);
19+
20+
const schemaParserFabric = {
21+
schemaUtils: {
22+
getSchemaType: () => "string",
23+
safeAddNullToType: (_: unknown, type: unknown) => type,
24+
resolveTypeName: (_usage: string, opts: { suffixes?: string[] }) =>
25+
`GetExport${(opts.suffixes ?? ["Data"])[0]}`,
26+
},
27+
schemaFormatters: { formatDescription: (v: string) => v },
28+
createParsedComponent(opts: { schema: unknown; typeName: string }) {
29+
const out = createParsedComponentMock(opts);
30+
return {
31+
$ref: out.$ref ?? `Components.Schemas.${opts.typeName}`,
32+
contentKind: out.contentKind,
33+
typeData: out.typeData ?? { isExtractedResponseBody: true },
34+
};
35+
},
36+
getInlineParseContent(ref: { $ref: string }) {
37+
return getInlineParseContent(ref);
38+
},
39+
} as any;
40+
41+
const schemaRoutes = new SchemaRoutes(
42+
config,
43+
schemaParserFabric,
44+
{} as any,
45+
{} as any,
46+
{} as any,
47+
) as any;
48+
49+
if (overrides.getSchemaFromRequestType) {
50+
schemaRoutes.getSchemaFromRequestType = overrides.getSchemaFromRequestType;
51+
}
52+
53+
return { schemaRoutes, config };
54+
}
55+
56+
describe("SchemaRoutes contentTypes", () => {
57+
test("getRequestInfoTypes derives contentTypes from response.content keys (not operationId)", () => {
58+
const config = new CodeGenConfig({});
59+
60+
const schemaParserFabric = {
61+
schemaUtils: {
62+
getSchemaType: () => "string",
63+
safeAddNullToType: (_requestInfo: unknown, type: unknown) => type,
64+
},
65+
schemaFormatters: {
66+
formatDescription: (value: string) => value,
67+
},
68+
} as any;
69+
70+
const schemaRoutes = new SchemaRoutes(
71+
config,
72+
schemaParserFabric,
73+
{} as any,
74+
{} as any,
75+
{} as any,
76+
) as any;
77+
78+
// Avoid pulling in full typing/parsing pipeline; not relevant for contentTypes.
79+
schemaRoutes.getTypeFromRequestInfo = () => "T";
80+
81+
const requestInfos = {
82+
"200": {
83+
description: "ok",
84+
content: {
85+
"application/json": { schema: { type: "object" } },
86+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
87+
schema: { type: "string", format: "binary" },
88+
},
89+
},
90+
},
91+
};
92+
93+
const operationId = "getMemoryLeak";
94+
const responseTypes = schemaRoutes.getRequestInfoTypes({
95+
requestInfos,
96+
parsedSchemas: {},
97+
operationId,
98+
defaultType: "any",
99+
resolvedSwaggerSchema: {} as any,
100+
});
101+
102+
expect(responseTypes).toHaveLength(1);
103+
104+
const contentTypes = responseTypes[0]?.contentTypes;
105+
expect(contentTypes).toEqual([
106+
"application/json",
107+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
108+
]);
109+
expect(contentTypes).not.toContain(operationId);
110+
expect(contentTypes).not.toContain("g");
111+
});
112+
});
113+
114+
describe("SchemaRoutes extractResponseBodyIfItNeeded", () => {
115+
test("binary response: sets success.type to Blob and creates extracted alias component", () => {
116+
const { schemaRoutes, config } = createSchemaRoutesWithExtractMocks({});
117+
const responseInfo = { contentKind: "JSON" };
118+
const responseBodyInfo = {
119+
responses: [responseInfo],
120+
success: {
121+
isBinary: true,
122+
schema: responseInfo,
123+
type: "BinaryResponse",
124+
},
125+
};
126+
const routeInfo = { operationId: "getExport" };
127+
const routeName = { usage: "getExport" };
128+
129+
schemaRoutes.extractResponseBodyIfItNeeded(
130+
routeInfo,
131+
responseBodyInfo,
132+
routeName,
133+
);
134+
135+
expect(responseBodyInfo.success.type).toBe(config.Ts.Keyword.Blob);
136+
expect(responseBodyInfo.success.schema).toBeDefined();
137+
expect(
138+
responseBodyInfo.success.schema.typeData?.isExtractedResponseBody,
139+
).toBe(true);
140+
expect(responseBodyInfo.responses[0].type).toBe(config.Ts.Keyword.Blob);
141+
});
142+
143+
test("non-binary response with $ref: creates type alias and sets success.type to alias ref", () => {
144+
const aliasTypeName = "GetPetByIdData";
145+
const { schemaRoutes } = createSchemaRoutesWithExtractMocks({
146+
getInlineParseContent: () => aliasTypeName,
147+
});
148+
const responseInfo = {
149+
content: {
150+
"application/json": {
151+
schema: { $ref: "#/components/schemas/Pet" },
152+
},
153+
},
154+
contentKind: "JSON",
155+
};
156+
const responseBodyInfo = {
157+
responses: [responseInfo],
158+
success: {
159+
isBinary: false,
160+
schema: responseInfo,
161+
type: "Pet",
162+
},
163+
};
164+
const routeInfo = { operationId: "getPetById" };
165+
const routeName = { usage: "getPetById" };
166+
167+
schemaRoutes.extractResponseBodyIfItNeeded(
168+
routeInfo,
169+
responseBodyInfo,
170+
routeName,
171+
);
172+
173+
expect(responseBodyInfo.success.type).toBe(aliasTypeName);
174+
expect(
175+
responseBodyInfo.success.schema.typeData?.isExtractedResponseBody,
176+
).toBe(true);
177+
expect(responseBodyInfo.success.schema.contentKind).toBe("JSON");
178+
expect(responseBodyInfo.responses[0].type).toBe(aliasTypeName);
179+
});
180+
181+
test("response with no content schema and type Any/void: creates alias (= any) and uses it in route", () => {
182+
const aliasTypeName = "SingleFormUrlEncodedData";
183+
const { schemaRoutes, config } = createSchemaRoutesWithExtractMocks({
184+
getSchemaFromRequestType: () => null,
185+
getInlineParseContent: () => aliasTypeName,
186+
});
187+
const responseInfo = {
188+
contentKind: "Form",
189+
/* no content with schema → getSchemaFromRequestType returns null */
190+
};
191+
const responseBodyInfo = {
192+
responses: [responseInfo],
193+
success: {
194+
isBinary: false,
195+
schema: responseInfo,
196+
type: config.Ts.Keyword.Any,
197+
},
198+
};
199+
const routeInfo = { operationId: "singleFormUrlEncoded" };
200+
const routeName = { usage: "singleFormUrlEncoded" };
201+
202+
schemaRoutes.extractResponseBodyIfItNeeded(
203+
routeInfo,
204+
responseBodyInfo,
205+
routeName,
206+
);
207+
208+
expect(responseBodyInfo.success.type).toBe(aliasTypeName);
209+
expect(
210+
responseBodyInfo.success.schema.typeData?.isExtractedResponseBody,
211+
).toBe(true);
212+
expect(responseBodyInfo.responses[0].type).toBe(aliasTypeName);
213+
});
214+
});

0 commit comments

Comments
 (0)