| 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/http"`] : []), |
| 59 | `import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity, OpenApi } from "effect/http-api"` |
| 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 | } |