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

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 +383 -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 +707 -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 +172 -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 +538 -0
  35. package/src/JsonSchemaGenerator.ts +272 -47
  36. package/src/OpenApiGenerator.ts +994 -173
  37. package/src/OpenApiPatch.ts +56 -31
  38. package/src/OpenApiTransformer.ts +89 -12
  39. package/src/ParsedOperation.ts +220 -1
  40. package/src/Utils.ts +61 -0
  41. package/src/main.ts +58 -10
@@ -0,0 +1,538 @@
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
+ switch (media.encoding) {
298
+ case "json": {
299
+ if (media.contentType === "application/json") {
300
+ return media.schema
301
+ }
302
+ return `${media.schema}.pipe(HttpApiSchema.asJson({ contentType: ${JSON.stringify(media.contentType)} }))`
303
+ }
304
+ case "multipart": {
305
+ return `${media.schema}.pipe(HttpApiSchema.asMultipart())`
306
+ }
307
+ case "form-url-encoded": {
308
+ if (media.contentType === "application/x-www-form-urlencoded") {
309
+ return `${media.schema}.pipe(HttpApiSchema.asFormUrlEncoded())`
310
+ }
311
+ return `${media.schema}.pipe(HttpApiSchema.asFormUrlEncoded({ contentType: ${
312
+ JSON.stringify(media.contentType)
313
+ } }))`
314
+ }
315
+ case "text": {
316
+ if (media.contentType === "text/plain") {
317
+ return `${media.schema}.pipe(HttpApiSchema.asText())`
318
+ }
319
+ return `${media.schema}.pipe(HttpApiSchema.asText({ contentType: ${JSON.stringify(media.contentType)} }))`
320
+ }
321
+ case "binary": {
322
+ if (media.contentType === "application/octet-stream") {
323
+ return `${media.schema}.pipe(HttpApiSchema.asUint8Array())`
324
+ }
325
+ return `${media.schema}.pipe(HttpApiSchema.asUint8Array({ contentType: ${JSON.stringify(media.contentType)} }))`
326
+ }
327
+ }
328
+ }
329
+
330
+ const renderApiAnnotations = (parsed: ParsedOpenApi): ReadonlyArray<string> => {
331
+ const annotations: Array<string> = [
332
+ `annotate(OpenApi.Title, ${JSON.stringify(parsed.metadata.title)})`,
333
+ `annotate(OpenApi.Version, ${JSON.stringify(parsed.metadata.version)})`
334
+ ]
335
+
336
+ if (parsed.metadata.summary !== undefined) {
337
+ annotations.push(`annotate(OpenApi.Summary, ${JSON.stringify(parsed.metadata.summary)})`)
338
+ }
339
+ if (parsed.metadata.description !== undefined) {
340
+ annotations.push(`annotate(OpenApi.Description, ${JSON.stringify(parsed.metadata.description)})`)
341
+ }
342
+ if (parsed.metadata.license !== undefined) {
343
+ annotations.push(`annotate(OpenApi.License, ${JSON.stringify(parsed.metadata.license)})`)
344
+ }
345
+ if (parsed.metadata.servers !== undefined) {
346
+ annotations.push(`annotate(OpenApi.Servers, ${JSON.stringify(parsed.metadata.servers)})`)
347
+ }
348
+
349
+ return annotations
350
+ }
351
+
352
+ const buildSecurityRenderModel = (parsed: ParsedOpenApi): SecurityRenderModel => {
353
+ const allocateName = makeNameAllocator()
354
+ const securityDeclarations: Array<string> = []
355
+ const middlewareDeclarations: Array<string> = []
356
+ const endpointMiddlewares = new Map<string, ReadonlyArray<string>>()
357
+ const schemeDeclarations = new Map<string, string>()
358
+ const middlewareNames = new Map<string, string>()
359
+
360
+ for (const securityScheme of parsed.securitySchemes) {
361
+ const baseName = ensureIdentifier(securityScheme.name, "Security")
362
+ const declarationName = allocateName(`${baseName}Security`)
363
+ schemeDeclarations.set(securityScheme.name, declarationName)
364
+ securityDeclarations.push(`export const ${declarationName} = ${renderSecurityScheme(securityScheme)}`)
365
+ }
366
+
367
+ for (const operation of parsed.operations) {
368
+ if (operation.effectiveSecurity.length === 0) {
369
+ continue
370
+ }
371
+ if (operation.effectiveSecurity.some((requirement) => Object.keys(requirement).length === 0)) {
372
+ continue
373
+ }
374
+
375
+ const operationMiddlewareNames: Array<string> = []
376
+ const seenOrSchemes = new Set<string>()
377
+ const andRequirements: Array<ReadonlyArray<string>> = []
378
+
379
+ for (const requirement of operation.effectiveSecurity) {
380
+ const schemes = Object.keys(requirement)
381
+ if (schemes.length === 1) {
382
+ const schemeName = schemes[0]
383
+ if (schemeDeclarations.has(schemeName) && !seenOrSchemes.has(schemeName)) {
384
+ seenOrSchemes.add(schemeName)
385
+ }
386
+ } else if (schemes.length > 1) {
387
+ andRequirements.push([...schemes].sort())
388
+ }
389
+ }
390
+
391
+ const orSchemeNames = Array.from(seenOrSchemes).sort()
392
+ if (orSchemeNames.length > 0) {
393
+ const className = getOrSecurityMiddlewareName(orSchemeNames)
394
+ operationMiddlewareNames.push(className)
395
+ }
396
+
397
+ const seenAndRequirements = new Set<string>()
398
+ for (const requirement of andRequirements) {
399
+ const key = requirement.join("\u0000")
400
+ if (seenAndRequirements.has(key)) {
401
+ continue
402
+ }
403
+ seenAndRequirements.add(key)
404
+ const className = getAndSecurityMiddlewareName(requirement)
405
+ operationMiddlewareNames.push(className)
406
+ }
407
+
408
+ if (operationMiddlewareNames.length > 0) {
409
+ endpointMiddlewares.set(toOperationKey(operation), operationMiddlewareNames)
410
+ }
411
+ }
412
+
413
+ return {
414
+ securityDeclarations,
415
+ middlewareDeclarations,
416
+ endpointMiddlewares
417
+ }
418
+
419
+ function getOrSecurityMiddlewareName(schemes: ReadonlyArray<string>): string {
420
+ const key = `or:${schemes.join("\u0000")}`
421
+ const existing = middlewareNames.get(key)
422
+ if (existing !== undefined) {
423
+ return existing
424
+ }
425
+
426
+ const className = allocateName(`${getSecurityMiddlewareBaseName(schemes, "Or")}SecurityMiddleware`)
427
+ const securityEntries = schemes.map((name) => `${JSON.stringify(name)}: ${schemeDeclarations.get(name)!}`).join(
428
+ ", "
429
+ )
430
+ middlewareDeclarations.push(
431
+ `export class ${className} extends HttpApiMiddleware.Service<${className}>()(${
432
+ JSON.stringify(`${schemes.join(" | ")} security`)
433
+ }, { security: { ${securityEntries} } }) {}`
434
+ )
435
+ middlewareNames.set(key, className)
436
+ return className
437
+ }
438
+
439
+ function getAndSecurityMiddlewareName(schemes: ReadonlyArray<string>): string {
440
+ const key = `and:${schemes.join("\u0000")}`
441
+ const existing = middlewareNames.get(key)
442
+ if (existing !== undefined) {
443
+ return existing
444
+ }
445
+
446
+ const className = allocateName(`${getSecurityMiddlewareBaseName(schemes, "And")}SecurityMiddleware`)
447
+ middlewareDeclarations.push(
448
+ `class ${className} extends HttpApiMiddleware.Service<${className}>()(${
449
+ JSON.stringify(`${schemes.join(" & ")} security`)
450
+ }) {}`
451
+ )
452
+ middlewareNames.set(key, className)
453
+ return className
454
+ }
455
+ }
456
+
457
+ const getSecurityMiddlewareBaseName = (
458
+ schemes: ReadonlyArray<string>,
459
+ joiner: "And" | "Or"
460
+ ): string => {
461
+ const [head, ...tail] = schemes.map((scheme) => ensureIdentifier(scheme, "Security"))
462
+ return tail.length === 0 ? head : [head, ...tail.map((scheme) => `${joiner}${scheme}`)].join("")
463
+ }
464
+
465
+ const renderSecurityScheme = (securityScheme: ParsedOpenApiSecurityScheme): string => {
466
+ let source: string
467
+ switch (securityScheme.type) {
468
+ case "basic": {
469
+ source = "HttpApiSecurity.basic"
470
+ break
471
+ }
472
+ case "bearer": {
473
+ source = "HttpApiSecurity.bearer"
474
+ break
475
+ }
476
+ case "http": {
477
+ source = `HttpApiSecurity.http({ scheme: ${JSON.stringify(securityScheme.scheme!)} })`
478
+ break
479
+ }
480
+ case "apiKey": {
481
+ source = `HttpApiSecurity.apiKey({ key: ${JSON.stringify(securityScheme.key!)}, in: ${
482
+ JSON.stringify(securityScheme.in!)
483
+ } })`
484
+ break
485
+ }
486
+ }
487
+
488
+ if (securityScheme.description !== undefined) {
489
+ source += `.pipe(HttpApiSecurity.annotate(OpenApi.Description, ${JSON.stringify(securityScheme.description)}))`
490
+ }
491
+ if (
492
+ (securityScheme.type === "bearer" || securityScheme.type === "http") && securityScheme.bearerFormat !== undefined
493
+ ) {
494
+ source += `.pipe(HttpApiSecurity.annotate(OpenApi.Format, ${JSON.stringify(securityScheme.bearerFormat)}))`
495
+ }
496
+
497
+ return source
498
+ }
499
+
500
+ const toOperationKey = (operation: ParsedOperation): string => `${operation.method}:${operation.path}`
501
+
502
+ const toHttpApiPath = (path: string): string => path.replace(/{([^}]+)}/g, ":$1")
503
+
504
+ const toStatus = (status: string): number | undefined => {
505
+ if (!/^\d{3}$/.test(status)) {
506
+ return
507
+ }
508
+ return Number(status)
509
+ }
510
+
511
+ const applyStatus = (schema: string, status: number, target: "success" | "error"): string => {
512
+ if ((target === "success" && status === 200) || (target === "error" && status === 500)) {
513
+ return schema
514
+ }
515
+ return `${schema}.pipe(HttpApiSchema.status(${status}))`
516
+ }
517
+
518
+ const methodSupportsBody = (method: ParsedOperation["method"]): boolean =>
519
+ method !== "get" && method !== "head" && method !== "options" && method !== "trace"
520
+
521
+ const ensureIdentifier = (value: string, fallback: string): string => {
522
+ const sanitized = Utils.identifier(value)
523
+ return sanitized.length > 0 ? sanitized : fallback
524
+ }
525
+
526
+ const makeNameAllocator = () => {
527
+ const used = new Set<string>()
528
+ return (base: string) => {
529
+ let candidate = base
530
+ let index = 2
531
+ while (used.has(candidate)) {
532
+ candidate = `${base}${index}`
533
+ index += 1
534
+ }
535
+ used.add(candidate)
536
+ return candidate
537
+ }
538
+ }