@mittwald/api-code-generator 0.0.0-development-860f5d6-20261009 → 0.0.0-development-172be3a-20261013

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/esm/generation/compileJsonSchema.test.js +32 -0
  2. package/dist/esm/generation/deprecation.js +40 -0
  3. package/dist/esm/generation/deprecation.test.js +43 -0
  4. package/dist/esm/generation/format.test.js +46 -0
  5. package/dist/esm/generation/model/CodeGenerationModel.test.js +244 -0
  6. package/dist/esm/generation/model/__fixtures__/petstore.js +152 -0
  7. package/dist/esm/generation/model/components/Parameters.js +7 -1
  8. package/dist/esm/generation/model/global/JSONSchema.js +9 -2
  9. package/dist/esm/generation/model/paths/operation/RequestParameters.js +4 -1
  10. package/dist/esm/generation/model/tags/Tag.test.js +13 -0
  11. package/dist/esm/generation/populateNullableTypes.test.js +153 -0
  12. package/dist/esm/generation/refs/assertNoRefs.test.js +10 -0
  13. package/dist/esm/generation/refs/componentRefsToCustomTypes.js +14 -0
  14. package/dist/esm/generation/refs/componentRefsToCustomTypes.test.js +11 -0
  15. package/dist/esm/generation/refs/extractComponentRefName.test.js +19 -0
  16. package/dist/esm/generation/refs/isRef.test.js +19 -0
  17. package/dist/esm/generation/tsNamespaceName.test.js +13 -0
  18. package/dist/esm/lib/makeError.test.js +36 -0
  19. package/dist/esm/lib/relativePath.test.js +16 -0
  20. package/dist/esm/lib/writeIfChangedAsync.test.js +37 -0
  21. package/dist/esm/loading/UniversalContentLoader.js +4 -2
  22. package/dist/esm/loading/UniversalFileLoader.test.js +50 -0
  23. package/dist/esm/openapi/OpenAPISchemaValidationError.test.js +27 -0
  24. package/dist/esm/openapi/OpenApiSpec.test.js +148 -0
  25. package/dist/types/generation/deprecation.d.ts +27 -0
  26. package/dist/types/generation/deprecation.test.d.ts +1 -0
  27. package/dist/types/generation/format.test.d.ts +1 -0
  28. package/dist/types/generation/model/__fixtures__/petstore.d.ts +12 -0
  29. package/dist/types/generation/model/tags/Tag.test.d.ts +1 -0
  30. package/dist/types/generation/populateNullableTypes.test.d.ts +1 -0
  31. package/dist/types/generation/refs/assertNoRefs.test.d.ts +1 -0
  32. package/dist/types/generation/refs/extractComponentRefName.test.d.ts +1 -0
  33. package/dist/types/generation/refs/isRef.test.d.ts +1 -0
  34. package/dist/types/generation/tsNamespaceName.test.d.ts +1 -0
  35. package/dist/types/lib/makeError.test.d.ts +1 -0
  36. package/dist/types/lib/relativePath.test.d.ts +1 -0
  37. package/dist/types/lib/writeIfChangedAsync.test.d.ts +1 -0
  38. package/dist/types/loading/UniversalFileLoader.test.d.ts +1 -0
  39. package/dist/types/openapi/OpenAPISchemaValidationError.test.d.ts +1 -0
  40. package/package.json +12 -10
@@ -64,3 +64,35 @@ test("const-tagged members compile to a discriminated union", async () => {
64
64
  };
65
65
  `));
66
66
  });
67
+ test("compiles an object schema into an interface", async () => {
68
+ const output = await compileJsonSchema({
69
+ type: "object",
70
+ required: ["id"],
71
+ properties: { id: { type: "string" }, name: { type: "string" } },
72
+ }, "Pet");
73
+ expect(output).toContain("export interface Pet {");
74
+ expect(output).toContain("id: string;");
75
+ expect(output).toContain("name?: string;");
76
+ });
77
+ test("emits no banner comment", async () => {
78
+ const output = await compileJsonSchema({ type: "string" }, "Name");
79
+ expect(output).not.toContain("/* eslint-disable */");
80
+ expect(output.trimStart().startsWith("export")).toBe(true);
81
+ });
82
+ test("disallows additional properties by default", async () => {
83
+ const output = await compileJsonSchema({ type: "object", properties: { id: { type: "string" } } }, "Pet");
84
+ expect(output).not.toContain("[k: string]: unknown");
85
+ });
86
+ test("lets callers override the defaults", async () => {
87
+ const output = await compileJsonSchema({ type: "object", properties: { id: { type: "string" } } }, "Pet", { additionalProperties: true });
88
+ expect(output).toContain("[k: string]: unknown");
89
+ });
90
+ test("compiles a scalar schema into a type alias", async () => {
91
+ const output = await compileJsonSchema({ type: "string" }, "Name");
92
+ expect(output).toContain("export type Name = string;");
93
+ });
94
+ test("compiles an enum into a union", async () => {
95
+ const output = await compileJsonSchema({ type: "string", enum: ["asc", "desc"] }, "Order");
96
+ expect(output).toContain('"asc"');
97
+ expect(output).toContain('"desc"');
98
+ });
@@ -0,0 +1,40 @@
1
+ export const isDeprecated = (doc) => doc?.deprecated === true;
2
+ /**
3
+ * Copies a `deprecated: true` annotation from an OpenAPI object onto the JSON
4
+ * schema that is generated for it. Used where the annotation lives on the
5
+ * OpenAPI object (e.g. a Parameter Object) rather than on its `schema`, because
6
+ * only the schema is handed to the TypeScript compiler.
7
+ */
8
+ export const withDeprecation = (schema, source) => isDeprecated(source) && !isDeprecated(schema)
9
+ ? { ...schema, deprecated: true }
10
+ : schema;
11
+ const jsDocStart = "/**";
12
+ const jsDocEnd = "*/";
13
+ const deprecatedTag = "@deprecated";
14
+ /**
15
+ * `json-schema-to-typescript` emits an `@deprecated` JSDoc tag for interface
16
+ * members and for standalone interfaces, but drops it for standalone type
17
+ * aliases: its `generateStandaloneType` calls the comment generator without the
18
+ * deprecation flag, so `export type Foo = string` ends up with an empty `/**
19
+ * *\/` block. We add the tag back so every generated declaration carries the
20
+ * deprecation.
21
+ */
22
+ export const ensureDeprecatedTypeAliasComment = (compiled, tsTypeName) => {
23
+ const declaration = `export type ${tsTypeName} `;
24
+ const declarationIndex = compiled.indexOf(declaration);
25
+ if (declarationIndex < 0) {
26
+ return compiled;
27
+ }
28
+ const before = compiled.slice(0, declarationIndex);
29
+ const trimmedBefore = before.trimEnd();
30
+ if (!trimmedBefore.endsWith(jsDocEnd)) {
31
+ return `${before}/**\n * ${deprecatedTag}\n */\n${compiled.slice(declarationIndex)}`;
32
+ }
33
+ const commentStart = trimmedBefore.lastIndexOf(jsDocStart);
34
+ if (commentStart < 0 ||
35
+ trimmedBefore.slice(commentStart).includes(deprecatedTag)) {
36
+ return compiled;
37
+ }
38
+ const insertAt = commentStart + jsDocStart.length;
39
+ return `${compiled.slice(0, insertAt)}\n * ${deprecatedTag}${compiled.slice(insertAt)}`;
40
+ };
@@ -0,0 +1,43 @@
1
+ import { ensureDeprecatedTypeAliasComment, isDeprecated, withDeprecation, } from "./deprecation.js";
2
+ test("isDeprecated only reports an explicit true", () => {
3
+ expect(isDeprecated({ deprecated: true })).toBe(true);
4
+ expect(isDeprecated({ deprecated: false })).toBe(false);
5
+ expect(isDeprecated({})).toBe(false);
6
+ expect(isDeprecated(undefined)).toBe(false);
7
+ });
8
+ test("withDeprecation copies the annotation onto the schema", () => {
9
+ const schema = { type: "string" };
10
+ expect(withDeprecation(schema, { deprecated: true })).toEqual({
11
+ type: "string",
12
+ deprecated: true,
13
+ });
14
+ });
15
+ test("withDeprecation leaves the schema untouched when not deprecated", () => {
16
+ const schema = { type: "string" };
17
+ expect(withDeprecation(schema, { deprecated: false })).toBe(schema);
18
+ expect(withDeprecation(schema, undefined)).toBe(schema);
19
+ });
20
+ test("withDeprecation keeps an annotation that is already on the schema", () => {
21
+ const schema = { type: "string", deprecated: true };
22
+ expect(withDeprecation(schema, { deprecated: true })).toBe(schema);
23
+ });
24
+ test("ensureDeprecatedTypeAliasComment adds the tag to an empty comment block", () => {
25
+ const compiled = "/**\n */\nexport type LegacySort = string;\n";
26
+ expect(ensureDeprecatedTypeAliasComment(compiled, "LegacySort")).toBe("/**\n * @deprecated\n */\nexport type LegacySort = string;\n");
27
+ });
28
+ test("ensureDeprecatedTypeAliasComment keeps an existing description", () => {
29
+ const compiled = "/**\n * Old thing\n */\nexport type LegacySort = string;\n";
30
+ expect(ensureDeprecatedTypeAliasComment(compiled, "LegacySort")).toBe("/**\n * @deprecated\n * Old thing\n */\nexport type LegacySort = string;\n");
31
+ });
32
+ test("ensureDeprecatedTypeAliasComment adds a comment block when there is none", () => {
33
+ const compiled = "export type LegacySort = string;\n";
34
+ expect(ensureDeprecatedTypeAliasComment(compiled, "LegacySort")).toBe("/**\n * @deprecated\n */\nexport type LegacySort = string;\n");
35
+ });
36
+ test("ensureDeprecatedTypeAliasComment is idempotent", () => {
37
+ const compiled = "/**\n * @deprecated\n */\nexport type LegacySort = string;\n";
38
+ expect(ensureDeprecatedTypeAliasComment(compiled, "LegacySort")).toBe(compiled);
39
+ });
40
+ test("ensureDeprecatedTypeAliasComment ignores output without a matching alias", () => {
41
+ const compiled = "export interface LegacySort {\n a?: string;\n}\n";
42
+ expect(ensureDeprecatedTypeAliasComment(compiled, "LegacySort")).toBe(compiled);
43
+ });
@@ -0,0 +1,46 @@
1
+ import { format } from "./format.js";
2
+ import { prepareTypeScriptOutput } from "./prepareTypeScriptOutput.js";
3
+ describe("format", () => {
4
+ test("formats TypeScript source", async () => {
5
+ await expect(format("const a={b:1}")).resolves.toBe("const a = { b: 1 };\n");
6
+ });
7
+ test("keeps already formatted source stable", async () => {
8
+ const formatted = await format("const a = 1;\n");
9
+ expect(await format(formatted)).toBe(formatted);
10
+ });
11
+ test("formats type declarations", async () => {
12
+ await expect(format("type A={b:string}")).resolves.toBe("type A = { b: string };\n");
13
+ });
14
+ test("reports a syntax error as a CodeFormattingError", async () => {
15
+ await expect(format("const a = {")).rejects.toMatchObject({
16
+ name: "CodeFormattingError",
17
+ });
18
+ });
19
+ test("points at the likely cause in the error message", async () => {
20
+ await expect(format("const a = {")).rejects.toThrow("Failed to format the generated code");
21
+ });
22
+ test("keeps the underlying prettier error as cause", async () => {
23
+ const error = await format("const a = {").catch((e) => e);
24
+ expect(error.cause()).toBeInstanceOf(Error);
25
+ });
26
+ });
27
+ describe("prepareTypeScriptOutput", () => {
28
+ test("prepends the generated-file header", async () => {
29
+ const output = await prepareTypeScriptOutput("const a=1");
30
+ expect(output).toBe([
31
+ "/* eslint-disable */",
32
+ "/* prettier-ignore */",
33
+ "/* This file is auto-generated with acg (@mittwald/api-code-generator) */",
34
+ "const a = 1;",
35
+ "",
36
+ ].join("\n"));
37
+ });
38
+ test("marks the output so lint and prettier skip it", async () => {
39
+ const output = await prepareTypeScriptOutput("const a=1");
40
+ expect(output).toContain("/* eslint-disable */");
41
+ expect(output).toContain("/* prettier-ignore */");
42
+ });
43
+ test("propagates formatting failures", async () => {
44
+ await expect(prepareTypeScriptOutput("const a = {")).rejects.toThrow("Failed to format the generated code");
45
+ });
46
+ });
@@ -1,5 +1,6 @@
1
1
  import { OpenApiSpec } from "../../openapi/OpenApiSpec.js";
2
2
  import { CodeGenerationModel } from "./CodeGenerationModel.js";
3
+ import { makePetstoreDoc } from "./__fixtures__/petstore.js";
3
4
  const normalize = (typeScript) => typeScript.replace(/\s+/g, " ").trim();
4
5
  /**
5
6
  * End-to-end check for the `const` keyword: an OpenAPI document describing a
@@ -64,3 +65,246 @@ test("const keywords survive from the spec into the generated types", async () =
64
65
  | Api.Components.Schemas.DoneEvent;
65
66
  `));
66
67
  });
68
+ const doc = {
69
+ openapi: "3.0.3",
70
+ info: { title: "Test API", version: "1.0.0" },
71
+ paths: {
72
+ "/conversations": {
73
+ get: {
74
+ operationId: "conversation-list",
75
+ tags: ["conversation"],
76
+ parameters: [
77
+ {
78
+ name: "legacyFilter",
79
+ in: "query",
80
+ deprecated: true,
81
+ schema: { type: "string" },
82
+ },
83
+ { $ref: "#/components/parameters/LegacySort" },
84
+ ],
85
+ responses: {
86
+ "200": {
87
+ description: "OK",
88
+ content: {
89
+ "application/json": {
90
+ schema: { $ref: "#/components/schemas/Conversation" },
91
+ },
92
+ },
93
+ },
94
+ },
95
+ },
96
+ },
97
+ },
98
+ components: {
99
+ parameters: {
100
+ LegacySort: {
101
+ name: "legacySort",
102
+ in: "query",
103
+ deprecated: true,
104
+ schema: { type: "string" },
105
+ },
106
+ },
107
+ schemas: {
108
+ Conversation: {
109
+ type: "object",
110
+ properties: {
111
+ lastMessageAt: {
112
+ type: "string",
113
+ format: "date-time",
114
+ deprecated: true,
115
+ },
116
+ },
117
+ },
118
+ LegacyState: {
119
+ type: "string",
120
+ enum: ["enabled", "disabled"],
121
+ deprecated: true,
122
+ },
123
+ },
124
+ },
125
+ };
126
+ const compileTypes = () => CodeGenerationModel.fromDoc("Api", doc).compileTypes({
127
+ rootNamespace: "Api",
128
+ });
129
+ /** Matches an `@deprecated` JSDoc tag directly in front of `declaration`. */
130
+ const deprecatedBefore = (declaration) => new RegExp(`@deprecated[^;{}]*?\\*/\\s*${declaration.replace(/[$?]/g, "\\$&")}`);
131
+ test("a deprecated schema property is emitted with an @deprecated tag", async () => {
132
+ const types = await compileTypes();
133
+ expect(types).toMatch(deprecatedBefore("lastMessageAt?"));
134
+ });
135
+ test("a deprecated standalone type gets an @deprecated tag", async () => {
136
+ const types = await compileTypes();
137
+ expect(types).toMatch(deprecatedBefore("export type LegacyState"));
138
+ });
139
+ test("a deprecated request parameter is emitted with an @deprecated tag", async () => {
140
+ const types = await compileTypes();
141
+ expect(types).toMatch(deprecatedBefore("legacyFilter?"));
142
+ });
143
+ test("a deprecated referenced request parameter is emitted with an @deprecated tag", async () => {
144
+ const types = await compileTypes();
145
+ expect(types).toMatch(deprecatedBefore("legacySort?"));
146
+ });
147
+ /**
148
+ * Component parameters are request-only, so they are compiled through
149
+ * `compileAsRequestInput` rather than `compile`. Both paths have to apply the
150
+ * type alias workaround, otherwise the standalone alias ends up with an empty
151
+ * JSDoc block.
152
+ */
153
+ test("a deprecated component parameter alias gets an @deprecated tag", async () => {
154
+ const types = await compileTypes();
155
+ expect(types).toMatch(deprecatedBefore("export type LegacySort"));
156
+ });
157
+ const buildModel = (doc = makePetstoreDoc()) => CodeGenerationModel.fromDoc("PetstoreAPI", doc);
158
+ const compile = (doc = makePetstoreDoc()) => buildModel(doc).compileTypes({ rootNamespace: "PetstoreAPI" });
159
+ /** Collapses whitespace so assertions do not depend on generated indentation. */
160
+ const flat = (s) => s.replace(/\s+/g, " ");
161
+ describe("model structure", () => {
162
+ test("names the root namespace", () => {
163
+ expect(buildModel().rootNamespace.tsType).toBe("PetstoreAPI");
164
+ });
165
+ test("keeps the source document", () => {
166
+ const doc = makePetstoreDoc();
167
+ expect(buildModel(doc).doc).toBe(doc);
168
+ });
169
+ test("builds a Tag per documented tag", () => {
170
+ const { tags } = buildModel();
171
+ expect(tags.map((t) => t.name.raw)).toEqual(["pet", "store"]);
172
+ expect(tags[0].description).toBe("Everything about pets");
173
+ expect(tags[1].description).toBeUndefined();
174
+ });
175
+ test("tolerates a document without tags", () => {
176
+ const model = buildModel({ ...makePetstoreDoc(), tags: undefined });
177
+ expect(model.tags).toEqual([]);
178
+ });
179
+ });
180
+ describe("compiled types", () => {
181
+ let types;
182
+ beforeAll(async () => {
183
+ types = await compile();
184
+ });
185
+ test("wraps everything in the root namespace", () => {
186
+ expect(types).toContain("export declare module PetstoreAPI {");
187
+ });
188
+ test("imports the descriptors and the commons helper types", () => {
189
+ expect(types).toContain('import * as descriptors from "./descriptors.js"');
190
+ expect(flat(types)).toContain('InferredRequestData, InferredResponseData, HttpStatus } from "@mittwald/api-client-commons"');
191
+ });
192
+ test("declares an Operations namespace entry per operation", () => {
193
+ for (const operation of ["ListPets", "CreatePet", "GetPet", "DeletePet"]) {
194
+ expect(types).toContain(`namespace ${operation} {`);
195
+ }
196
+ });
197
+ test("wires operation request and response data to the descriptors", () => {
198
+ expect(flat(types)).toContain("type RequestData = InferredRequestData<typeof descriptors.listPets>;");
199
+ expect(flat(types)).toContain("type ResponseData<TStatus extends HttpStatus = 200> = InferredResponseData<typeof descriptors.listPets, TStatus>;");
200
+ });
201
+ test("compiles component schemas into interfaces", () => {
202
+ expect(flat(types)).toContain("export interface Pet { id: string; name: string; nickname?: string | null; tags?: PetstoreAPI.Components.Schemas.Tag[]; }");
203
+ });
204
+ test("turns schema refs into fully namespaced type references", () => {
205
+ expect(types).toContain("PetstoreAPI.Components.Schemas.Tag[]");
206
+ });
207
+ test("marks a nullable property as nullable", () => {
208
+ expect(flat(types)).toContain("nickname?: string | null;");
209
+ });
210
+ test("keeps required properties required", () => {
211
+ expect(flat(types)).toContain("export interface Tag { label: string; }");
212
+ });
213
+ test("compiles component parameters", () => {
214
+ expect(flat(types)).toContain("export type PetId = string;");
215
+ });
216
+ test("compiles component responses through to the referenced schema", () => {
217
+ expect(flat(types)).toContain("export type ApplicationJson = PetstoreAPI.Components.Schemas.Error;");
218
+ });
219
+ test("compiles the security scheme into a required header", () => {
220
+ expect(flat(types)).toContain('export interface AccessToken { "x-access-token": string; }');
221
+ });
222
+ test("derives a path namespace name from the path template", () => {
223
+ expect(types).toContain("namespace Pets {");
224
+ expect(types).toContain("namespace PetsPetId {");
225
+ });
226
+ test("declares a namespace per HTTP method of a path", () => {
227
+ expect(types).toContain("namespace Get {");
228
+ expect(types).toContain("namespace Post {");
229
+ expect(types).toContain("namespace Delete {");
230
+ });
231
+ test("splits operation parameters by location", () => {
232
+ expect(flat(types)).toContain("export type Query = { limit?: number; };");
233
+ expect(flat(types)).toContain('export type Header = { "x-access-token": string; };');
234
+ expect(flat(types)).toContain("export type Path = { petId: PetstoreAPI.Components.Parameters.PetId; };");
235
+ });
236
+ test("emits an empty parameter type where a location has no parameters", () => {
237
+ expect(flat(types)).toContain("export type Path = {};");
238
+ });
239
+ test("declares a namespace per response status", () => {
240
+ expect(types).toContain("namespace $200 {");
241
+ expect(types).toContain("namespace $201 {");
242
+ expect(types).toContain("namespace $204 {");
243
+ expect(types).toContain("namespace Default {");
244
+ });
245
+ test("types an inline array response through to the item schema", () => {
246
+ expect(flat(types)).toContain("export type ApplicationJson = PetstoreAPI.Components.Schemas.Pet[];");
247
+ });
248
+ test("resolves a referenced request body to its component type", () => {
249
+ expect(flat(types)).toContain("export type RequestBody = PetstoreAPI.Components.RequestBodies.PetBody;");
250
+ });
251
+ /**
252
+ * `ResponseContentTypes.buildContentTypesFromReferenceObject` passes
253
+ * `c.schema` — a `JSONSchema` _model instance_ — into `ResponseContent`,
254
+ * whose parameter is a raw JSON schema object. The instance is then wrapped
255
+ * in another `JSONSchema`, so nothing usable reaches
256
+ * `json-schema-to-typescript` and it falls back to an index signature.
257
+ * `c.schema.schemaObject` is what it should pass.
258
+ *
259
+ * Every error response in the real mittwald spec is a `$ref` to a component
260
+ * response, so every error payload in the published client is untyped even
261
+ * though `Components.Responses.*.ApplicationJson` is generated correctly.
262
+ */
263
+ test("loses the type of a response that references a component response", () => {
264
+ expect(flat(types)).toContain("export interface ApplicationJson { [k: string]: unknown; }");
265
+ expect(flat(types)).not.toContain("export type ApplicationJson = PetstoreAPI.Components.Responses.NotFound.ApplicationJson;");
266
+ });
267
+ /** Same root cause, reached through the request body component. */
268
+ test("loses the type of a request body component that references a schema", () => {
269
+ expect(flat(types)).toContain("export interface PetBody { [k: string]: unknown; }");
270
+ });
271
+ });
272
+ describe("compiled descriptors", () => {
273
+ let descriptors;
274
+ beforeAll(() => {
275
+ descriptors = buildModel().paths.compileDescriptors();
276
+ });
277
+ test("exports a descriptor per operation", () => {
278
+ for (const operationId of [
279
+ "listPets",
280
+ "createPet",
281
+ "getPet",
282
+ "deletePet",
283
+ ]) {
284
+ expect(descriptors).toContain(operationId);
285
+ }
286
+ });
287
+ test("records the path and method of each operation", () => {
288
+ expect(flat(descriptors)).toContain('path: "/pets/{petId}"');
289
+ expect(flat(descriptors)).toContain('method: "DELETE"');
290
+ expect(flat(descriptors)).toContain('method: "GET"');
291
+ expect(flat(descriptors)).toContain('method: "POST"');
292
+ });
293
+ test("records the response statuses and media types", () => {
294
+ expect(flat(descriptors)).toContain('"application/json"');
295
+ expect(flat(descriptors)).toContain('"default"');
296
+ });
297
+ });
298
+ describe("optional headers", () => {
299
+ test("makes a named header optional in the generated parameter type", async () => {
300
+ const types = await buildModel().compileTypes({
301
+ rootNamespace: "PetstoreAPI",
302
+ optionalHeaders: ["x-access-token"],
303
+ });
304
+ expect(flat(types)).toContain('export type Header = { "x-access-token"?: string; };');
305
+ });
306
+ test("leaves the header required when it is not listed", async () => {
307
+ const types = await compile();
308
+ expect(flat(types)).toContain('export type Header = { "x-access-token": string; };');
309
+ });
310
+ });
@@ -0,0 +1,152 @@
1
+ /**
2
+ * A deliberately small but structurally complete OpenAPI v3 document: refs
3
+ * between components, both safe and unsafe operations, path/query/header
4
+ * parameters, several response statuses, and a security scheme. It exercises
5
+ * the code generation model without the bulk of a real spec.
6
+ *
7
+ * Returned fresh on every call: building a `CodeGenerationModel` runs
8
+ * `populateNullableTypes` over the document and mutates it in place, so a
9
+ * shared instance would accumulate changes between tests.
10
+ */
11
+ export const makePetstoreDoc = () => ({
12
+ openapi: "3.0.3",
13
+ info: { title: "Petstore", version: "1.0.0" },
14
+ tags: [
15
+ { name: "pet", description: "Everything about pets" },
16
+ { name: "store" },
17
+ ],
18
+ components: {
19
+ securitySchemes: {
20
+ accessToken: {
21
+ type: "apiKey",
22
+ in: "header",
23
+ name: "x-access-token",
24
+ },
25
+ },
26
+ schemas: {
27
+ Pet: {
28
+ type: "object",
29
+ required: ["id", "name"],
30
+ properties: {
31
+ id: { type: "string" },
32
+ name: { type: "string" },
33
+ nickname: { type: "string", nullable: true },
34
+ tags: { type: "array", items: { $ref: "#/components/schemas/Tag" } },
35
+ },
36
+ },
37
+ Tag: {
38
+ type: "object",
39
+ required: ["label"],
40
+ properties: { label: { type: "string" } },
41
+ },
42
+ Error: {
43
+ type: "object",
44
+ required: ["message"],
45
+ properties: { message: { type: "string" } },
46
+ },
47
+ },
48
+ parameters: {
49
+ PetId: {
50
+ name: "petId",
51
+ in: "path",
52
+ required: true,
53
+ schema: { type: "string" },
54
+ },
55
+ },
56
+ requestBodies: {
57
+ PetBody: {
58
+ required: true,
59
+ content: {
60
+ "application/json": { schema: { $ref: "#/components/schemas/Pet" } },
61
+ },
62
+ },
63
+ },
64
+ responses: {
65
+ NotFound: {
66
+ description: "Not found",
67
+ content: {
68
+ "application/json": {
69
+ schema: { $ref: "#/components/schemas/Error" },
70
+ },
71
+ },
72
+ },
73
+ },
74
+ },
75
+ paths: {
76
+ "/pets": {
77
+ get: {
78
+ operationId: "listPets",
79
+ tags: ["pet"],
80
+ parameters: [
81
+ {
82
+ name: "limit",
83
+ in: "query",
84
+ required: false,
85
+ schema: { type: "integer" },
86
+ },
87
+ {
88
+ name: "x-access-token",
89
+ in: "header",
90
+ required: true,
91
+ schema: { type: "string" },
92
+ },
93
+ ],
94
+ responses: {
95
+ "200": {
96
+ description: "A list of pets",
97
+ content: {
98
+ "application/json": {
99
+ schema: {
100
+ type: "array",
101
+ items: { $ref: "#/components/schemas/Pet" },
102
+ },
103
+ },
104
+ },
105
+ },
106
+ default: { $ref: "#/components/responses/NotFound" },
107
+ },
108
+ },
109
+ post: {
110
+ operationId: "createPet",
111
+ tags: ["pet"],
112
+ requestBody: { $ref: "#/components/requestBodies/PetBody" },
113
+ responses: {
114
+ "201": {
115
+ description: "The created pet",
116
+ content: {
117
+ "application/json": {
118
+ schema: { $ref: "#/components/schemas/Pet" },
119
+ },
120
+ },
121
+ },
122
+ "400": { $ref: "#/components/responses/NotFound" },
123
+ },
124
+ },
125
+ },
126
+ "/pets/{petId}": {
127
+ get: {
128
+ operationId: "getPet",
129
+ tags: ["pet"],
130
+ parameters: [{ $ref: "#/components/parameters/PetId" }],
131
+ responses: {
132
+ "200": {
133
+ description: "The pet",
134
+ content: {
135
+ "application/json": {
136
+ schema: { $ref: "#/components/schemas/Pet" },
137
+ },
138
+ },
139
+ },
140
+ "404": { $ref: "#/components/responses/NotFound" },
141
+ },
142
+ },
143
+ delete: {
144
+ operationId: "deletePet",
145
+ parameters: [{ $ref: "#/components/parameters/PetId" }],
146
+ responses: {
147
+ "204": { description: "Deleted" },
148
+ },
149
+ },
150
+ },
151
+ },
152
+ });
@@ -2,6 +2,7 @@ import { JSONSchema } from "../global/JSONSchema.js";
2
2
  import { Name } from "../global/Name.js";
3
3
  import { asyncStringJoin } from "../../asyncStringJoin.js";
4
4
  import { assertNoRefs } from "../../refs/assertNoRefs.js";
5
+ import { withDeprecation } from "../../deprecation.js";
5
6
  export class Parameters {
6
7
  static ns = "Parameters";
7
8
  components;
@@ -15,7 +16,12 @@ export class Parameters {
15
16
  async compileTypes(opts) {
16
17
  const schemas = Object.entries(this.parameters).map(([name, param]) => {
17
18
  assertNoRefs(param);
18
- return new JSONSchema(new Name(name, this.name), param.schema);
19
+ return new JSONSchema(new Name(name, this.name),
20
+ // `deprecated` lives on the Parameter Object, not on its schema, so it
21
+ // has to be carried over explicitly to end up in the generated JSDoc.
22
+ param.schema === undefined
23
+ ? undefined
24
+ : withDeprecation(param.schema, param));
19
25
  });
20
26
  const t = {
21
27
  ns: Parameters.ns,
@@ -2,6 +2,7 @@ import { compileJsonSchema } from "../../compileJsonSchema.js";
2
2
  import { Name } from "./Name.js";
3
3
  import cloneDeep from "clone-deep";
4
4
  import { componentRefsToCustomTypes } from "../../refs/componentRefsToCustomTypes.js";
5
+ import { ensureDeprecatedTypeAliasComment, isDeprecated, } from "../../deprecation.js";
5
6
  import { widenDateTimeInputs } from "../../dateTime/dateTimeInput.js";
6
7
  import { dateTimeInputRefTSNameResolver } from "../../dateTime/dateTimeInputRefs.js";
7
8
  export class JSONSchema {
@@ -13,7 +14,10 @@ export class JSONSchema {
13
14
  }
14
15
  async compile(opts) {
15
16
  const withCustomRefTypes = componentRefsToCustomTypes(opts.rootNamespace, this.schemaObject);
16
- return compileJsonSchema(withCustomRefTypes, this.name.tsType);
17
+ const compiled = await compileJsonSchema(withCustomRefTypes, this.name.tsType);
18
+ return isDeprecated(this.schemaObject)
19
+ ? ensureDeprecatedTypeAliasComment(compiled, this.name.tsType)
20
+ : compiled;
17
21
  }
18
22
  /**
19
23
  * Compiles the schema for usage in _request_ position.
@@ -29,7 +33,10 @@ export class JSONSchema {
29
33
  async compileAsRequestInput(opts, dateTimeInputSchemaNames) {
30
34
  const widened = widenDateTimeInputs(this.schemaObject);
31
35
  const withCustomRefTypes = componentRefsToCustomTypes(opts.rootNamespace, widened, false, dateTimeInputRefTSNameResolver(dateTimeInputSchemaNames));
32
- return compileJsonSchema(withCustomRefTypes, this.name.tsType);
36
+ const compiled = await compileJsonSchema(withCustomRefTypes, this.name.tsType);
37
+ return isDeprecated(this.schemaObject)
38
+ ? ensureDeprecatedTypeAliasComment(compiled, this.name.tsType)
39
+ : compiled;
33
40
  }
34
41
  clone() {
35
42
  return new JSONSchema(new Name(this.name.raw), cloneDeep(this.schemaObject));
@@ -1,6 +1,7 @@
1
1
  import { JSONSchema } from "../../global/JSONSchema.js";
2
2
  import { Name } from "../../global/Name.js";
3
3
  import { isRef } from "../../../refs/isRef.js";
4
+ import { withDeprecation } from "../../../deprecation.js";
4
5
  import invariant from "invariant";
5
6
  export class RequestParameters {
6
7
  static ns = "Parameters";
@@ -26,7 +27,9 @@ export class RequestParameters {
26
27
  ? parameter
27
28
  : parameter.schema;
28
29
  invariant(!!jsonSchemaObject, `Could not find schema for request parameter ${name.raw}`);
29
- properties[resolvedParameter.name] = jsonSchemaObject;
30
+ // `deprecated` lives on the Parameter Object, not on its schema, so it
31
+ // has to be carried over explicitly to end up in the generated JSDoc.
32
+ properties[resolvedParameter.name] = withDeprecation(jsonSchemaObject, resolvedParameter);
30
33
  if (resolvedParameter.required) {
31
34
  required.push(resolvedParameter.name);
32
35
  }
@@ -0,0 +1,13 @@
1
+ import { Tag } from "./Tag.js";
2
+ test("keeps the raw tag name", () => {
3
+ expect(Tag.fromDoc({ name: "pet" }).name.raw).toBe("pet");
4
+ });
5
+ test("derives a TypeScript type name from the tag name", () => {
6
+ expect(Tag.fromDoc({ name: "pet-store" }).name.tsType).toBe("PetStore");
7
+ });
8
+ test("keeps the description when present", () => {
9
+ expect(Tag.fromDoc({ name: "pet", description: "About pets" }).description).toBe("About pets");
10
+ });
11
+ test("leaves the description undefined when absent", () => {
12
+ expect(Tag.fromDoc({ name: "pet" }).description).toBeUndefined();
13
+ });