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