typedapi-client-helpers 0.3.3 → 0.3.5

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.
@@ -1,411 +1,412 @@
1
- import { operationMethodName, operationTypeName } from "./names.mjs";
2
-
3
- export const httpMethods = new Set([
4
- "get",
5
- "put",
6
- "post",
7
- "delete",
8
- "options",
9
- "head",
10
- "patch",
11
- "trace",
12
- ]);
13
-
14
- export function isObject(value) {
15
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
16
- }
17
-
18
- export function dereference(openApi, value, kind = "OpenAPI reference") {
19
- if (!value || !value.$ref) return value;
20
-
21
- const ref = String(value.$ref);
22
- if (!ref.startsWith("#/")) {
23
- throw new Error(`${kind} uses an unsupported external reference: ${ref}`);
24
- }
25
-
26
- const path = ref.replace(/^#\//, "").split("/");
27
- let current = openApi;
28
-
29
- for (const rawPart of path) {
30
- const part = rawPart.replace(/~1/g, "/").replace(/~0/g, "~");
31
- current = current?.[part];
32
- }
33
-
34
- if (current === undefined) {
35
- throw new Error(`${kind} could not be resolved: ${ref}`);
36
- }
37
-
38
- return current;
39
- }
40
-
41
- export function getParameterSchema(openApi, parameter) {
42
- if (parameter?.schema)
43
- return dereference(openApi, parameter.schema, "Parameter schema reference");
44
-
45
- const content = parameter?.content ?? {};
46
- const mediaTypes = Object.entries(content);
47
- if (mediaTypes.length === 0) return {};
48
- if (mediaTypes.length > 1) {
49
- throw new Error(
50
- `Parameter ${JSON.stringify(parameter?.name ?? "unknown")} contains multiple media types, which is not supported.`,
51
- );
52
- }
53
- return dereference(
54
- openApi,
55
- mediaTypes[0][1]?.schema ?? {},
56
- "Parameter content schema reference",
57
- );
58
- }
59
-
60
- export function getRequestBodyContent(openApi, requestBodyOrRef) {
61
- const requestBody = dereference(
62
- openApi,
63
- requestBodyOrRef,
64
- "Request body reference",
65
- );
66
- const content = requestBody?.content ?? {};
67
- const preferredTypes = [
68
- "application/json",
69
- "application/vnd.api+json",
70
- "multipart/form-data",
71
- "application/x-www-form-urlencoded",
72
- "text/plain",
73
- "application/octet-stream",
74
- ];
75
-
76
- for (const contentType of preferredTypes) {
77
- if (content[contentType])
78
- return { contentType, mediaType: content[contentType], requestBody };
79
- }
80
-
81
- const [contentType, mediaType] = Object.entries(content)[0] ?? [];
82
- return contentType ? { contentType, mediaType, requestBody } : undefined;
83
- }
84
-
85
- export function getSuccessResponse(
86
- openApi,
87
- operation,
88
- defaultResponseAsSuccess = false,
89
- ) {
90
- const responses = operation.responses ?? {};
91
- const preferredStatusCodes = [
92
- "200",
93
- "201",
94
- "202",
95
- "203",
96
- "204",
97
- "205",
98
- "206",
99
- "207",
100
- "208",
101
- "226",
102
- ];
103
-
104
- for (const statusCode of preferredStatusCodes) {
105
- if (responses[statusCode]) {
106
- return {
107
- statusCode,
108
- response: dereference(
109
- openApi,
110
- responses[statusCode],
111
- "Response reference",
112
- ),
113
- };
114
- }
115
- }
116
-
117
- const successEntry = Object.entries(responses).find(([statusCode]) =>
118
- /^2(?:\d\d|XX)$/i.test(statusCode),
119
- );
120
- if (successEntry) {
121
- return {
122
- statusCode: successEntry[0],
123
- response: dereference(openApi, successEntry[1], "Response reference"),
124
- };
125
- }
126
-
127
- if (defaultResponseAsSuccess && responses.default) {
128
- return {
129
- statusCode: "default",
130
- response: dereference(openApi, responses.default, "Response reference"),
131
- };
132
- }
133
-
134
- return undefined;
135
- }
136
-
137
- export function getErrorResponses(openApi, operation) {
138
- return Object.entries(operation.responses ?? {})
139
- .filter(
140
- ([statusCode]) =>
141
- statusCode === "default" || !/^2(?:\d\d|XX)$/i.test(statusCode),
142
- )
143
- .map(([statusCode, response]) => ({
144
- statusCode,
145
- response: dereference(openApi, response, "Response reference"),
146
- }));
147
- }
148
-
149
- export function getResponseContent(response) {
150
- const content = response?.content ?? {};
151
- const preferredTypes = [
152
- "application/problem+json",
153
- "application/json",
154
- "text/json",
155
- "application/octet-stream",
156
- "text/plain",
157
- ];
158
-
159
- for (const contentType of preferredTypes) {
160
- if (content[contentType])
161
- return { contentType, mediaType: content[contentType] };
162
- }
163
-
164
- const [contentType, mediaType] = Object.entries(content)[0] ?? [];
165
- return contentType ? { contentType, mediaType } : undefined;
166
- }
167
-
168
- function fallbackOperationId(routePath, method, operation) {
169
- const tag = operation.tags?.[0];
170
- const route = routePath.replace(/[{}]/g, " ");
171
- return `${tag ?? "Endpoint"} ${method} ${route}`;
172
- }
173
-
174
- function getTypedApiOperationMetadata(operation) {
175
- const metadata = operation?.["x-typedapi-operation"];
176
- return isObject(metadata) ? metadata : {};
177
- }
178
-
179
- function normalizeControllerName(value) {
180
- return String(value ?? "")
181
- .trim()
182
- .replace(/Controller$/i, "")
183
- .trim();
184
- }
185
-
186
- function resolveControllerName(operation, routePath) {
187
- const metadataControllerName = normalizeControllerName(
188
- getTypedApiOperationMetadata(operation).controllerName,
189
- );
190
- if (metadataControllerName) return metadataControllerName;
191
-
192
- const [tag] = operation.tags ?? [];
193
- const tagControllerName = normalizeControllerName(tag);
194
- if (tagControllerName) return tagControllerName;
195
-
196
- const parts = routePath.split("/").filter(Boolean);
197
- return normalizeControllerName(parts[1] ?? parts[0] ?? "");
198
- }
199
-
200
- function controllerActionName(controllerName, actionName) {
201
- const normalizedControllerName = normalizeControllerName(controllerName);
202
- const normalizedActionName = String(actionName ?? "").trim();
203
-
204
- if (!normalizedControllerName) return normalizedActionName;
205
- if (!normalizedActionName) return normalizedControllerName;
206
-
207
- if (
208
- normalizedActionName
209
- .toLowerCase()
210
- .startsWith(normalizedControllerName.toLowerCase())
211
- ) {
212
- return normalizedActionName;
213
- }
214
-
215
- return `${normalizedControllerName}${normalizedActionName}`;
216
- }
217
-
218
- function resolveMethodNameSource(
219
- operation,
220
- rawOperationId,
221
- options,
222
- method,
223
- routePath,
224
- ) {
225
- const style = options.methodNameStyle ?? "operationId";
226
- if (style === "operationId") return rawOperationId;
227
-
228
- if (style === "action") {
229
- const metadata = getTypedApiOperationMetadata(operation);
230
- const actionName = String(metadata.actionName ?? "").trim();
231
-
232
- if (!actionName) {
233
- throw new Error(
234
- `Cannot use typedApiMethodNameStyle "action" for ${method.toUpperCase()} ${routePath}: ` +
235
- "the operation does not contain x-typedapi-operation.actionName metadata. " +
236
- 'Update TypedApi.Swagger to the matching 0.3.0 package or use typedApiMethodNameStyle "operationId".',
237
- );
238
- }
239
-
240
- if (options.prefixMethodNamesWithController === false) {
241
- return actionName;
242
- }
243
-
244
- const controllerName = resolveControllerName(operation, routePath);
245
-
246
- if (!controllerName) {
247
- throw new Error(
248
- `Cannot prefix the action name for ${method.toUpperCase()} ${routePath}: ` +
249
- "the controller name could not be determined from x-typedapi-operation.controllerName, the first tag, or the route. " +
250
- "Set typedApiPrefixMethodNamesWithController to false to use only the action name.",
251
- );
252
- }
253
-
254
- return controllerActionName(controllerName, actionName);
255
- }
256
-
257
- throw new Error(
258
- `Unsupported method name style ${JSON.stringify(style)}. Use "operationId" or "action".`,
259
- );
260
- }
261
-
262
- export function collectOperations(openApi, options = {}) {
263
- const operations = [];
264
- const rawIds = new Map();
265
- const typeNames = new Map();
266
- const methodNames = new Map();
267
-
268
- for (const [routePath, pathItemOrRef] of Object.entries(
269
- openApi.paths ?? {},
270
- )) {
271
- const pathItem = dereference(openApi, pathItemOrRef, "Path item reference");
272
- const pathParameters = Array.isArray(pathItem.parameters)
273
- ? pathItem.parameters.map((item) =>
274
- dereference(openApi, item, "Parameter reference"),
275
- )
276
- : [];
277
-
278
- for (const [method, operation] of Object.entries(pathItem)) {
279
- if (!httpMethods.has(method)) continue;
280
-
281
- const rawOperationId = String(
282
- operation.operationId ??
283
- (options.generateMissingOperationIds
284
- ? fallbackOperationId(routePath, method, operation)
285
- : ""),
286
- ).trim();
287
-
288
- if (!rawOperationId) {
289
- throw new Error(
290
- `Missing operationId for ${method.toUpperCase()} ${routePath}. ` +
291
- "Add a unique operationId or enable generateMissingOperationIds.",
292
- );
293
- }
294
-
295
- if (rawIds.has(rawOperationId)) {
296
- throw new Error(
297
- `Duplicate operationId ${JSON.stringify(rawOperationId)} for ` +
298
- `${method.toUpperCase()} ${routePath} and ${rawIds.get(rawOperationId)}.`,
299
- );
300
- }
301
- rawIds.set(rawOperationId, `${method.toUpperCase()} ${routePath}`);
302
-
303
- const operationId = operationTypeName(rawOperationId);
304
- const methodNameSource = resolveMethodNameSource(
305
- operation,
306
- rawOperationId,
307
- options,
308
- method,
309
- routePath,
310
- );
311
- const methodName = operationMethodName(methodNameSource);
312
- const typeName = operationTypeName(methodNameSource);
313
-
314
- if (typeNames.has(operationId)) {
315
- throw new Error(
316
- `Operation IDs ${JSON.stringify(rawOperationId)} and ${JSON.stringify(typeNames.get(operationId))} ` +
317
- `both normalize to TypeScript type name ${operationId}.`,
318
- );
319
- }
320
- if (methodNames.has(methodName)) {
321
- const previous = methodNames.get(methodName);
322
- throw new Error(
323
- `Generated TypeScript method name ${JSON.stringify(methodName)} is duplicated by ` +
324
- `${JSON.stringify(previous.operationId)} and ${JSON.stringify(rawOperationId)} ` +
325
- `while typedApiMethodNameStyle is ${JSON.stringify(options.methodNameStyle ?? "operationId")}. ` +
326
- (options.methodNameStyle === "action" && options.prefixMethodNamesWithController === false
327
- ? "Enable typedApiPrefixMethodNamesWithController, rename one controller action, or use typedApiMethodNameStyle \"operationId\"."
328
- : 'Rename one controller action or use typedApiMethodNameStyle "operationId".'),
329
- );
330
- }
331
- typeNames.set(operationId, rawOperationId);
332
- methodNames.set(methodName, {
333
- operationId: rawOperationId,
334
- source: methodNameSource,
335
- });
336
-
337
- const operationParameters = Array.isArray(operation.parameters)
338
- ? operation.parameters.map((item) =>
339
- dereference(openApi, item, "Parameter reference"),
340
- )
341
- : [];
342
- const parametersByKey = new Map();
343
- for (const parameter of [...pathParameters, ...operationParameters]) {
344
- const location = String(parameter?.in ?? "");
345
- const name = String(parameter?.name ?? "");
346
- if (!["path", "query", "header", "cookie"].includes(location)) {
347
- throw new Error(
348
- `Unsupported parameter location ${JSON.stringify(location)} for ${method.toUpperCase()} ${routePath}.`,
349
- );
350
- }
351
- if (!name) {
352
- throw new Error(
353
- `A ${location} parameter for ${method.toUpperCase()} ${routePath} has no name.`,
354
- );
355
- }
356
- if (location === "path" && parameter.required !== true) {
357
- throw new Error(
358
- `Path parameter ${JSON.stringify(name)} must be required for ${method.toUpperCase()} ${routePath}.`,
359
- );
360
- }
361
- parametersByKey.set(`${location}:${name}`, parameter);
362
- }
363
- const parameters = [...parametersByKey.values()];
364
-
365
- operations.push({
366
- routePath,
367
- method: method.toUpperCase(),
368
- operation,
369
- originalOperationId: rawOperationId,
370
- operationId,
371
- typeName,
372
- methodName,
373
- methodNameSource,
374
- parameters,
375
- });
376
- }
377
- }
378
-
379
- return operations;
380
- }
381
-
382
- export function validateOpenApiDocument(openApi, options = {}) {
383
- if (!isObject(openApi))
384
- throw new Error("The OpenAPI input is not a JSON object.");
385
- if (
386
- typeof openApi.openapi !== "string" ||
387
- !openApi.openapi.startsWith("3.")
388
- ) {
389
- throw new Error(
390
- `TypedApi requires an OpenAPI 3.x document. Received: ${JSON.stringify(openApi.openapi ?? openApi.swagger ?? "unknown")}.`,
391
- );
392
- }
393
- if (!isObject(openApi.info))
394
- throw new Error(
395
- "The OpenAPI document does not contain a valid info object.",
396
- );
397
- if (!isObject(openApi.paths))
398
- throw new Error(
399
- "The OpenAPI document does not contain a valid paths object.",
400
- );
401
-
402
- const contract = openApi["x-typedapi"];
403
- if (contract?.contractVersion !== undefined) {
404
- const supported = options.supportedContractVersion ?? 1;
405
- if (Number(contract.contractVersion) !== supported) {
406
- throw new Error(
407
- `Unsupported TypedApi contract version ${contract.contractVersion}. This generator supports version ${supported}.`,
408
- );
409
- }
410
- }
411
- }
1
+ import { operationMethodName, operationTypeName } from "./names.mjs";
2
+
3
+ export const httpMethods = new Set([
4
+ "get",
5
+ "put",
6
+ "post",
7
+ "delete",
8
+ "options",
9
+ "head",
10
+ "patch",
11
+ "trace",
12
+ ]);
13
+
14
+ export function isObject(value) {
15
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
16
+ }
17
+
18
+ export function dereference(openApi, value, kind = "OpenAPI reference") {
19
+ if (!value || !value.$ref) return value;
20
+
21
+ const ref = String(value.$ref);
22
+ if (!ref.startsWith("#/")) {
23
+ throw new Error(`${kind} uses an unsupported external reference: ${ref}`);
24
+ }
25
+
26
+ const path = ref.replace(/^#\//, "").split("/");
27
+ let current = openApi;
28
+
29
+ for (const rawPart of path) {
30
+ const part = rawPart.replace(/~1/g, "/").replace(/~0/g, "~");
31
+ current = current?.[part];
32
+ }
33
+
34
+ if (current === undefined) {
35
+ throw new Error(`${kind} could not be resolved: ${ref}`);
36
+ }
37
+
38
+ return current;
39
+ }
40
+
41
+ export function getParameterSchema(openApi, parameter) {
42
+ if (parameter?.schema)
43
+ return dereference(openApi, parameter.schema, "Parameter schema reference");
44
+
45
+ const content = parameter?.content ?? {};
46
+ const mediaTypes = Object.entries(content);
47
+ if (mediaTypes.length === 0) return {};
48
+ if (mediaTypes.length > 1) {
49
+ throw new Error(
50
+ `Parameter ${JSON.stringify(parameter?.name ?? "unknown")} contains multiple media types, which is not supported.`,
51
+ );
52
+ }
53
+ return dereference(
54
+ openApi,
55
+ mediaTypes[0][1]?.schema ?? {},
56
+ "Parameter content schema reference",
57
+ );
58
+ }
59
+
60
+ export function getRequestBodyContent(openApi, requestBodyOrRef) {
61
+ const requestBody = dereference(
62
+ openApi,
63
+ requestBodyOrRef,
64
+ "Request body reference",
65
+ );
66
+ const content = requestBody?.content ?? {};
67
+ const preferredTypes = [
68
+ "application/json",
69
+ "application/vnd.api+json",
70
+ "multipart/form-data",
71
+ "application/x-www-form-urlencoded",
72
+ "text/plain",
73
+ "application/octet-stream",
74
+ ];
75
+
76
+ for (const contentType of preferredTypes) {
77
+ if (content[contentType])
78
+ return { contentType, mediaType: content[contentType], requestBody };
79
+ }
80
+
81
+ const [contentType, mediaType] = Object.entries(content)[0] ?? [];
82
+ return contentType ? { contentType, mediaType, requestBody } : undefined;
83
+ }
84
+
85
+ export function getSuccessResponse(
86
+ openApi,
87
+ operation,
88
+ defaultResponseAsSuccess = false,
89
+ ) {
90
+ const responses = operation.responses ?? {};
91
+ const preferredStatusCodes = [
92
+ "200",
93
+ "201",
94
+ "202",
95
+ "203",
96
+ "204",
97
+ "205",
98
+ "206",
99
+ "207",
100
+ "208",
101
+ "226",
102
+ ];
103
+
104
+ for (const statusCode of preferredStatusCodes) {
105
+ if (responses[statusCode]) {
106
+ return {
107
+ statusCode,
108
+ response: dereference(
109
+ openApi,
110
+ responses[statusCode],
111
+ "Response reference",
112
+ ),
113
+ };
114
+ }
115
+ }
116
+
117
+ const successEntry = Object.entries(responses).find(([statusCode]) =>
118
+ /^2(?:\d\d|XX)$/i.test(statusCode),
119
+ );
120
+ if (successEntry) {
121
+ return {
122
+ statusCode: successEntry[0],
123
+ response: dereference(openApi, successEntry[1], "Response reference"),
124
+ };
125
+ }
126
+
127
+ if (defaultResponseAsSuccess && responses.default) {
128
+ return {
129
+ statusCode: "default",
130
+ response: dereference(openApi, responses.default, "Response reference"),
131
+ };
132
+ }
133
+
134
+ return undefined;
135
+ }
136
+
137
+ export function getErrorResponses(openApi, operation) {
138
+ return Object.entries(operation.responses ?? {})
139
+ .filter(
140
+ ([statusCode]) =>
141
+ statusCode === "default" || !/^2(?:\d\d|XX)$/i.test(statusCode),
142
+ )
143
+ .map(([statusCode, response]) => ({
144
+ statusCode,
145
+ response: dereference(openApi, response, "Response reference"),
146
+ }));
147
+ }
148
+
149
+ export function getResponseContent(response) {
150
+ const content = response?.content ?? {};
151
+ const preferredTypes = [
152
+ "application/problem+json",
153
+ "application/json",
154
+ "text/json",
155
+ "application/octet-stream",
156
+ "text/plain",
157
+ ];
158
+
159
+ for (const contentType of preferredTypes) {
160
+ if (content[contentType])
161
+ return { contentType, mediaType: content[contentType] };
162
+ }
163
+
164
+ const [contentType, mediaType] = Object.entries(content)[0] ?? [];
165
+ return contentType ? { contentType, mediaType } : undefined;
166
+ }
167
+
168
+ function fallbackOperationId(routePath, method, operation) {
169
+ const tag = operation.tags?.[0];
170
+ const route = routePath.replace(/[{}]/g, " ");
171
+ return `${tag ?? "Endpoint"} ${method} ${route}`;
172
+ }
173
+
174
+ function getTypedApiOperationMetadata(operation) {
175
+ const metadata = operation?.["x-typedapi-operation"];
176
+ return isObject(metadata) ? metadata : {};
177
+ }
178
+
179
+ function normalizeControllerName(value) {
180
+ return String(value ?? "")
181
+ .trim()
182
+ .replace(/Controller$/i, "")
183
+ .trim();
184
+ }
185
+
186
+ function resolveControllerName(operation, routePath) {
187
+ const metadataControllerName = normalizeControllerName(
188
+ getTypedApiOperationMetadata(operation).controllerName,
189
+ );
190
+ if (metadataControllerName) return metadataControllerName;
191
+
192
+ const [tag] = operation.tags ?? [];
193
+ const tagControllerName = normalizeControllerName(tag);
194
+ if (tagControllerName) return tagControllerName;
195
+
196
+ const parts = routePath.split("/").filter(Boolean);
197
+ return normalizeControllerName(parts[1] ?? parts[0] ?? "");
198
+ }
199
+
200
+ function controllerActionName(controllerName, actionName) {
201
+ const normalizedControllerName = normalizeControllerName(controllerName);
202
+ const normalizedActionName = String(actionName ?? "").trim();
203
+
204
+ if (!normalizedControllerName) return normalizedActionName;
205
+ if (!normalizedActionName) return normalizedControllerName;
206
+
207
+ if (
208
+ normalizedActionName
209
+ .toLowerCase()
210
+ .startsWith(normalizedControllerName.toLowerCase())
211
+ ) {
212
+ return normalizedActionName;
213
+ }
214
+
215
+ return `${normalizedControllerName}${normalizedActionName}`;
216
+ }
217
+
218
+ function resolveMethodNameSource(
219
+ operation,
220
+ rawOperationId,
221
+ options,
222
+ method,
223
+ routePath,
224
+ ) {
225
+ const style = options.methodNameStyle ?? "operationId";
226
+ if (style === "operationId") return rawOperationId;
227
+
228
+ if (style === "action") {
229
+ const metadata = getTypedApiOperationMetadata(operation);
230
+ const actionName = String(metadata.actionName ?? "").trim();
231
+
232
+ if (!actionName) {
233
+ throw new Error(
234
+ `Cannot use typedApiMethodNameStyle "action" for ${method.toUpperCase()} ${routePath}: ` +
235
+ "the operation does not contain x-typedapi-operation.actionName metadata. " +
236
+ 'Update TypedApi.Swagger to version 0.3.0 or newer, or use typedApiMethodNameStyle "operationId".',
237
+ );
238
+ }
239
+
240
+ if (options.prefixMethodNamesWithController === false) {
241
+ return actionName;
242
+ }
243
+
244
+ const controllerName = resolveControllerName(operation, routePath);
245
+
246
+ if (!controllerName) {
247
+ throw new Error(
248
+ `Cannot prefix the action name for ${method.toUpperCase()} ${routePath}: ` +
249
+ "the controller name could not be determined from x-typedapi-operation.controllerName, the first tag, or the route. " +
250
+ "Set typedApiPrefixMethodNamesWithController to false to use only the action name.",
251
+ );
252
+ }
253
+
254
+ return controllerActionName(controllerName, actionName);
255
+ }
256
+
257
+ throw new Error(
258
+ `Unsupported method name style ${JSON.stringify(style)}. Use "operationId" or "action".`,
259
+ );
260
+ }
261
+
262
+ export function collectOperations(openApi, options = {}) {
263
+ const operations = [];
264
+ const rawIds = new Map();
265
+ const typeNames = new Map();
266
+ const methodNames = new Map();
267
+
268
+ for (const [routePath, pathItemOrRef] of Object.entries(
269
+ openApi.paths ?? {},
270
+ )) {
271
+ const pathItem = dereference(openApi, pathItemOrRef, "Path item reference");
272
+ const pathParameters = Array.isArray(pathItem.parameters)
273
+ ? pathItem.parameters.map((item) =>
274
+ dereference(openApi, item, "Parameter reference"),
275
+ )
276
+ : [];
277
+
278
+ for (const [method, operation] of Object.entries(pathItem)) {
279
+ if (!httpMethods.has(method)) continue;
280
+
281
+ const rawOperationId = String(
282
+ operation.operationId ??
283
+ (options.generateMissingOperationIds
284
+ ? fallbackOperationId(routePath, method, operation)
285
+ : ""),
286
+ ).trim();
287
+
288
+ if (!rawOperationId) {
289
+ throw new Error(
290
+ `Missing operationId for ${method.toUpperCase()} ${routePath}. ` +
291
+ "Add a unique operationId or enable generateMissingOperationIds.",
292
+ );
293
+ }
294
+
295
+ if (rawIds.has(rawOperationId)) {
296
+ throw new Error(
297
+ `Duplicate operationId ${JSON.stringify(rawOperationId)} for ` +
298
+ `${method.toUpperCase()} ${routePath} and ${rawIds.get(rawOperationId)}.`,
299
+ );
300
+ }
301
+ rawIds.set(rawOperationId, `${method.toUpperCase()} ${routePath}`);
302
+
303
+ const operationId = operationTypeName(rawOperationId);
304
+ const methodNameSource = resolveMethodNameSource(
305
+ operation,
306
+ rawOperationId,
307
+ options,
308
+ method,
309
+ routePath,
310
+ );
311
+ const methodName = operationMethodName(methodNameSource);
312
+ const typeName = operationTypeName(methodNameSource);
313
+
314
+ if (typeNames.has(operationId)) {
315
+ throw new Error(
316
+ `Operation IDs ${JSON.stringify(rawOperationId)} and ${JSON.stringify(typeNames.get(operationId))} ` +
317
+ `both normalize to TypeScript type name ${operationId}.`,
318
+ );
319
+ }
320
+ if (methodNames.has(methodName)) {
321
+ const previous = methodNames.get(methodName);
322
+ throw new Error(
323
+ `Generated TypeScript method name ${JSON.stringify(methodName)} is duplicated by ` +
324
+ `${JSON.stringify(previous.operationId)} and ${JSON.stringify(rawOperationId)} ` +
325
+ `while typedApiMethodNameStyle is ${JSON.stringify(options.methodNameStyle ?? "operationId")}. ` +
326
+ (options.methodNameStyle === "action" && options.prefixMethodNamesWithController === false
327
+ ? "Enable typedApiPrefixMethodNamesWithController, rename one controller action, or use typedApiMethodNameStyle \"operationId\"."
328
+ : 'Rename one controller action or use typedApiMethodNameStyle "operationId".'),
329
+ );
330
+ }
331
+ typeNames.set(operationId, rawOperationId);
332
+ methodNames.set(methodName, {
333
+ operationId: rawOperationId,
334
+ source: methodNameSource,
335
+ });
336
+
337
+ const operationParameters = Array.isArray(operation.parameters)
338
+ ? operation.parameters.map((item) =>
339
+ dereference(openApi, item, "Parameter reference"),
340
+ )
341
+ : [];
342
+ const parametersByKey = new Map();
343
+ for (const parameter of [...pathParameters, ...operationParameters]) {
344
+ const location = String(parameter?.in ?? "");
345
+ const name = String(parameter?.name ?? "");
346
+ if (!["path", "query", "header", "cookie"].includes(location)) {
347
+ throw new Error(
348
+ `Unsupported parameter location ${JSON.stringify(location)} for ${method.toUpperCase()} ${routePath}.`,
349
+ );
350
+ }
351
+ if (!name) {
352
+ throw new Error(
353
+ `A ${location} parameter for ${method.toUpperCase()} ${routePath} has no name.`,
354
+ );
355
+ }
356
+ if (location === "path" && parameter.required !== true) {
357
+ throw new Error(
358
+ `Path parameter ${JSON.stringify(name)} must be required for ${method.toUpperCase()} ${routePath}.`,
359
+ );
360
+ }
361
+ parametersByKey.set(`${location}:${name}`, parameter);
362
+ }
363
+ const parameters = [...parametersByKey.values()];
364
+
365
+ operations.push({
366
+ routePath,
367
+ method: method.toUpperCase(),
368
+ operation,
369
+ originalOperationId: rawOperationId,
370
+ operationId,
371
+ typeName,
372
+ methodName,
373
+ methodNameSource,
374
+ parameters,
375
+ });
376
+ }
377
+ }
378
+
379
+ return operations;
380
+ }
381
+
382
+ export function validateOpenApiDocument(openApi, options = {}) {
383
+ if (!isObject(openApi))
384
+ throw new Error("The OpenAPI input is not a JSON object.");
385
+ if (
386
+ typeof openApi.openapi !== "string" ||
387
+ !openApi.openapi.startsWith("3.")
388
+ ) {
389
+ throw new Error(
390
+ `TypedApi requires an OpenAPI 3.x document. Received: ${JSON.stringify(openApi.openapi ?? openApi.swagger ?? "unknown")}.`,
391
+ );
392
+ }
393
+ if (!isObject(openApi.info))
394
+ throw new Error(
395
+ "The OpenAPI document does not contain a valid info object.",
396
+ );
397
+ if (!isObject(openApi.paths))
398
+ throw new Error(
399
+ "The OpenAPI document does not contain a valid paths object.",
400
+ );
401
+
402
+ const contract = openApi["x-typedapi"];
403
+ if (contract?.contractVersion !== undefined) {
404
+ const supported = options.supportedContractVersion ?? 1;
405
+ const received = Number(contract.contractVersion);
406
+ if (!Number.isInteger(received) || received < 1 || received > supported) {
407
+ throw new Error(
408
+ `Unsupported TypedApi contract version ${contract.contractVersion}. This generator supports versions 1 through ${supported}.`,
409
+ );
410
+ }
411
+ }
412
+ }