@effect/openapi-generator 4.0.0-beta.8 → 4.0.0-beta.81

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 (41) hide show
  1. package/dist/HttpApiTransformer.d.ts +41 -0
  2. package/dist/HttpApiTransformer.d.ts.map +1 -0
  3. package/dist/HttpApiTransformer.js +393 -0
  4. package/dist/HttpApiTransformer.js.map +1 -0
  5. package/dist/JsonSchemaGenerator.d.ts +25 -3
  6. package/dist/JsonSchemaGenerator.d.ts.map +1 -1
  7. package/dist/JsonSchemaGenerator.js +178 -47
  8. package/dist/JsonSchemaGenerator.js.map +1 -1
  9. package/dist/OpenApiGenerator.d.ts +81 -7
  10. package/dist/OpenApiGenerator.d.ts.map +1 -1
  11. package/dist/OpenApiGenerator.js +763 -119
  12. package/dist/OpenApiGenerator.js.map +1 -1
  13. package/dist/OpenApiPatch.d.ts +47 -24
  14. package/dist/OpenApiPatch.d.ts.map +1 -1
  15. package/dist/OpenApiPatch.js +42 -19
  16. package/dist/OpenApiPatch.js.map +1 -1
  17. package/dist/OpenApiTransformer.d.ts +85 -12
  18. package/dist/OpenApiTransformer.d.ts.map +1 -1
  19. package/dist/OpenApiTransformer.js +85 -8
  20. package/dist/OpenApiTransformer.js.map +1 -1
  21. package/dist/ParsedOperation.d.ts +184 -1
  22. package/dist/ParsedOperation.d.ts.map +1 -1
  23. package/dist/ParsedOperation.js +32 -0
  24. package/dist/ParsedOperation.js.map +1 -1
  25. package/dist/Utils.d.ts +51 -0
  26. package/dist/Utils.d.ts.map +1 -1
  27. package/dist/Utils.js +61 -0
  28. package/dist/Utils.js.map +1 -1
  29. package/dist/main.d.ts +12 -0
  30. package/dist/main.d.ts.map +1 -1
  31. package/dist/main.js +41 -8
  32. package/dist/main.js.map +1 -1
  33. package/package.json +10 -8
  34. package/src/HttpApiTransformer.ts +552 -0
  35. package/src/JsonSchemaGenerator.ts +272 -47
  36. package/src/OpenApiGenerator.ts +1057 -165
  37. package/src/OpenApiPatch.ts +56 -31
  38. package/src/OpenApiTransformer.ts +89 -12
  39. package/src/ParsedOperation.ts +235 -1
  40. package/src/Utils.ts +61 -0
  41. package/src/main.ts +58 -10
@@ -0,0 +1,552 @@
1
+ /**
2
+ * Renders parsed OpenAPI operations into generated Effect HttpApi source code.
3
+ *
4
+ * This module maps normalized OpenAPI metadata, tags, request bodies,
5
+ * responses, and security requirements into import declarations, HttpApi
6
+ * groups, endpoint definitions, schema annotations, and middleware classes.
7
+ * The rendered TypeScript can be emitted by the OpenAPI generator as an
8
+ * executable Effect `HttpApi` module.
9
+ *
10
+ * @since 4.0.0
11
+ */
12
+ import type {
13
+ ParsedOpenApi,
14
+ ParsedOpenApiSecurityScheme,
15
+ ParsedOpenApiTag,
16
+ ParsedOperation,
17
+ ParsedOperationMediaTypeSchema,
18
+ ParsedOperationResponse
19
+ } from "./ParsedOperation.ts"
20
+ import * as Utils from "./Utils.ts"
21
+
22
+ interface GroupRenderModel {
23
+ readonly identifier: string
24
+ readonly topLevel: boolean
25
+ readonly metadata: ParsedOpenApiTag | undefined
26
+ readonly operations: ReadonlyArray<ParsedOperation>
27
+ readonly constName: string
28
+ }
29
+
30
+ interface SecurityRenderModel {
31
+ readonly securityDeclarations: ReadonlyArray<string>
32
+ readonly middlewareDeclarations: ReadonlyArray<string>
33
+ readonly endpointMiddlewares: ReadonlyMap<string, ReadonlyArray<string>>
34
+ }
35
+
36
+ const fallbackGroupIdentifier = "default"
37
+
38
+ /**
39
+ * Render the import declarations required by generated HttpApi source.
40
+ *
41
+ * **Details**
42
+ *
43
+ * The schema namespace import is named by the caller so generated code can
44
+ * avoid collisions with symbols already present in the output module. Multipart
45
+ * support is included only when the parsed OpenAPI document needs it.
46
+ *
47
+ * @category code generation
48
+ * @since 4.0.0
49
+ */
50
+ export const imports = (
51
+ importName: string,
52
+ options?: {
53
+ readonly multipart?: boolean | undefined
54
+ }
55
+ ): string =>
56
+ [
57
+ `import * as ${importName} from "effect/Schema"`,
58
+ ...(options?.multipart === true ? [`import { Multipart } from "effect/unstable/http"`] : []),
59
+ `import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity, OpenApi } from "effect/unstable/httpapi"`
60
+ ].join("\n")
61
+
62
+ /**
63
+ * Convert a parsed OpenAPI document into Effect HttpApi source code.
64
+ *
65
+ * **Details**
66
+ *
67
+ * The generated implementation contains security declarations, reusable
68
+ * middleware classes, HttpApi groups, endpoint definitions, and OpenAPI
69
+ * annotations derived from the parsed operation metadata.
70
+ *
71
+ * @category code generation
72
+ * @since 4.0.0
73
+ */
74
+ export const toImplementation = (
75
+ _importName: string,
76
+ name: string,
77
+ parsed: ParsedOpenApi
78
+ ): string => {
79
+ const security = buildSecurityRenderModel(parsed)
80
+ const groups = groupOperations(parsed)
81
+ const groupSources = groups.map((group) => renderGroup(group, security.endpointMiddlewares))
82
+ const metadataAnnotations = renderApiAnnotations(parsed)
83
+
84
+ let apiValue = `export class ${name} extends HttpApi.make(${JSON.stringify(name)})`
85
+ for (const annotation of metadataAnnotations) {
86
+ apiValue += `\n .${annotation}`
87
+ }
88
+ if (groups.length > 0) {
89
+ apiValue += `\n .add(${groups.map((group) => group.constName).join(", ")})`
90
+ }
91
+ apiValue += ` {}`
92
+
93
+ return [
94
+ ...security.securityDeclarations,
95
+ ...security.middlewareDeclarations,
96
+ ...groupSources,
97
+ apiValue
98
+ ].join("\n\n")
99
+ }
100
+
101
+ const groupOperations = (parsed: ParsedOpenApi): ReadonlyArray<GroupRenderModel> => {
102
+ const tagMetadata = new Map(parsed.tags.map((tag) => [tag.name, tag]))
103
+ const byIdentifier = new Map<string, {
104
+ readonly identifier: string
105
+ topLevel: boolean
106
+ readonly metadata: ParsedOpenApiTag | undefined
107
+ readonly operations: Array<ParsedOperation>
108
+ }>()
109
+
110
+ for (const operation of parsed.operations) {
111
+ const identifier = operation.tags[0] ?? fallbackGroupIdentifier
112
+ const topLevel = operation.tags.length === 0
113
+ const existing = byIdentifier.get(identifier)
114
+ if (existing) {
115
+ if (topLevel) {
116
+ existing.topLevel = true
117
+ }
118
+ existing.operations.push(operation)
119
+ continue
120
+ }
121
+ byIdentifier.set(identifier, {
122
+ identifier,
123
+ topLevel,
124
+ metadata: tagMetadata.get(identifier),
125
+ operations: [operation]
126
+ })
127
+ }
128
+
129
+ const allocateName = makeNameAllocator()
130
+ const groups: Array<GroupRenderModel> = []
131
+ for (const group of byIdentifier.values()) {
132
+ const baseName = ensureIdentifier(group.identifier, "Group")
133
+ groups.push({
134
+ ...group,
135
+ constName: allocateName(`${baseName}Group`)
136
+ })
137
+ }
138
+ return groups
139
+ }
140
+
141
+ const renderGroup = (
142
+ group: GroupRenderModel,
143
+ endpointMiddlewares: ReadonlyMap<string, ReadonlyArray<string>>
144
+ ): string => {
145
+ let source = `class ${group.constName} extends HttpApiGroup.make(${JSON.stringify(group.identifier)}${
146
+ group.topLevel ? ", { topLevel: true }" : ""
147
+ })`
148
+
149
+ const allocateEndpointName = makeNameAllocator()
150
+ const endpointSources = group.operations.map((operation) =>
151
+ renderEndpoint(
152
+ operation,
153
+ allocateEndpointName(operation.id),
154
+ endpointMiddlewares.get(toOperationKey(operation)) ?? []
155
+ )
156
+ )
157
+ if (endpointSources.length > 0) {
158
+ source += `\n .add(${endpointSources.join(", \n ")})`
159
+ }
160
+
161
+ if (group.metadata?.description !== undefined) {
162
+ source += `\n .annotate(OpenApi.Description, ${JSON.stringify(group.metadata.description)})`
163
+ }
164
+ if (group.metadata?.externalDocs !== undefined) {
165
+ source += `\n .annotate(OpenApi.ExternalDocs, ${JSON.stringify(group.metadata.externalDocs)})`
166
+ }
167
+
168
+ source += ` {}`
169
+
170
+ return source
171
+ }
172
+
173
+ const renderEndpoint = (
174
+ operation: ParsedOperation,
175
+ endpointName: string,
176
+ endpointMiddlewares: ReadonlyArray<string>
177
+ ): string => {
178
+ const options: Array<string> = []
179
+ if (operation.pathSchema !== undefined) {
180
+ options.push(`params: ${operation.pathSchema}`)
181
+ }
182
+ if (operation.querySchema !== undefined) {
183
+ options.push(`query: ${operation.querySchema}`)
184
+ }
185
+ if (operation.headersSchema !== undefined) {
186
+ options.push(`headers: ${operation.headersSchema}`)
187
+ }
188
+
189
+ const payload = renderPayload(operation)
190
+ if (payload !== undefined) {
191
+ options.push(`payload: ${payload}`)
192
+ }
193
+
194
+ const success = renderResponseSet(operation.responses, "success")
195
+ if (success !== undefined) {
196
+ options.push(`success: ${success}`)
197
+ }
198
+
199
+ const error = renderResponseSet(operation.responses, "error")
200
+ if (error !== undefined) {
201
+ options.push(`error: ${error}`)
202
+ }
203
+
204
+ const endpoint = options.length === 0
205
+ ? `HttpApiEndpoint.${operation.method}(${JSON.stringify(endpointName)}, ${
206
+ JSON.stringify(toHttpApiPath(operation.path))
207
+ })`
208
+ : `HttpApiEndpoint.${operation.method}(${JSON.stringify(endpointName)}, ${
209
+ JSON.stringify(toHttpApiPath(operation.path))
210
+ }, { ${options.join(", ")} })`
211
+
212
+ const annotations: Array<string> = []
213
+ if (operation.operationId !== undefined) {
214
+ annotations.push(`annotate(OpenApi.Identifier, ${JSON.stringify(operation.operationId)})`)
215
+ }
216
+ if (operation.metadata.summary !== undefined) {
217
+ annotations.push(`annotate(OpenApi.Summary, ${JSON.stringify(operation.metadata.summary)})`)
218
+ }
219
+ if (operation.metadata.description !== undefined) {
220
+ annotations.push(`annotate(OpenApi.Description, ${JSON.stringify(operation.metadata.description)})`)
221
+ }
222
+ if (operation.metadata.deprecated) {
223
+ annotations.push(`annotate(OpenApi.Deprecated, true)`)
224
+ }
225
+ if (operation.metadata.externalDocs !== undefined) {
226
+ annotations.push(`annotate(OpenApi.ExternalDocs, ${JSON.stringify(operation.metadata.externalDocs)})`)
227
+ }
228
+
229
+ if (annotations.length === 0 && endpointMiddlewares.length === 0) {
230
+ return endpoint
231
+ }
232
+
233
+ let out = endpoint
234
+ for (const middleware of endpointMiddlewares) {
235
+ out += `\n .middleware(${middleware})`
236
+ }
237
+ for (const annotation of annotations) {
238
+ out += `\n .${annotation}`
239
+ }
240
+ return out
241
+ }
242
+
243
+ const renderPayload = (operation: ParsedOperation): string | undefined => {
244
+ if (!methodSupportsBody(operation.method)) {
245
+ return
246
+ }
247
+ const payloads = operation.requestBodyRepresentable.map((schema) => renderMediaSchema(schema))
248
+ if (payloads.length === 0) {
249
+ return
250
+ }
251
+
252
+ if (operation.requestBody?.required === false) {
253
+ payloads.unshift("HttpApiSchema.NoContent")
254
+ }
255
+
256
+ return joinSchemas(payloads)
257
+ }
258
+
259
+ const renderResponseSet = (
260
+ responses: ReadonlyArray<ParsedOperationResponse>,
261
+ target: "success" | "error"
262
+ ): string | undefined => {
263
+ const rendered: Array<string> = []
264
+
265
+ for (const response of responses) {
266
+ const status = toStatus(response.status)
267
+ if (status === undefined) {
268
+ continue
269
+ }
270
+
271
+ const isSuccess = status < 400
272
+ if ((target === "success") !== isSuccess) {
273
+ continue
274
+ }
275
+
276
+ if (response.isEmpty) {
277
+ rendered.push(`HttpApiSchema.Empty(${status})`)
278
+ continue
279
+ }
280
+
281
+ for (const media of response.representable) {
282
+ rendered.push(applyStatus(renderMediaSchema(media), status, target))
283
+ }
284
+ }
285
+
286
+ if (rendered.length === 0) {
287
+ return
288
+ }
289
+
290
+ return joinSchemas(rendered)
291
+ }
292
+
293
+ const joinSchemas = (schemas: ReadonlyArray<string>): string =>
294
+ schemas.length === 1 ? schemas[0] : `[${schemas.join(", ")}]`
295
+
296
+ const renderMediaSchema = (media: ParsedOperationMediaTypeSchema): string => {
297
+ if (media.effectStream === "sse") {
298
+ const options = media.contentType === "text/event-stream"
299
+ ? `{ events: ${media.schema}, error: ${media.errorSchema} }`
300
+ : `{ contentType: ${JSON.stringify(media.contentType)}, events: ${media.schema}, error: ${media.errorSchema} }`
301
+ return `HttpApiSchema.StreamSse(${options})`
302
+ }
303
+
304
+ if (media.effectStream === "uint8array") {
305
+ if (media.contentType === "application/octet-stream") {
306
+ return "HttpApiSchema.StreamUint8Array()"
307
+ }
308
+ return `HttpApiSchema.StreamUint8Array({ contentType: ${JSON.stringify(media.contentType)} })`
309
+ }
310
+
311
+ switch (media.encoding) {
312
+ case "json": {
313
+ if (media.contentType === "application/json") {
314
+ return media.schema
315
+ }
316
+ return `${media.schema}.pipe(HttpApiSchema.asJson({ contentType: ${JSON.stringify(media.contentType)} }))`
317
+ }
318
+ case "multipart": {
319
+ return `${media.schema}.pipe(HttpApiSchema.asMultipart())`
320
+ }
321
+ case "form-url-encoded": {
322
+ if (media.contentType === "application/x-www-form-urlencoded") {
323
+ return `${media.schema}.pipe(HttpApiSchema.asFormUrlEncoded())`
324
+ }
325
+ return `${media.schema}.pipe(HttpApiSchema.asFormUrlEncoded({ contentType: ${
326
+ JSON.stringify(media.contentType)
327
+ } }))`
328
+ }
329
+ case "text": {
330
+ if (media.contentType === "text/plain") {
331
+ return `${media.schema}.pipe(HttpApiSchema.asText())`
332
+ }
333
+ return `${media.schema}.pipe(HttpApiSchema.asText({ contentType: ${JSON.stringify(media.contentType)} }))`
334
+ }
335
+ case "binary": {
336
+ if (media.contentType === "application/octet-stream") {
337
+ return `${media.schema}.pipe(HttpApiSchema.asUint8Array())`
338
+ }
339
+ return `${media.schema}.pipe(HttpApiSchema.asUint8Array({ contentType: ${JSON.stringify(media.contentType)} }))`
340
+ }
341
+ }
342
+ }
343
+
344
+ const renderApiAnnotations = (parsed: ParsedOpenApi): ReadonlyArray<string> => {
345
+ const annotations: Array<string> = [
346
+ `annotate(OpenApi.Title, ${JSON.stringify(parsed.metadata.title)})`,
347
+ `annotate(OpenApi.Version, ${JSON.stringify(parsed.metadata.version)})`
348
+ ]
349
+
350
+ if (parsed.metadata.summary !== undefined) {
351
+ annotations.push(`annotate(OpenApi.Summary, ${JSON.stringify(parsed.metadata.summary)})`)
352
+ }
353
+ if (parsed.metadata.description !== undefined) {
354
+ annotations.push(`annotate(OpenApi.Description, ${JSON.stringify(parsed.metadata.description)})`)
355
+ }
356
+ if (parsed.metadata.license !== undefined) {
357
+ annotations.push(`annotate(OpenApi.License, ${JSON.stringify(parsed.metadata.license)})`)
358
+ }
359
+ if (parsed.metadata.servers !== undefined) {
360
+ annotations.push(`annotate(OpenApi.Servers, ${JSON.stringify(parsed.metadata.servers)})`)
361
+ }
362
+
363
+ return annotations
364
+ }
365
+
366
+ const buildSecurityRenderModel = (parsed: ParsedOpenApi): SecurityRenderModel => {
367
+ const allocateName = makeNameAllocator()
368
+ const securityDeclarations: Array<string> = []
369
+ const middlewareDeclarations: Array<string> = []
370
+ const endpointMiddlewares = new Map<string, ReadonlyArray<string>>()
371
+ const schemeDeclarations = new Map<string, string>()
372
+ const middlewareNames = new Map<string, string>()
373
+
374
+ for (const securityScheme of parsed.securitySchemes) {
375
+ const baseName = ensureIdentifier(securityScheme.name, "Security")
376
+ const declarationName = allocateName(`${baseName}Security`)
377
+ schemeDeclarations.set(securityScheme.name, declarationName)
378
+ securityDeclarations.push(`export const ${declarationName} = ${renderSecurityScheme(securityScheme)}`)
379
+ }
380
+
381
+ for (const operation of parsed.operations) {
382
+ if (operation.effectiveSecurity.length === 0) {
383
+ continue
384
+ }
385
+ if (operation.effectiveSecurity.some((requirement) => Object.keys(requirement).length === 0)) {
386
+ continue
387
+ }
388
+
389
+ const operationMiddlewareNames: Array<string> = []
390
+ const seenOrSchemes = new Set<string>()
391
+ const andRequirements: Array<ReadonlyArray<string>> = []
392
+
393
+ for (const requirement of operation.effectiveSecurity) {
394
+ const schemes = Object.keys(requirement)
395
+ if (schemes.length === 1) {
396
+ const schemeName = schemes[0]
397
+ if (schemeDeclarations.has(schemeName) && !seenOrSchemes.has(schemeName)) {
398
+ seenOrSchemes.add(schemeName)
399
+ }
400
+ } else if (schemes.length > 1) {
401
+ andRequirements.push([...schemes].sort())
402
+ }
403
+ }
404
+
405
+ const orSchemeNames = Array.from(seenOrSchemes).sort()
406
+ if (orSchemeNames.length > 0) {
407
+ const className = getOrSecurityMiddlewareName(orSchemeNames)
408
+ operationMiddlewareNames.push(className)
409
+ }
410
+
411
+ const seenAndRequirements = new Set<string>()
412
+ for (const requirement of andRequirements) {
413
+ const key = requirement.join("\u0000")
414
+ if (seenAndRequirements.has(key)) {
415
+ continue
416
+ }
417
+ seenAndRequirements.add(key)
418
+ const className = getAndSecurityMiddlewareName(requirement)
419
+ operationMiddlewareNames.push(className)
420
+ }
421
+
422
+ if (operationMiddlewareNames.length > 0) {
423
+ endpointMiddlewares.set(toOperationKey(operation), operationMiddlewareNames)
424
+ }
425
+ }
426
+
427
+ return {
428
+ securityDeclarations,
429
+ middlewareDeclarations,
430
+ endpointMiddlewares
431
+ }
432
+
433
+ function getOrSecurityMiddlewareName(schemes: ReadonlyArray<string>): string {
434
+ const key = `or:${schemes.join("\u0000")}`
435
+ const existing = middlewareNames.get(key)
436
+ if (existing !== undefined) {
437
+ return existing
438
+ }
439
+
440
+ const className = allocateName(`${getSecurityMiddlewareBaseName(schemes, "Or")}SecurityMiddleware`)
441
+ const securityEntries = schemes.map((name) => `${JSON.stringify(name)}: ${schemeDeclarations.get(name)!}`).join(
442
+ ", "
443
+ )
444
+ middlewareDeclarations.push(
445
+ `export class ${className} extends HttpApiMiddleware.Service<${className}>()(${
446
+ JSON.stringify(`${schemes.join(" | ")} security`)
447
+ }, { security: { ${securityEntries} } }) {}`
448
+ )
449
+ middlewareNames.set(key, className)
450
+ return className
451
+ }
452
+
453
+ function getAndSecurityMiddlewareName(schemes: ReadonlyArray<string>): string {
454
+ const key = `and:${schemes.join("\u0000")}`
455
+ const existing = middlewareNames.get(key)
456
+ if (existing !== undefined) {
457
+ return existing
458
+ }
459
+
460
+ const className = allocateName(`${getSecurityMiddlewareBaseName(schemes, "And")}SecurityMiddleware`)
461
+ middlewareDeclarations.push(
462
+ `class ${className} extends HttpApiMiddleware.Service<${className}>()(${
463
+ JSON.stringify(`${schemes.join(" & ")} security`)
464
+ }) {}`
465
+ )
466
+ middlewareNames.set(key, className)
467
+ return className
468
+ }
469
+ }
470
+
471
+ const getSecurityMiddlewareBaseName = (
472
+ schemes: ReadonlyArray<string>,
473
+ joiner: "And" | "Or"
474
+ ): string => {
475
+ const [head, ...tail] = schemes.map((scheme) => ensureIdentifier(scheme, "Security"))
476
+ return tail.length === 0 ? head : [head, ...tail.map((scheme) => `${joiner}${scheme}`)].join("")
477
+ }
478
+
479
+ const renderSecurityScheme = (securityScheme: ParsedOpenApiSecurityScheme): string => {
480
+ let source: string
481
+ switch (securityScheme.type) {
482
+ case "basic": {
483
+ source = "HttpApiSecurity.basic"
484
+ break
485
+ }
486
+ case "bearer": {
487
+ source = "HttpApiSecurity.bearer"
488
+ break
489
+ }
490
+ case "http": {
491
+ source = `HttpApiSecurity.http({ scheme: ${JSON.stringify(securityScheme.scheme!)} })`
492
+ break
493
+ }
494
+ case "apiKey": {
495
+ source = `HttpApiSecurity.apiKey({ key: ${JSON.stringify(securityScheme.key!)}, in: ${
496
+ JSON.stringify(securityScheme.in!)
497
+ } })`
498
+ break
499
+ }
500
+ }
501
+
502
+ if (securityScheme.description !== undefined) {
503
+ source += `.pipe(HttpApiSecurity.annotate(OpenApi.Description, ${JSON.stringify(securityScheme.description)}))`
504
+ }
505
+ if (
506
+ (securityScheme.type === "bearer" || securityScheme.type === "http") && securityScheme.bearerFormat !== undefined
507
+ ) {
508
+ source += `.pipe(HttpApiSecurity.annotate(OpenApi.Format, ${JSON.stringify(securityScheme.bearerFormat)}))`
509
+ }
510
+
511
+ return source
512
+ }
513
+
514
+ const toOperationKey = (operation: ParsedOperation): string => `${operation.method}:${operation.path}`
515
+
516
+ const toHttpApiPath = (path: string): string => path.replace(/{([^}]+)}/g, ":$1")
517
+
518
+ const toStatus = (status: string): number | undefined => {
519
+ if (!/^\d{3}$/.test(status)) {
520
+ return
521
+ }
522
+ return Number(status)
523
+ }
524
+
525
+ const applyStatus = (schema: string, status: number, target: "success" | "error"): string => {
526
+ if ((target === "success" && status === 200) || (target === "error" && status === 500)) {
527
+ return schema
528
+ }
529
+ return `${schema}.pipe(HttpApiSchema.status(${status}))`
530
+ }
531
+
532
+ const methodSupportsBody = (method: ParsedOperation["method"]): boolean =>
533
+ method !== "get" && method !== "head" && method !== "options" && method !== "trace"
534
+
535
+ const ensureIdentifier = (value: string, fallback: string): string => {
536
+ const sanitized = Utils.identifier(value)
537
+ return sanitized.length > 0 ? sanitized : fallback
538
+ }
539
+
540
+ const makeNameAllocator = () => {
541
+ const used = new Set<string>()
542
+ return (base: string) => {
543
+ let candidate = base
544
+ let index = 2
545
+ while (used.has(candidate)) {
546
+ candidate = `${base}${index}`
547
+ index += 1
548
+ }
549
+ used.add(candidate)
550
+ return candidate
551
+ }
552
+ }